diff --git a/game/campaignloader/defaultsquadronassigner.py b/game/campaignloader/defaultsquadronassigner.py index adb21dd9..8702496a 100644 --- a/game/campaignloader/defaultsquadronassigner.py +++ b/game/campaignloader/defaultsquadronassigner.py @@ -73,7 +73,7 @@ class DefaultSquadronAssigner: # If we can't find any squadron matching the requirement, we should # create one. return self.air_wing.squadron_def_generator.generate_for_task( - config.primary, control_point + config.primary, control_point, self.game.settings.squadron_random_chance ) def find_preferred_squadron( diff --git a/game/campaignloader/squadrondefgenerator.py b/game/campaignloader/squadrondefgenerator.py index 6871b03d..fffcae7a 100644 --- a/game/campaignloader/squadrondefgenerator.py +++ b/game/campaignloader/squadrondefgenerator.py @@ -21,7 +21,7 @@ class SquadronDefGenerator: self.used_nicknames: set[str] = set() def generate_for_task( - self, task: FlightType, control_point: ControlPoint + self, task: FlightType, control_point: ControlPoint, squadron_random_chance: int ) -> Optional[SquadronDef]: aircraft_choice: Optional[AircraftType] = None for aircraft in AircraftType.priority_list_for_task(task): @@ -30,9 +30,9 @@ class SquadronDefGenerator: if not control_point.can_operate(aircraft): continue aircraft_choice = aircraft - # 50/50 chance to keep looking for an aircraft that isn't as far up the + # squadron_random_chance percent chance to keep looking for an aircraft that isn't as far up the # priority list to maintain some unit variety. - if random.choice([True, False]): + if squadron_random_chance >= random.randint(1, 100): break if aircraft_choice is None: diff --git a/game/game.py b/game/game.py index dfb0c2f7..43598f40 100644 --- a/game/game.py +++ b/game/game.py @@ -157,6 +157,7 @@ class Game: 2: {}, } self.pretense_air_groups: dict[str, Flight] = {} + self.pretense_carrier_zones: List[str] = [] self.on_load(game_still_initializing=True) diff --git a/game/missiongenerator/aircraft/flightgroupspawner.py b/game/missiongenerator/aircraft/flightgroupspawner.py index bec3f522..cc14db7a 100644 --- a/game/missiongenerator/aircraft/flightgroupspawner.py +++ b/game/missiongenerator/aircraft/flightgroupspawner.py @@ -15,6 +15,7 @@ from dcs.planes import ( C_101CC, Su_33, MiG_15bis, + M_2000C, ) from dcs.point import PointAction from dcs.ships import KUZNECOW @@ -35,7 +36,7 @@ from game.missiongenerator.missiondata import MissionData from game.naming import namegen from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn from game.utils import feet, meters -from pydcs_extensions import A_4E_C +from pydcs_extensions import A_4E_C, VSN_F4B, VSN_F4C WARM_START_HELI_ALT = meters(500) WARM_START_ALTITUDE = meters(3000) @@ -400,6 +401,18 @@ class FlightGroupSpawner: group.points[0].type = "TakeOffGround" group.units[0].heading = ground_spawn[0].units[0].heading + if ( + cp.coalition.game.settings.ground_start_airbase_statics_farps_remove + and isinstance(cp, Airfield) + ): + # Remove invisible FARPs from airfields because they are unnecessary + neutral_country = self.mission.country( + cp.coalition.game.neutral_country.name + ) + neutral_country.remove_static_group(ground_spawn[0]) + group.points[0].link_unit = None + group.points[0].helipad_id = None + # Hot start aircraft which require ground power to start, when ground power # trucks have been disabled for performance reasons ground_power_available = ( @@ -410,10 +423,31 @@ class FlightGroupSpawner: and self.flight.coalition.game.settings.ground_start_ground_power_trucks_roadbase ) - if self.start_type is not StartType.COLD or ( - not ground_power_available - and self.flight.unit_type.dcs_unit_type - in [A_4E_C, F_5E_3, F_86F_Sabre, MiG_15bis, F_14A_135_GR, F_14B, C_101CC] + # Also hot start aircraft which require ground crew support (ground air or chock removal) + # which might not be available at roadbases + if ( + self.start_type is not StartType.COLD + or ( + not ground_power_available + and self.flight.unit_type.dcs_unit_type + in [ + A_4E_C, + F_86F_Sabre, + MiG_15bis, + F_14A_135_GR, + F_14B, + C_101CC, + ] + ) + or ( + self.flight.unit_type.dcs_unit_type + in [ + F_5E_3, + M_2000C, + VSN_F4B, + VSN_F4C, + ] + ) ): group.points[0].action = PointAction.FromGroundAreaHot group.points[0].type = "TakeOffGroundHot" @@ -435,6 +469,17 @@ class FlightGroupSpawner: ground_spawn[0].x, ground_spawn[0].y, terrain=terrain ) group.units[1 + i].heading = ground_spawn[0].units[0].heading + + if ( + cp.coalition.game.settings.ground_start_airbase_statics_farps_remove + and isinstance(cp, Airfield) + ): + # Remove invisible FARPs from airfields because they are unnecessary + neutral_country = self.mission.country( + cp.coalition.game.neutral_country.name + ) + neutral_country.remove_static_group(ground_spawn[0]) + except IndexError as ex: raise NoParkingSlotError( f"Not enough STOL slots available at {cp}" diff --git a/game/missiongenerator/missiondata.py b/game/missiongenerator/missiondata.py index 8906feb4..062d6a56 100644 --- a/game/missiongenerator/missiondata.py +++ b/game/missiongenerator/missiondata.py @@ -52,6 +52,8 @@ class CarrierInfo(UnitInfo): """Carrier information.""" tacan: TacanChannel + icls_channel: int | None + link4_freq: RadioFrequency | None @dataclass diff --git a/game/missiongenerator/tgogenerator.py b/game/missiongenerator/tgogenerator.py index ed83823e..43f7052d 100644 --- a/game/missiongenerator/tgogenerator.py +++ b/game/missiongenerator/tgogenerator.py @@ -60,6 +60,7 @@ from game.theater import ( TheaterGroundObject, TheaterUnit, NavalControlPoint, + Airfield, ) from game.theater.theatergroundobject import ( CarrierGroundObject, @@ -626,6 +627,8 @@ class GenericCarrierGenerator(GroundObjectGenerator): callsign=tacan_callsign, freq=atc, tacan=tacan, + icls_channel=icls, + link4_freq=link4, blue=self.control_point.captured, ) ) @@ -940,7 +943,11 @@ class GroundSpawnRoadbaseGenerator: country.id ) - if self.game.position_culled(ground_spawn[0]): + if self.game.settings.ground_start_airbase_statics_farps_remove and isinstance( + self.cp, Airfield + ): + cull_farp_statics = True + elif self.game.position_culled(ground_spawn[0]): cull_farp_statics = True if self.cp.coalition.player: for package in self.cp.coalition.ato.packages: @@ -1072,7 +1079,11 @@ class GroundSpawnGenerator: country.id ) - if self.game.position_culled(vtol_pad[0]): + if self.game.settings.ground_start_airbase_statics_farps_remove and isinstance( + self.cp, Airfield + ): + cull_farp_statics = True + elif self.game.position_culled(vtol_pad[0]): cull_farp_statics = True if self.cp.coalition.player: for package in self.cp.coalition.ato.packages: diff --git a/game/pretense/pretenseaircraftgenerator.py b/game/pretense/pretenseaircraftgenerator.py index f1b0d90c..f8f13157 100644 --- a/game/pretense/pretenseaircraftgenerator.py +++ b/game/pretense/pretenseaircraftgenerator.py @@ -191,13 +191,13 @@ class PretenseAircraftGenerator: """ squadron_def = coalition.air_wing.squadron_def_generator.generate_for_task( - flight_type, cp + flight_type, cp, self.game.settings.squadron_random_chance ) for retries in range(num_retries): if squadron_def is None or fixed_wing == squadron_def.aircraft.helicopter: squadron_def = ( coalition.air_wing.squadron_def_generator.generate_for_task( - flight_type, cp + flight_type, cp, self.game.settings.squadron_random_chance ) ) @@ -302,7 +302,10 @@ class PretenseAircraftGenerator: # First check what are the capabilities of the squadrons on this CP for squadron in cp.squadrons: for task in sead_tasks: - if task in squadron.auto_assignable_mission_types: + if ( + task in squadron.auto_assignable_mission_types + or FlightType.DEAD in squadron.auto_assignable_mission_types + ): sead_capable_cp = True for task in strike_tasks: if task in squadron.auto_assignable_mission_types: @@ -360,6 +363,8 @@ class PretenseAircraftGenerator: continue if cp.coalition != squadron.coalition: continue + if num_of_sead >= self.game.settings.pretense_sead_flights_per_cp: + break mission_types = squadron.auto_assignable_mission_types if ( @@ -400,6 +405,11 @@ class PretenseAircraftGenerator: continue if cp.coalition != squadron.coalition: continue + if ( + num_of_strike + >= self.game.settings.pretense_strike_flights_per_cp + ): + break mission_types = squadron.auto_assignable_mission_types for task in strike_tasks: @@ -422,6 +432,8 @@ class PretenseAircraftGenerator: continue if cp.coalition != squadron.coalition: continue + if num_of_cap >= self.game.settings.pretense_barcap_flights_per_cp: + break mission_types = squadron.auto_assignable_mission_types for task in patrol_tasks: @@ -444,6 +456,8 @@ class PretenseAircraftGenerator: continue if cp.coalition != squadron.coalition: continue + if num_of_cas >= self.game.settings.pretense_cas_flights_per_cp: + break mission_types = squadron.auto_assignable_mission_types if ( @@ -467,6 +481,8 @@ class PretenseAircraftGenerator: continue if cp.coalition != squadron.coalition: continue + if num_of_bai >= self.game.settings.pretense_bai_flights_per_cp: + break mission_types = squadron.auto_assignable_mission_types if FlightType.BAI in mission_types: diff --git a/game/pretense/pretenseluagenerator.py b/game/pretense/pretenseluagenerator.py index 971588a7..5a13d8fb 100644 --- a/game/pretense/pretenseluagenerator.py +++ b/game/pretense/pretenseluagenerator.py @@ -7,13 +7,15 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, List, Type from dcs import Mission from dcs.action import DoScript, DoScriptFile +from dcs.ships import Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal from dcs.translation import String from dcs.triggers import TriggerStart -from dcs.vehicles import AirDefence +from dcs.unittype import VehicleType, ShipType +from dcs.vehicles import AirDefence, Unarmed from game.ato import FlightType from game.coalition import Coalition @@ -283,6 +285,7 @@ class PretenseLuaGenerator(LuaGenerator): "nasamsb", "nasamsc", "rapier", + "roland", "irondome", "davidsling", ]: @@ -381,6 +384,8 @@ class PretenseLuaGenerator(LuaGenerator): == AirDefence.Rapier_fsa_launcher ): sam_presets["rapier"].enabled = True + if ground_unit.unit_type.dcs_unit_type == AirDefence.Roland_ADS: + sam_presets["roland"].enabled = True if ground_unit.unit_type.dcs_unit_type == IRON_DOME_LN: sam_presets["irondome"].enabled = True if ground_unit.unit_type.dcs_unit_type == DAVID_SLING_LN: @@ -526,22 +531,9 @@ class PretenseLuaGenerator(LuaGenerator): cp_name_trimmed = "".join([i for i in cp_name.lower() if i.isalpha()]) cp_side_str = "blue" if cp_side == PRETENSE_BLUE_SIDE else "red" - if cp_side == PRETENSE_BLUE_SIDE: - if random.randint(0, 1): - supply_ship = "shipSupplyTilde" - else: - supply_ship = "shipLandingShipLstMk2" - tanker_ship = "shipTankerSeawisegiant" - command_ship = "shipLandingShipSamuelChase" - ship_group = "blueShipGroup" - else: - if random.randint(0, 1): - supply_ship = "shipBulkerYakushev" - else: - supply_ship = "shipCargoIvanov" - tanker_ship = "shipTankerElnya" - command_ship = "shipLandingShipRopucha" - ship_group = "redShipGroup" + supply_ship = "oilPump" + tanker_ship = "chemTank" + command_ship = "comCenter" lua_string_zones += ( " presets.upgrades.supply." + supply_ship + ":extend({\n" @@ -581,7 +573,7 @@ class PretenseLuaGenerator(LuaGenerator): lua_string_zones += " }\n" lua_string_zones += " }),\n" lua_string_zones += ( - " presets.upgrades.attack." + command_ship + ":extend({\n" + " presets.upgrades.airdef." + command_ship + ":extend({\n" ) lua_string_zones += ( f" name = '{cp_name_trimmed}-mission-command-" @@ -592,11 +584,9 @@ class PretenseLuaGenerator(LuaGenerator): lua_string_zones += ( " presets.defenses." + cp_side_str - + "." - + ship_group - + ":extend({ name='" + + ".shorad:extend({ name='" + cp_name_trimmed - + "-sam-" + + "-shorad-" + cp_side_str + "' }),\n" ) @@ -719,6 +709,8 @@ class PretenseLuaGenerator(LuaGenerator): return lua_string_zones def generate_pretense_zone_land(self, cp_name: str) -> str: + is_artillery_zone = random.choice([True, False]) + lua_string_zones = "" cp_name_trimmed = "".join([i for i in cp_name.lower() if i.isalpha()]) @@ -727,11 +719,12 @@ class PretenseLuaGenerator(LuaGenerator): lua_string_zones += " presets.upgrades.basic.tent:extend({\n" lua_string_zones += f" name='{cp_name_trimmed}-tent-red',\n" lua_string_zones += " products = {\n" - lua_string_zones += ( - " presets.special.red.infantry:extend({ name='" - + cp_name_trimmed - + "-defense-red'})\n" - ) + if not is_artillery_zone: + lua_string_zones += ( + " presets.special.red.infantry:extend({ name='" + + cp_name_trimmed + + "-defense-red'})\n" + ) lua_string_zones += " }\n" lua_string_zones += " }),\n" lua_string_zones += " presets.upgrades.basic.comPost:extend({\n" @@ -742,13 +735,25 @@ class PretenseLuaGenerator(LuaGenerator): + cp_name_trimmed + "-defense-red'}),\n" ) - lua_string_zones += ( - " presets.defenses.red.infantry:extend({ name='" - + cp_name_trimmed - + "-garrison-red' })\n" - ) + if not is_artillery_zone: + lua_string_zones += ( + " presets.defenses.red.infantry:extend({ name='" + + cp_name_trimmed + + "-garrison-red' })\n" + ) lua_string_zones += " }\n" lua_string_zones += " }),\n" + if is_artillery_zone: + lua_string_zones += " presets.upgrades.basic.artyBunker:extend({\n" + lua_string_zones += f" name='{cp_name_trimmed}-arty-red',\n" + lua_string_zones += " products = {\n" + lua_string_zones += ( + " presets.defenses.red.artillery:extend({ name='" + + cp_name_trimmed + + "-artillery-red'})\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" lua_string_zones += self.generate_pretense_land_upgrade_supply( cp_name, PRETENSE_RED_SIDE @@ -760,11 +765,12 @@ class PretenseLuaGenerator(LuaGenerator): lua_string_zones += " presets.upgrades.basic.tent:extend({\n" lua_string_zones += f" name='{cp_name_trimmed}-tent-blue',\n" lua_string_zones += " products = {\n" - lua_string_zones += ( - " presets.special.blue.infantry:extend({ name='" - + cp_name_trimmed - + "-defense-blue'})\n" - ) + if not is_artillery_zone: + lua_string_zones += ( + " presets.special.blue.infantry:extend({ name='" + + cp_name_trimmed + + "-defense-blue'})\n" + ) lua_string_zones += " }\n" lua_string_zones += " }),\n" lua_string_zones += " presets.upgrades.basic.comPost:extend({\n" @@ -775,13 +781,25 @@ class PretenseLuaGenerator(LuaGenerator): + cp_name_trimmed + "-defense-blue'}),\n" ) - lua_string_zones += ( - " presets.defenses.blue.infantry:extend({ name='" - + cp_name_trimmed - + "-garrison-blue' })\n" - ) + if not is_artillery_zone: + lua_string_zones += ( + " presets.defenses.blue.infantry:extend({ name='" + + cp_name_trimmed + + "-garrison-blue' })\n" + ) lua_string_zones += " }\n" lua_string_zones += " }),\n" + if is_artillery_zone: + lua_string_zones += " presets.upgrades.basic.artyBunker:extend({\n" + lua_string_zones += f" name='{cp_name_trimmed}-arty-blue',\n" + lua_string_zones += " products = {\n" + lua_string_zones += ( + " presets.defenses.blue.artillery:extend({ name='" + + cp_name_trimmed + + "-artillery-blue'})\n" + ) + lua_string_zones += " }\n" + lua_string_zones += " }),\n" lua_string_zones += self.generate_pretense_land_upgrade_supply( cp_name, PRETENSE_BLUE_SIDE @@ -816,14 +834,204 @@ class PretenseLuaGenerator(LuaGenerator): return lua_string_zones + def generate_pretense_carrier_zones(self) -> str: + lua_string_carrier_zones = "cmap1 = CarrierMap:new({" + for zone_name in self.game.pretense_carrier_zones: + lua_string_carrier_zones += f'"{zone_name}",' + lua_string_carrier_zones += "})\n" + + return lua_string_carrier_zones + + def generate_pretense_carriers( + self, + cp_name: str, + cp_side: int, + cp_carrier_group_type: Type[ShipType] | None, + cp_carrier_group_name: str | None, + ) -> str: + lua_string_carrier = "\n" + cp_name_trimmed = "".join([i for i in cp_name.lower() if i.isalpha()]) + + link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal] + is_link4carrier = False + carrier_unit_name = "" + icls_channel = 10 + link4_freq = 339000000 + tacan_channel = 44 + tacan_callsign = "" + radio = 137500000 + if cp_carrier_group_type is not None: + if cp_carrier_group_type in link4carriers: + is_link4carrier = True + else: + return lua_string_carrier + if cp_carrier_group_name is None: + return lua_string_carrier + + for carrier in self.mission_data.carriers: + if cp_carrier_group_name == carrier.group_name: + carrier_unit_name = carrier.unit_name + tacan_channel = carrier.tacan.number + tacan_callsign = carrier.callsign + radio = carrier.freq.hertz + if carrier.link4_freq is not None: + link4_freq = carrier.link4_freq.hertz + if carrier.icls_channel is not None: + icls_channel = carrier.icls_channel + break + + lua_string_carrier += ( + f'{cp_name_trimmed} = CarrierCommand:new("' + + carrier_unit_name + + '", 3000, cmap1:getNavMap(), ' + + "{\n" + ) + if is_link4carrier: + lua_string_carrier += " icls = " + str(icls_channel) + ",\n" + lua_string_carrier += " acls = true,\n" + lua_string_carrier += " link4 = " + str(link4_freq) + ",\n" + lua_string_carrier += ( + " tacan = {channel = " + + str(tacan_channel) + + ', callsign="' + + tacan_callsign + + '"},\n' + ) + lua_string_carrier += " radio = " + str(radio) + "\n" + lua_string_carrier += "}, 30000)\n" + + for mission_type in self.game.pretense_air[cp_side][cp_name_trimmed]: + if mission_type == FlightType.SEAD: + mission_name = "supportTypes.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 1000, CarrierCommand.{mission_name}, ' + + "{altitude = 25000, expend=AI.Task.WeaponExpend.ALL})\n" + ) + elif mission_type == FlightType.CAS: + mission_name = "supportTypes.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 1000, CarrierCommand.{mission_name}, ' + + "{altitude = 15000, expend=AI.Task.WeaponExpend.ONE})\n" + ) + elif mission_type == FlightType.BAI: + mission_name = "supportTypes.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 1000, CarrierCommand.{mission_name}, ' + + "{altitude = 10000, expend=AI.Task.WeaponExpend.ONE})\n" + ) + elif mission_type == FlightType.STRIKE: + mission_name = "supportTypes.strike" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 2000, CarrierCommand.{mission_name}, ' + + "{altitude = 20000})\n" + ) + elif mission_type == FlightType.BARCAP: + mission_name = "supportTypes.cap" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 1000, CarrierCommand.{mission_name}, ' + + "{altitude = 25000, range=25})\n" + ) + elif mission_type == FlightType.REFUELING: + mission_name = "supportTypes.tanker" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + tanker_freq = 257.0 + tanker_tacan = 37.0 + tanker_variant = "Drogue" + for tanker in self.mission_data.tankers: + if tanker.group_name == air_group: + tanker_freq = tanker.freq.hertz / 1000000 + tanker_tacan = tanker.tacan.number if tanker.tacan else 0.0 + if tanker.variant == "KC-135 Stratotanker": + tanker_variant = "Boom" + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 3000, CarrierCommand.{mission_name}, ' + + "{altitude = 19000, freq=" + + str(tanker_freq) + + ", tacan=" + + str(tanker_tacan) + + "})\n" + ) + elif mission_type == FlightType.AEWC: + mission_name = "supportTypes.awacs" + for air_group in self.game.pretense_air[cp_side][cp_name_trimmed][ + mission_type + ]: + awacs_freq = 257.5 + for awacs in self.mission_data.awacs: + if awacs.group_name == air_group: + awacs_freq = awacs.freq.hertz / 1000000 + lua_string_carrier += ( + f'{cp_name_trimmed}:addSupportFlight("{air_group}", 5000, CarrierCommand.{mission_name}, ' + + "{altitude = 30000, freq=" + + str(awacs_freq) + + "})\n" + ) + + # lua_string_carrier += f'{cp_name_trimmed}:addExtraSupport("BGM-109B", 10000, CarrierCommand.supportTypes.mslstrike, ' + '{salvo = 10, wpType = \'weapons.missiles.BGM_109B\'})\n' + + return lua_string_carrier + def get_ground_unit( self, coalition: Coalition, side: int, desired_unit_classes: list[UnitClass] ) -> str: + ammo_trucks: List[Type[VehicleType]] = [ + Unarmed.S_75_ZIL, + Unarmed.GAZ_3308, + Unarmed.GAZ_66, + Unarmed.KAMAZ_Truck, + Unarmed.KrAZ6322, + Unarmed.Ural_375, + Unarmed.Ural_375_PBU, + Unarmed.Ural_4320_31, + Unarmed.Ural_4320T, + Unarmed.ZIL_135, + Unarmed.Blitz_36_6700A, + Unarmed.M_818, + Unarmed.Bedford_MWD, + ] + for unit_class in desired_unit_classes: if coalition.faction.has_access_to_unit_class(unit_class): dcs_unit_type = PretenseGroundObjectGenerator.ground_unit_of_class( coalition=coalition, unit_class=unit_class ) + if ( + dcs_unit_type is not None + and unit_class == UnitClass.LOGISTICS + and dcs_unit_type.dcs_unit_type.__class__ not in ammo_trucks + ): + # ground_unit_of_class returned a logistics unit not capable of ammo resupply + # Retry up to 10 times + for truck_retry in range(10): + dcs_unit_type = ( + PretenseGroundObjectGenerator.ground_unit_of_class( + coalition=coalition, unit_class=unit_class + ) + ) + if ( + dcs_unit_type is not None + and dcs_unit_type.dcs_unit_type in ammo_trucks + ): + break + else: + dcs_unit_type = None if dcs_unit_type is not None: return dcs_unit_type.dcs_id @@ -849,6 +1057,11 @@ class PretenseLuaGenerator(LuaGenerator): return "LAV-25" else: return "BTR-80" + elif desired_unit_classes[0] == UnitClass.ARTILLERY: + if side == PRETENSE_BLUE_SIDE: + return "M-109" + else: + return "SAU Gvozdika" elif desired_unit_classes[0] == UnitClass.RECON: if side == PRETENSE_BLUE_SIDE: return "M1043 HMMWV Armament" @@ -865,10 +1078,16 @@ class PretenseLuaGenerator(LuaGenerator): else: return "KS-19" elif desired_unit_classes[0] == UnitClass.MANPAD: - if side == PRETENSE_BLUE_SIDE: - return "Soldier stinger" + if coalition.game.date.year >= 1990: + if side == PRETENSE_BLUE_SIDE: + return "Soldier stinger" + else: + return "SA-18 Igla manpad" else: - return "SA-18 Igla manpad" + if side == PRETENSE_BLUE_SIDE: + return "Soldier M4" + else: + return "Infantry AK" elif desired_unit_classes[0] == UnitClass.LOGISTICS: if side == PRETENSE_BLUE_SIDE: return "M 818" @@ -910,6 +1129,26 @@ class PretenseLuaGenerator(LuaGenerator): lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" lua_string_ground_groups += "}\n" + lua_string_ground_groups += ( + 'TemplateDB.templates["artillery-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.ARTILLERY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.ARTILLERY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.ARTILLERY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.ARTILLERY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.INFANTRY])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.MANPAD, UnitClass.INFANTRY])}"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + lua_string_ground_groups += ( 'TemplateDB.templates["defense-' + side_str + '"] = {\n' ) @@ -1173,6 +1412,30 @@ class PretenseLuaGenerator(LuaGenerator): lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" lua_string_ground_groups += "}\n" + lua_string_ground_groups += ( + 'TemplateDB.templates["roland-' + side_str + '"] = {\n' + ) + lua_string_ground_groups += " units = {\n" + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.AAA, UnitClass.SHORAD, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.LOGISTICS])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += f' "{self.get_ground_unit(coalition, side, [UnitClass.SHORAD, UnitClass.AAA, UnitClass.MANPAD])}",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland ADS",\n' + lua_string_ground_groups += ' "Roland Radar",\n' + lua_string_ground_groups += ' "Roland Radar",\n' + lua_string_ground_groups += ' "Roland Radar"\n' + lua_string_ground_groups += " },\n" + lua_string_ground_groups += " maxDist = 300,\n" + lua_string_ground_groups += f' skill = "{skill_str}",\n' + lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" + lua_string_ground_groups += "}\n" + return lua_string_ground_groups @staticmethod @@ -1219,7 +1482,6 @@ class PretenseLuaGenerator(LuaGenerator): + str(self.game.settings.pretense_maxdistfromfront_distance * 1000) + "\n" ) - trigger = TriggerStart(comment="Pretense config") trigger.add_action(DoScript(String(lua_string_config))) self.mission.triggerrules.triggers.append(trigger) @@ -1247,16 +1509,30 @@ class PretenseLuaGenerator(LuaGenerator): ) lua_string_zones = "" + lua_string_carriers = self.generate_pretense_carrier_zones() for cp in self.game.theater.controlpoints: - if isinstance(cp, OffMapSpawn): - continue - cp_name_trimmed = "".join([i for i in cp.name.lower() if i.isalpha()]) cp_name = "".join( [i for i in cp.name if i.isalnum() or i.isspace() or i == "-"] ) cp_side = 2 if cp.captured else 1 + + if isinstance(cp, OffMapSpawn): + continue + elif ( + cp.is_fleet + and cp.captured + and self.game.settings.pretense_controllable_carrier + ): + # Friendly carrier, generate carrier parameters + cp_carrier_group_type = cp.get_carrier_group_type() + cp_carrier_group_name = cp.get_carrier_group_name() + lua_string_carriers += self.generate_pretense_carriers( + cp_name, cp_side, cp_carrier_group_type, cp_carrier_group_name + ) + continue + for side in range(1, 3): if cp_name_trimmed not in self.game.pretense_air[cp_side]: self.game.pretense_air[side][cp_name_trimmed] = {} @@ -1306,7 +1582,10 @@ class PretenseLuaGenerator(LuaGenerator): lua_string_zones += ( f"zones.{cp_name_trimmed}.keepActive = " + is_keep_active + "\n" ) - lua_string_zones += self.generate_pretense_zone_land(cp.name) + if cp.is_fleet: + lua_string_zones += self.generate_pretense_zone_sea(cp_name) + else: + lua_string_zones += self.generate_pretense_zone_land(cp_name) lua_string_connman = " cm = ConnectionManager:new()\n" @@ -1326,7 +1605,10 @@ class PretenseLuaGenerator(LuaGenerator): ) if len(cp.connected_points) == 0 and len(cp.shipping_lanes) == 0: # Also connect carrier and LHA control points to adjacent friendly points - if cp.is_fleet: + if cp.is_fleet and ( + not self.game.settings.pretense_controllable_carrier + or not cp.captured + ): num_of_carrier_connections = 0 for ( other_cp @@ -1347,7 +1629,19 @@ class PretenseLuaGenerator(LuaGenerator): for extra_connection in range( self.game.settings.pretense_extra_zone_connections ): - if len(closest_cps) > extra_connection: + if ( + cp.is_fleet + and cp.captured + and self.game.settings.pretense_controllable_carrier + ): + break + elif ( + closest_cps[extra_connection].is_fleet + and closest_cps[extra_connection].captured + and self.game.settings.pretense_controllable_carrier + ): + break + elif len(closest_cps) > extra_connection: lua_string_connman += self.generate_pretense_zone_connection( connected_points, cp.name, @@ -1387,9 +1681,9 @@ class PretenseLuaGenerator(LuaGenerator): lua_string_jtac = "" for jtac in self.mission_data.jtacs: - lua_string_jtac = f"Group.getByName('{jtac.group_name}'): destroy()" + lua_string_jtac = f"Group.getByName('{jtac.group_name}'): destroy()\n" lua_string_jtac += ( - "CommandFunctions.jtac = JTAC:new({name = '" + jtac.group_name + "'})" + "CommandFunctions.jtac = JTAC:new({name = '" + jtac.group_name + "'})\n" ) init_body_2_file = open("./resources/plugins/pretense/init_body_2.lua", "r") @@ -1411,6 +1705,7 @@ class PretenseLuaGenerator(LuaGenerator): + lua_string_connman + init_body_2 + lua_string_jtac + + lua_string_carriers + init_body_3 + lua_string_supply + init_footer diff --git a/game/pretense/pretensemissiongenerator.py b/game/pretense/pretensemissiongenerator.py index 7b18b35b..57b53b26 100644 --- a/game/pretense/pretensemissiongenerator.py +++ b/game/pretense/pretensemissiongenerator.py @@ -38,6 +38,7 @@ from ..ato.airtaaskingorder import AirTaskingOrder from ..callsigns import callsign_for_support_unit from ..dcs.aircrafttype import AircraftType from ..missiongenerator import MissionGenerator +from ..theater import Airfield if TYPE_CHECKING: from game import Game @@ -104,6 +105,26 @@ class PretenseMissionGenerator(MissionGenerator): self.generate_ground_conflicts() self.generate_air_units(tgo_generator) + for cp in self.game.theater.controlpoints: + if ( + self.game.settings.ground_start_airbase_statics_farps_remove + and isinstance(cp, Airfield) + ): + while len(tgo_generator.ground_spawns[cp]) > 0: + ground_spawn = tgo_generator.ground_spawns[cp].pop() + # Remove invisible FARPs from airfields because they are unnecessary + neutral_country = self.mission.country( + cp.coalition.game.neutral_country.name + ) + neutral_country.remove_static_group(ground_spawn[0]) + while len(tgo_generator.ground_spawns_roadbase[cp]) > 0: + ground_spawn = tgo_generator.ground_spawns_roadbase[cp].pop() + # Remove invisible FARPs from airfields because they are unnecessary + neutral_country = self.mission.country( + cp.coalition.game.neutral_country.name + ) + neutral_country.remove_static_group(ground_spawn[0]) + self.mission.triggerrules.triggers.clear() PretenseTriggerGenerator(self.mission, self.game).generate() ForcedOptionsGenerator(self.mission, self.game).generate() diff --git a/game/pretense/pretensetgogenerator.py b/game/pretense/pretensetgogenerator.py index 58a1cbed..82217973 100644 --- a/game/pretense/pretensetgogenerator.py +++ b/game/pretense/pretensetgogenerator.py @@ -8,12 +8,14 @@ create the pydcs groups and statics for those areas and add them to the mission. from __future__ import annotations import random +import logging from collections import defaultdict -from typing import Dict, Optional, TYPE_CHECKING, Tuple, Type +from typing import Dict, Optional, TYPE_CHECKING, Tuple, Type, Iterator from dcs import Mission, Point from dcs.countries import * from dcs.country import Country +from dcs.ships import Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal, LHA_Tarawa from dcs.unitgroup import StaticGroup, VehicleGroup from dcs.unittype import VehicleType @@ -23,7 +25,7 @@ from game.dcs.groundunittype import GroundUnitType from game.missiongenerator.groundforcepainter import ( GroundForcePainter, ) -from game.missiongenerator.missiondata import MissionData +from game.missiongenerator.missiondata import MissionData, CarrierInfo from game.missiongenerator.tgogenerator import ( TgoGenerator, HelipadGenerator, @@ -33,10 +35,11 @@ from game.missiongenerator.tgogenerator import ( CarrierGenerator, LhaGenerator, MissileSiteGenerator, + GenericCarrierGenerator, ) from game.point_with_heading import PointWithHeading from game.radio.radios import RadioRegistry -from game.radio.tacan import TacanRegistry +from game.radio.tacan import TacanRegistry, TacanBand, TacanUsage from game.runways import RunwayData from game.theater import ( ControlPoint, @@ -51,9 +54,11 @@ from game.theater.theatergroundobject import ( MissileSiteGroundObject, BuildingGroundObject, VehicleGroupGroundObject, + GenericCarrierGroundObject, ) from game.theater.theatergroup import TheaterGroup from game.unitmap import UnitMap +from game.utils import Heading from pydcs_extensions import ( Char_M551_Sheridan, BV410_RBS70, @@ -147,6 +152,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator): faction_units = ( set(coalition.faction.frontline_units) | set(coalition.faction.artillery_units) + | set(coalition.faction.air_defense_units) | set(coalition.faction.logistics_units) ) of_class = list({u for u in faction_units if u.unit_class is unit_class}) @@ -608,6 +614,172 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator): return vehicle_group +class PretenseGenericCarrierGenerator(GenericCarrierGenerator): + """Base type for carrier group generation. + + Used by both CV(N) groups and LHA groups. + """ + + def __init__( + self, + ground_object: GenericCarrierGroundObject, + control_point: NavalControlPoint, + country: Country, + game: Game, + mission: Mission, + radio_registry: RadioRegistry, + tacan_registry: TacanRegistry, + icls_alloc: Iterator[int], + runways: Dict[str, RunwayData], + unit_map: UnitMap, + mission_data: MissionData, + ) -> None: + super().__init__( + ground_object, + control_point, + country, + game, + mission, + radio_registry, + tacan_registry, + icls_alloc, + runways, + unit_map, + mission_data, + ) + self.ground_object = ground_object + self.control_point = control_point + self.radio_registry = radio_registry + self.tacan_registry = tacan_registry + self.icls_alloc = icls_alloc + self.runways = runways + self.mission_data = mission_data + + def generate(self) -> None: + if self.control_point.frequency is not None: + atc = self.control_point.frequency + if atc not in self.radio_registry.allocated_channels: + self.radio_registry.reserve(atc) + else: + atc = self.radio_registry.alloc_uhf() + + for g_id, group in enumerate(self.ground_object.groups): + if not group.units: + logging.warning(f"Found empty carrier group in {self.control_point}") + continue + + ship_units = [] + for unit in group.units: + if unit.alive: + # All alive Ships + print( + f"Added {unit.unit_name} to ship_units of group {group.group_name}" + ) + ship_units.append(unit) + + if not ship_units: + # Empty array (no alive units), skip this group + continue + + ship_group = self.create_ship_group(group.group_name, ship_units, atc) + + if self.game.settings.pretense_carrier_steams_into_wind: + # Always steam into the wind, even if the carrier is being moved. + # There are multiple unsimulated hours between turns, so we can + # count those as the time the carrier uses to move and the mission + # time as the recovery window. + brc = self.steam_into_wind(ship_group) + else: + brc = Heading(0) + + # Set Carrier Specific Options + if g_id == 0 and self.control_point.runway_is_operational(): + # Get Correct unit type for the carrier. + # This will upgrade to super carrier if option is enabled + carrier_type = self.carrier_type + if carrier_type is None: + raise RuntimeError( + f"Error generating carrier group for {self.control_point.name}" + ) + ship_group.units[0].type = carrier_type.id + if self.control_point.tacan is None: + tacan = self.tacan_registry.alloc_for_band( + TacanBand.X, TacanUsage.TransmitReceive + ) + else: + tacan = self.control_point.tacan + if self.control_point.tcn_name is None: + tacan_callsign = self.tacan_callsign() + else: + tacan_callsign = self.control_point.tcn_name + link4 = None + link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal] + if carrier_type in link4carriers: + if self.control_point.link4 is None: + link4 = self.radio_registry.alloc_uhf() + else: + link4 = self.control_point.link4 + icls = None + icls_name = self.control_point.icls_name + if carrier_type in link4carriers or carrier_type == LHA_Tarawa: + if self.control_point.icls_channel is None: + icls = next(self.icls_alloc) + else: + icls = self.control_point.icls_channel + self.activate_beacons( + ship_group, tacan, tacan_callsign, icls, icls_name, link4 + ) + self.add_runway_data( + brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls + ) + self.mission_data.carriers.append( + CarrierInfo( + group_name=ship_group.name, + unit_name=ship_group.units[0].name, + callsign=tacan_callsign, + freq=atc, + tacan=tacan, + icls_channel=icls, + link4_freq=link4, + blue=self.control_point.captured, + ) + ) + + +class PretenseCarrierGenerator(PretenseGenericCarrierGenerator): + def tacan_callsign(self) -> str: + # TODO: Assign these properly. + return random.choice( + [ + "STE", + "CVN", + "CVH", + "CCV", + "ACC", + "ARC", + "GER", + "ABR", + "LIN", + "TRU", + ] + ) + + +class PretenseLhaGenerator(PretenseGenericCarrierGenerator): + def tacan_callsign(self) -> str: + # TODO: Assign these properly. + return random.choice( + [ + "LHD", + "LHA", + "LHB", + "LHC", + "LHD", + "LDS", + ] + ) + + class PretenseTgoGenerator(TgoGenerator): """Creates DCS groups and statics for the theater during mission generation. @@ -693,7 +865,7 @@ class PretenseTgoGenerator(TgoGenerator): if isinstance(ground_object, CarrierGroundObject) and isinstance( cp, NavalControlPoint ): - generator = CarrierGenerator( + generator = PretenseCarrierGenerator( ground_object, cp, country, @@ -709,7 +881,7 @@ class PretenseTgoGenerator(TgoGenerator): elif isinstance(ground_object, LhaGroundObject) and isinstance( cp, NavalControlPoint ): - generator = LhaGenerator( + generator = PretenseLhaGenerator( ground_object, cp, country, diff --git a/game/pretense/pretensetriggergenerator.py b/game/pretense/pretensetriggergenerator.py index 0f33f68a..8500bf47 100644 --- a/game/pretense/pretensetriggergenerator.py +++ b/game/pretense/pretensetriggergenerator.py @@ -1,6 +1,8 @@ from __future__ import annotations import logging +import math +import random from typing import TYPE_CHECKING, List from dcs import Point @@ -29,7 +31,10 @@ from dcs.terrain.syria.airports import Damascus, Khalkhalah from dcs.translation import String from dcs.triggers import Event, TriggerCondition, TriggerOnce from dcs.unit import Skill +from numpy import cross, einsum, arctan2 +from shapely import MultiPolygon, Point as ShapelyPoint +from game.naming import ALPHA_MILITARY from game.theater import Airfield from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn @@ -56,10 +61,14 @@ TRIGGER_RADIUS_PRETENSE_TGO = 500 TRIGGER_RADIUS_PRETENSE_SUPPLY = 500 TRIGGER_RADIUS_PRETENSE_HELI = 1000 TRIGGER_RADIUS_PRETENSE_HELI_BUFFER = 500 -TRIGGER_RADIUS_PRETENSE_CARRIER = 50000 +TRIGGER_RADIUS_PRETENSE_CARRIER = 20000 +TRIGGER_RADIUS_PRETENSE_CARRIER_SMALL = 3000 +TRIGGER_RADIUS_PRETENSE_CARRIER_CORNER = 25000 TRIGGER_RUNWAY_LENGTH_PRETENSE = 2500 TRIGGER_RUNWAY_WIDTH_PRETENSE = 400 +SIMPLIFY_RUNS_PRETENSE_CARRIER = 10000 + class Silence(Option): Key = 7 @@ -221,12 +230,105 @@ class PretenseTriggerGenerator: self.mission.triggerrules.triggers.append(recapture_trigger) def _generate_pretense_zone_triggers(self) -> None: - """Creates a pair of triggers for each control point of `cls.capture_zone_types`. - One for the initial capture of a control point, and one if it is recaptured. - Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua` + """Creates triggger zones for the Pretense campaign. These include: + - Carrier zones for friendly forces, generated from the navmesh / sea zone intersection + - Carrier zones for opposing forces + - Airfield and FARP zones + - Airfield and FARP spawn points / helicopter spawn points / ground object positions """ + + # First generate carrier zones for friendly forces + use_blue_navmesh = ( + self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh" + ) + sea_zones_landmap = self.game.coalition_for( + player=False + ).nav_mesh.theater.landmap + if ( + self.game.settings.pretense_controllable_carrier + and sea_zones_landmap is not None + ): + navmesh_number = 0 + for navmesh_poly in self.game.coalition_for( + player=use_blue_navmesh + ).nav_mesh.polys: + navmesh_number += 1 + if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly): + # Get the intersection between the navmesh zone and the sea zone + navmesh_sea_intersection = sea_zones_landmap.sea_zones.intersection( + navmesh_poly.poly + ) + navmesh_zone_verticies = navmesh_sea_intersection + + # Simplify it to get a quadrangle + for simplify_run in range(SIMPLIFY_RUNS_PRETENSE_CARRIER): + navmesh_zone_verticies = navmesh_sea_intersection.simplify( + float(simplify_run * 10), preserve_topology=False + ) + if isinstance(navmesh_zone_verticies, MultiPolygon): + break + if len(navmesh_zone_verticies.exterior.coords) <= 4: + break + if isinstance(navmesh_zone_verticies, MultiPolygon): + continue + trigger_zone_verticies = [] + terrain = self.game.theater.terrain + alpha = random.choice(ALPHA_MILITARY) + + # Generate the quadrangle zone and four points inside it for carrier navigation + if len(navmesh_zone_verticies.exterior.coords) == 4: + zone_color = {1: 1.0, 2: 1.0, 3: 1.0, 4: 0.15} + corner_point_num = 0 + for point_coord in navmesh_zone_verticies.exterior.coords: + corner_point = Point( + x=point_coord[0], y=point_coord[1], terrain=terrain + ) + nav_point = corner_point.point_from_heading( + corner_point.heading_between_point( + navmesh_sea_intersection.centroid + ), + TRIGGER_RADIUS_PRETENSE_CARRIER_CORNER, + ) + corner_point_num += 1 + + zone_name = f"{alpha}-{navmesh_number}-{corner_point_num}" + if sea_zones_landmap.sea_zones.contains( + ShapelyPoint(nav_point.x, nav_point.y) + ): + self.mission.triggers.add_triggerzone( + nav_point, + radius=TRIGGER_RADIUS_PRETENSE_CARRIER_SMALL, + hidden=False, + name=zone_name, + color=zone_color, + ) + + trigger_zone_verticies.append(corner_point) + + zone_name = f"{alpha}-{navmesh_number}" + trigger_zone = self.mission.triggers.add_triggerzone_quad( + navmesh_sea_intersection.centroid, + trigger_zone_verticies, + hidden=False, + name=zone_name, + color=zone_color, + ) + try: + if len(self.game.pretense_carrier_zones) == 0: + self.game.pretense_carrier_zones = [] + except AttributeError: + self.game.pretense_carrier_zones = [] + self.game.pretense_carrier_zones.append(zone_name) + for cp in self.game.theater.controlpoints: - if cp.is_fleet: + if ( + cp.is_fleet + and self.game.settings.pretense_controllable_carrier + and cp.captured + ): + # Friendly carrier zones are generated above + continue + elif cp.is_fleet: trigger_radius = float(TRIGGER_RADIUS_PRETENSE_CARRIER) elif isinstance(cp, Fob) and cp.has_helipads: trigger_radius = TRIGGER_RADIUS_PRETENSE_HELI @@ -247,6 +349,8 @@ class PretenseTriggerGenerator: or isinstance(cp.dcs_airport, Khalkhalah) or isinstance(cp.dcs_airport, Krasnodar_Pashkovsky) ): + # Increase the size of Pretense zones at Damascus, Khalkhalah and Krasnodar-Pashkovsky + # (which are quite spread out) so the zone would encompass the entire airfield. trigger_radius = int(TRIGGER_RADIUS_CAPTURE * 1.8) else: trigger_radius = TRIGGER_RADIUS_CAPTURE diff --git a/game/settings/settings.py b/game/settings/settings.py index 6cfc0b3b..e2df6a2c 100644 --- a/game/settings/settings.py +++ b/game/settings/settings.py @@ -158,6 +158,7 @@ class Settings: MISSION_RESTRICTIONS_SECTION, default=True, ) + easy_communication: Optional[bool] = choices_option( "Easy Communication", page=DIFFICULTY_PAGE, @@ -176,6 +177,20 @@ class Settings: # Campaign management # General + squadron_random_chance: int = bounded_int_option( + "Percentage of randomly selected aircraft types (only for generated squadrons)", + page=CAMPAIGN_MANAGEMENT_PAGE, + section=GENERAL_SECTION, + default=50, + min=0, + max=100, + detail=( + "

Aircraft type selection is governed by the campaign and the squadron definitions available to " + "Retribution. Squadrons are generated by Retribution if the faction does not have access to the campaign " + "designer's squadron/aircraft definitions. Use the above to increase/decrease aircraft variety by making " + "some selections random instead of picking aircraft types from a priority list.

" + ), + ) restrict_weapons_by_date: bool = boolean_option( "Restrict weapons by date (WIP)", page=CAMPAIGN_MANAGEMENT_PAGE, @@ -831,6 +846,17 @@ class Settings: "Needed to cold-start some aircraft types. Might have a performance impact." ), ) + ground_start_airbase_statics_farps_remove: bool = boolean_option( + "Remove ground spawn statics, including invisible FARPs, at airbases", + MISSION_GENERATOR_PAGE, + GAMEPLAY_SECTION, + default=True, + detail=( + "Ammo and fuel statics and invisible FARPs should be unnecessary when creating " + "additional spawns for players at airbases. This setting will disable them and " + "potentially grant a marginal performance benefit." + ), + ) ai_unlimited_fuel: bool = boolean_option( "AI flights have unlimited fuel", MISSION_GENERATOR_PAGE, @@ -994,6 +1020,43 @@ class Settings: "parts of the economy. Use this to adjust performance." ), ) + pretense_controllable_carrier: bool = boolean_option( + "Controllable carrier", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=True, + detail=( + "This can be used to enable or disable the native carrier support in Pretense. The Pretense carrier " + "can be controlled through the communication menu (if the Pretense character has enough rank/CMD points) " + "and the player can call in AI aerial and cruise missile missions using it." + "The controllable carriers in Pretense do not build and deploy AI missions autonomously, so if you prefer " + "to have both sides deploy carrier aviation autonomously, you might want to disable this option. " + "When this option is disabled, moving the carrier can only be done with the Retribution interface." + ), + ) + pretense_carrier_steams_into_wind: bool = boolean_option( + "Carriers steam into wind", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + default=True, + detail=( + "This setting controls whether carriers and their escorts will steam into wind. Disable to " + "to ensure that the carriers stay within the carrier zone in Pretense, but note that " + "doing so might limit carrier operations, takeoff weights and landings." + ), + ) + pretense_carrier_zones_navmesh: str = choices_option( + "Navmesh to use for Pretense carrier zones", + page=PRETENSE_PAGE, + section=GENERAL_SECTION, + choices=["Blue navmesh", "Red navmesh"], + default="Blue navmesh", + detail=( + "Use the Retribution map interface options to compare the blue navmesh and the red navmesh." + "You can select which navmesh to use when generating the zones in which the controllable carrier(s) " + "move and operate." + ), + ) pretense_extra_zone_connections: int = bounded_int_option( "Extra friendly zone connections", page=PRETENSE_PAGE, @@ -1018,7 +1081,7 @@ class Settings: "Number of AI SEAD flights per control point / zone", page=PRETENSE_PAGE, section=GENERAL_SECTION, - default=2, + default=1, min=1, max=10, ) @@ -1026,7 +1089,7 @@ class Settings: "Number of AI CAS flights per control point / zone", page=PRETENSE_PAGE, section=GENERAL_SECTION, - default=2, + default=1, min=1, max=10, ) @@ -1034,7 +1097,7 @@ class Settings: "Number of AI BAI flights per control point / zone", page=PRETENSE_PAGE, section=GENERAL_SECTION, - default=2, + default=1, min=1, max=10, ) @@ -1042,7 +1105,7 @@ class Settings: "Number of AI Strike flights per control point / zone", page=PRETENSE_PAGE, section=GENERAL_SECTION, - default=2, + default=1, min=1, max=10, ) @@ -1050,7 +1113,7 @@ class Settings: "Number of AI BARCAP flights per control point / zone", page=PRETENSE_PAGE, section=GENERAL_SECTION, - default=2, + default=1, min=1, max=10, ) @@ -1066,7 +1129,7 @@ class Settings: "Number of player flights per aircraft type at each base", page=PRETENSE_PAGE, section=GENERAL_SECTION, - default=2, + default=1, min=1, max=10, ) diff --git a/resources/plugins/pretense/init_body_1.lua b/resources/plugins/pretense/init_body_1.lua index ad7ae685..2cac3514 100644 --- a/resources/plugins/pretense/init_body_1.lua +++ b/resources/plugins/pretense/init_body_1.lua @@ -21,6 +21,12 @@ presets = { cost = 1500, type = 'upgrade', template = "outpost" + }), + artyBunker = Preset:new({ + display = 'Artillery Bunker', + cost = 2000, + type = 'upgrade', + template = "ammo-depot" }) }, attack = { @@ -36,30 +42,12 @@ presets = { type = 'upgrade', template = "ammo-depot" }), - shipTankerSeawisegiant = Preset:new({ - display = 'Tanker Seawise Giant', - cost = 1500, - type = 'upgrade', - template = "ship-tanker-seawisegiant" - }), - shipLandingShipSamuelChase = Preset:new({ - display = 'LST USS Samuel Chase', - cost = 1500, - type = 'upgrade', - template = "ship-landingship-samuelchase" - }), - shipLandingShipRopucha = Preset:new({ - display = 'LS Ropucha', - cost = 1500, - type = 'upgrade', - template = "ship-landingship-ropucha" - }), - shipTankerElnya = Preset:new({ - display = 'Tanker Elnya', - cost = 1500, - type = 'upgrade', - template = "ship-tanker-elnya" - }) + chemTank = Preset:new({ + display='Chemical Tank', + cost = 2000, + type ='upgrade', + template = "chem-tank" + }), }, supply = { fuelCache = Preset:new({ @@ -178,30 +166,6 @@ presets = { income = 50, template = "tv-tower" }), - shipSupplyTilde = Preset:new({ - display = 'Ship_Tilde_Supply', - cost = 1500, - type = 'upgrade', - template = "ship-supply-tilde" - }), - shipLandingShipLstMk2 = Preset:new({ - display = 'LST Mk.II', - cost = 1500, - type = 'upgrade', - template = "ship-landingship-lstmk2" - }), - shipBulkerYakushev = Preset:new({ - display = 'Bulker Yakushev', - cost = 1500, - type = 'upgrade', - template = "ship-bulker-yakushev" - }), - shipCargoIvanov = Preset:new({ - display = 'Cargo Ivanov', - cost = 1500, - type = 'upgrade', - template = "ship-cargo-ivanov" - }) }, airdef = { bunker = Preset:new({ @@ -226,6 +190,12 @@ presets = { type='defense', template='infantry-red', }), + artillery = Preset:new({ + display = 'Artillery', + cost=2500, + type='defense', + template='artillery-red', + }), shorad = Preset:new({ display = 'SHORAD', cost=2500, @@ -298,6 +268,12 @@ presets = { type='defense', template='rapier-red', }), + roland = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='roland-red', + }), irondome = Preset:new({ display = 'SAM', cost=20000, @@ -324,6 +300,12 @@ presets = { type='defense', template='infantry-blue', }), + artillery = Preset:new({ + display = 'Artillery', + cost=2500, + type='defense', + template='artillery-blue', + }), shorad = Preset:new({ display = 'SHORAD', cost=2500, @@ -396,6 +378,12 @@ presets = { type='defense', template='rapier-blue', }), + roland = Preset:new({ + display = 'SAM', + cost=3000, + type='defense', + template='roland-blue', + }), irondome = Preset:new({ display = 'SAM', cost=20000,