Implemented support for player controllable carriers in Pretense campaigns. This functionality can be enabled or disabled in settings, because the controllable carriers in Pretense do not build and deploy AI missions autonomously, so the old functionality is retained.

Added new options in settings:
- Carriers steam into wind
- Navmesh to use for Pretense carrier zones
- Remove ground spawn statics, including invisible FARPs, at airbases.
- Percentage of randomly selected aircraft types (only for generated squadrons)
intended to allow the user to increase aircraft variety.

Will now store the ICLS channel and Link4 frequency in missiondata.py CarrierInfo.

Implemented artillery groups as Pretense garrisons. Artillery groups are spawned by the Artillery Bunker. Will now also ensure that the logistics units spawned as part of Pretense garrisons are actually capable of ammo resupply.

Fixed the Pretense generator generating a bit too many missions per squadron. Ground spawns: Also hot start aircraft which require ground crew support (ground air or chock removal) which might not be available at roadbases. Also, pretensetgogenerator.py will now correctly handle air defence units in ground_unit_of_class(). Added Roland groups in the Pretense generator.
This commit is contained in:
MetalStormGhost 2024-04-06 15:46:11 +03:00
parent 55b173ec10
commit 64b1410de8
13 changed files with 851 additions and 133 deletions

View File

@ -73,7 +73,7 @@ class DefaultSquadronAssigner:
# If we can't find any squadron matching the requirement, we should # If we can't find any squadron matching the requirement, we should
# create one. # create one.
return self.air_wing.squadron_def_generator.generate_for_task( 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( def find_preferred_squadron(

View File

@ -21,7 +21,7 @@ class SquadronDefGenerator:
self.used_nicknames: set[str] = set() self.used_nicknames: set[str] = set()
def generate_for_task( def generate_for_task(
self, task: FlightType, control_point: ControlPoint self, task: FlightType, control_point: ControlPoint, squadron_random_chance: int
) -> Optional[SquadronDef]: ) -> Optional[SquadronDef]:
aircraft_choice: Optional[AircraftType] = None aircraft_choice: Optional[AircraftType] = None
for aircraft in AircraftType.priority_list_for_task(task): for aircraft in AircraftType.priority_list_for_task(task):
@ -30,9 +30,9 @@ class SquadronDefGenerator:
if not control_point.can_operate(aircraft): if not control_point.can_operate(aircraft):
continue continue
aircraft_choice = aircraft 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. # priority list to maintain some unit variety.
if random.choice([True, False]): if squadron_random_chance >= random.randint(1, 100):
break break
if aircraft_choice is None: if aircraft_choice is None:

View File

@ -157,6 +157,7 @@ class Game:
2: {}, 2: {},
} }
self.pretense_air_groups: dict[str, Flight] = {} self.pretense_air_groups: dict[str, Flight] = {}
self.pretense_carrier_zones: List[str] = []
self.on_load(game_still_initializing=True) self.on_load(game_still_initializing=True)

View File

@ -15,6 +15,7 @@ from dcs.planes import (
C_101CC, C_101CC,
Su_33, Su_33,
MiG_15bis, MiG_15bis,
M_2000C,
) )
from dcs.point import PointAction from dcs.point import PointAction
from dcs.ships import KUZNECOW from dcs.ships import KUZNECOW
@ -35,7 +36,7 @@ from game.missiongenerator.missiondata import MissionData
from game.naming import namegen from game.naming import namegen
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn
from game.utils import feet, meters 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_HELI_ALT = meters(500)
WARM_START_ALTITUDE = meters(3000) WARM_START_ALTITUDE = meters(3000)
@ -400,6 +401,18 @@ class FlightGroupSpawner:
group.points[0].type = "TakeOffGround" group.points[0].type = "TakeOffGround"
group.units[0].heading = ground_spawn[0].units[0].heading 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 # Hot start aircraft which require ground power to start, when ground power
# trucks have been disabled for performance reasons # trucks have been disabled for performance reasons
ground_power_available = ( ground_power_available = (
@ -410,10 +423,31 @@ class FlightGroupSpawner:
and self.flight.coalition.game.settings.ground_start_ground_power_trucks_roadbase and self.flight.coalition.game.settings.ground_start_ground_power_trucks_roadbase
) )
if self.start_type is not StartType.COLD or ( # Also hot start aircraft which require ground crew support (ground air or chock removal)
not ground_power_available # which might not be available at roadbases
and self.flight.unit_type.dcs_unit_type if (
in [A_4E_C, F_5E_3, F_86F_Sabre, MiG_15bis, F_14A_135_GR, F_14B, C_101CC] 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].action = PointAction.FromGroundAreaHot
group.points[0].type = "TakeOffGroundHot" group.points[0].type = "TakeOffGroundHot"
@ -435,6 +469,17 @@ class FlightGroupSpawner:
ground_spawn[0].x, ground_spawn[0].y, terrain=terrain ground_spawn[0].x, ground_spawn[0].y, terrain=terrain
) )
group.units[1 + i].heading = ground_spawn[0].units[0].heading 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: except IndexError as ex:
raise NoParkingSlotError( raise NoParkingSlotError(
f"Not enough STOL slots available at {cp}" f"Not enough STOL slots available at {cp}"

View File

@ -52,6 +52,8 @@ class CarrierInfo(UnitInfo):
"""Carrier information.""" """Carrier information."""
tacan: TacanChannel tacan: TacanChannel
icls_channel: int | None
link4_freq: RadioFrequency | None
@dataclass @dataclass

View File

@ -60,6 +60,7 @@ from game.theater import (
TheaterGroundObject, TheaterGroundObject,
TheaterUnit, TheaterUnit,
NavalControlPoint, NavalControlPoint,
Airfield,
) )
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
CarrierGroundObject, CarrierGroundObject,
@ -626,6 +627,8 @@ class GenericCarrierGenerator(GroundObjectGenerator):
callsign=tacan_callsign, callsign=tacan_callsign,
freq=atc, freq=atc,
tacan=tacan, tacan=tacan,
icls_channel=icls,
link4_freq=link4,
blue=self.control_point.captured, blue=self.control_point.captured,
) )
) )
@ -940,7 +943,11 @@ class GroundSpawnRoadbaseGenerator:
country.id 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 cull_farp_statics = True
if self.cp.coalition.player: if self.cp.coalition.player:
for package in self.cp.coalition.ato.packages: for package in self.cp.coalition.ato.packages:
@ -1072,7 +1079,11 @@ class GroundSpawnGenerator:
country.id 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 cull_farp_statics = True
if self.cp.coalition.player: if self.cp.coalition.player:
for package in self.cp.coalition.ato.packages: for package in self.cp.coalition.ato.packages:

View File

@ -191,13 +191,13 @@ class PretenseAircraftGenerator:
""" """
squadron_def = coalition.air_wing.squadron_def_generator.generate_for_task( 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): for retries in range(num_retries):
if squadron_def is None or fixed_wing == squadron_def.aircraft.helicopter: if squadron_def is None or fixed_wing == squadron_def.aircraft.helicopter:
squadron_def = ( squadron_def = (
coalition.air_wing.squadron_def_generator.generate_for_task( 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 # First check what are the capabilities of the squadrons on this CP
for squadron in cp.squadrons: for squadron in cp.squadrons:
for task in sead_tasks: 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 sead_capable_cp = True
for task in strike_tasks: for task in strike_tasks:
if task in squadron.auto_assignable_mission_types: if task in squadron.auto_assignable_mission_types:
@ -360,6 +363,8 @@ class PretenseAircraftGenerator:
continue continue
if cp.coalition != squadron.coalition: if cp.coalition != squadron.coalition:
continue continue
if num_of_sead >= self.game.settings.pretense_sead_flights_per_cp:
break
mission_types = squadron.auto_assignable_mission_types mission_types = squadron.auto_assignable_mission_types
if ( if (
@ -400,6 +405,11 @@ class PretenseAircraftGenerator:
continue continue
if cp.coalition != squadron.coalition: if cp.coalition != squadron.coalition:
continue continue
if (
num_of_strike
>= self.game.settings.pretense_strike_flights_per_cp
):
break
mission_types = squadron.auto_assignable_mission_types mission_types = squadron.auto_assignable_mission_types
for task in strike_tasks: for task in strike_tasks:
@ -422,6 +432,8 @@ class PretenseAircraftGenerator:
continue continue
if cp.coalition != squadron.coalition: if cp.coalition != squadron.coalition:
continue continue
if num_of_cap >= self.game.settings.pretense_barcap_flights_per_cp:
break
mission_types = squadron.auto_assignable_mission_types mission_types = squadron.auto_assignable_mission_types
for task in patrol_tasks: for task in patrol_tasks:
@ -444,6 +456,8 @@ class PretenseAircraftGenerator:
continue continue
if cp.coalition != squadron.coalition: if cp.coalition != squadron.coalition:
continue continue
if num_of_cas >= self.game.settings.pretense_cas_flights_per_cp:
break
mission_types = squadron.auto_assignable_mission_types mission_types = squadron.auto_assignable_mission_types
if ( if (
@ -467,6 +481,8 @@ class PretenseAircraftGenerator:
continue continue
if cp.coalition != squadron.coalition: if cp.coalition != squadron.coalition:
continue continue
if num_of_bai >= self.game.settings.pretense_bai_flights_per_cp:
break
mission_types = squadron.auto_assignable_mission_types mission_types = squadron.auto_assignable_mission_types
if FlightType.BAI in mission_types: if FlightType.BAI in mission_types:

View File

@ -7,13 +7,15 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional, List, Type
from dcs import Mission from dcs import Mission
from dcs.action import DoScript, DoScriptFile 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.translation import String
from dcs.triggers import TriggerStart 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.ato import FlightType
from game.coalition import Coalition from game.coalition import Coalition
@ -283,6 +285,7 @@ class PretenseLuaGenerator(LuaGenerator):
"nasamsb", "nasamsb",
"nasamsc", "nasamsc",
"rapier", "rapier",
"roland",
"irondome", "irondome",
"davidsling", "davidsling",
]: ]:
@ -381,6 +384,8 @@ class PretenseLuaGenerator(LuaGenerator):
== AirDefence.Rapier_fsa_launcher == AirDefence.Rapier_fsa_launcher
): ):
sam_presets["rapier"].enabled = True 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: if ground_unit.unit_type.dcs_unit_type == IRON_DOME_LN:
sam_presets["irondome"].enabled = True sam_presets["irondome"].enabled = True
if ground_unit.unit_type.dcs_unit_type == DAVID_SLING_LN: 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_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" cp_side_str = "blue" if cp_side == PRETENSE_BLUE_SIDE else "red"
if cp_side == PRETENSE_BLUE_SIDE: supply_ship = "oilPump"
if random.randint(0, 1): tanker_ship = "chemTank"
supply_ship = "shipSupplyTilde" command_ship = "comCenter"
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"
lua_string_zones += ( lua_string_zones += (
" presets.upgrades.supply." + supply_ship + ":extend({\n" " 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 += " }),\n" lua_string_zones += " }),\n"
lua_string_zones += ( lua_string_zones += (
" presets.upgrades.attack." + command_ship + ":extend({\n" " presets.upgrades.airdef." + command_ship + ":extend({\n"
) )
lua_string_zones += ( lua_string_zones += (
f" name = '{cp_name_trimmed}-mission-command-" f" name = '{cp_name_trimmed}-mission-command-"
@ -592,11 +584,9 @@ class PretenseLuaGenerator(LuaGenerator):
lua_string_zones += ( lua_string_zones += (
" presets.defenses." " presets.defenses."
+ cp_side_str + cp_side_str
+ "." + ".shorad:extend({ name='"
+ ship_group
+ ":extend({ name='"
+ cp_name_trimmed + cp_name_trimmed
+ "-sam-" + "-shorad-"
+ cp_side_str + cp_side_str
+ "' }),\n" + "' }),\n"
) )
@ -719,6 +709,8 @@ class PretenseLuaGenerator(LuaGenerator):
return lua_string_zones return lua_string_zones
def generate_pretense_zone_land(self, cp_name: str) -> str: def generate_pretense_zone_land(self, cp_name: str) -> str:
is_artillery_zone = random.choice([True, False])
lua_string_zones = "" lua_string_zones = ""
cp_name_trimmed = "".join([i for i in cp_name.lower() if i.isalpha()]) 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 += " presets.upgrades.basic.tent:extend({\n"
lua_string_zones += f" name='{cp_name_trimmed}-tent-red',\n" lua_string_zones += f" name='{cp_name_trimmed}-tent-red',\n"
lua_string_zones += " products = {\n" lua_string_zones += " products = {\n"
lua_string_zones += ( if not is_artillery_zone:
" presets.special.red.infantry:extend({ name='" lua_string_zones += (
+ cp_name_trimmed " presets.special.red.infantry:extend({ name='"
+ "-defense-red'})\n" + cp_name_trimmed
) + "-defense-red'})\n"
)
lua_string_zones += " }\n" lua_string_zones += " }\n"
lua_string_zones += " }),\n" lua_string_zones += " }),\n"
lua_string_zones += " presets.upgrades.basic.comPost:extend({\n" lua_string_zones += " presets.upgrades.basic.comPost:extend({\n"
@ -742,13 +735,25 @@ class PretenseLuaGenerator(LuaGenerator):
+ cp_name_trimmed + cp_name_trimmed
+ "-defense-red'}),\n" + "-defense-red'}),\n"
) )
lua_string_zones += ( if not is_artillery_zone:
" presets.defenses.red.infantry:extend({ name='" lua_string_zones += (
+ cp_name_trimmed " presets.defenses.red.infantry:extend({ name='"
+ "-garrison-red' })\n" + cp_name_trimmed
) + "-garrison-red' })\n"
)
lua_string_zones += " }\n" lua_string_zones += " }\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( lua_string_zones += self.generate_pretense_land_upgrade_supply(
cp_name, PRETENSE_RED_SIDE cp_name, PRETENSE_RED_SIDE
@ -760,11 +765,12 @@ class PretenseLuaGenerator(LuaGenerator):
lua_string_zones += " presets.upgrades.basic.tent:extend({\n" lua_string_zones += " presets.upgrades.basic.tent:extend({\n"
lua_string_zones += f" name='{cp_name_trimmed}-tent-blue',\n" lua_string_zones += f" name='{cp_name_trimmed}-tent-blue',\n"
lua_string_zones += " products = {\n" lua_string_zones += " products = {\n"
lua_string_zones += ( if not is_artillery_zone:
" presets.special.blue.infantry:extend({ name='" lua_string_zones += (
+ cp_name_trimmed " presets.special.blue.infantry:extend({ name='"
+ "-defense-blue'})\n" + cp_name_trimmed
) + "-defense-blue'})\n"
)
lua_string_zones += " }\n" lua_string_zones += " }\n"
lua_string_zones += " }),\n" lua_string_zones += " }),\n"
lua_string_zones += " presets.upgrades.basic.comPost:extend({\n" lua_string_zones += " presets.upgrades.basic.comPost:extend({\n"
@ -775,13 +781,25 @@ class PretenseLuaGenerator(LuaGenerator):
+ cp_name_trimmed + cp_name_trimmed
+ "-defense-blue'}),\n" + "-defense-blue'}),\n"
) )
lua_string_zones += ( if not is_artillery_zone:
" presets.defenses.blue.infantry:extend({ name='" lua_string_zones += (
+ cp_name_trimmed " presets.defenses.blue.infantry:extend({ name='"
+ "-garrison-blue' })\n" + cp_name_trimmed
) + "-garrison-blue' })\n"
)
lua_string_zones += " }\n" lua_string_zones += " }\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( lua_string_zones += self.generate_pretense_land_upgrade_supply(
cp_name, PRETENSE_BLUE_SIDE cp_name, PRETENSE_BLUE_SIDE
@ -816,14 +834,204 @@ class PretenseLuaGenerator(LuaGenerator):
return lua_string_zones 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( def get_ground_unit(
self, coalition: Coalition, side: int, desired_unit_classes: list[UnitClass] self, coalition: Coalition, side: int, desired_unit_classes: list[UnitClass]
) -> str: ) -> 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: for unit_class in desired_unit_classes:
if coalition.faction.has_access_to_unit_class(unit_class): if coalition.faction.has_access_to_unit_class(unit_class):
dcs_unit_type = PretenseGroundObjectGenerator.ground_unit_of_class( dcs_unit_type = PretenseGroundObjectGenerator.ground_unit_of_class(
coalition=coalition, unit_class=unit_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: if dcs_unit_type is not None:
return dcs_unit_type.dcs_id return dcs_unit_type.dcs_id
@ -849,6 +1057,11 @@ class PretenseLuaGenerator(LuaGenerator):
return "LAV-25" return "LAV-25"
else: else:
return "BTR-80" 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: elif desired_unit_classes[0] == UnitClass.RECON:
if side == PRETENSE_BLUE_SIDE: if side == PRETENSE_BLUE_SIDE:
return "M1043 HMMWV Armament" return "M1043 HMMWV Armament"
@ -865,10 +1078,16 @@ class PretenseLuaGenerator(LuaGenerator):
else: else:
return "KS-19" return "KS-19"
elif desired_unit_classes[0] == UnitClass.MANPAD: elif desired_unit_classes[0] == UnitClass.MANPAD:
if side == PRETENSE_BLUE_SIDE: if coalition.game.date.year >= 1990:
return "Soldier stinger" if side == PRETENSE_BLUE_SIDE:
return "Soldier stinger"
else:
return "SA-18 Igla manpad"
else: 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: elif desired_unit_classes[0] == UnitClass.LOGISTICS:
if side == PRETENSE_BLUE_SIDE: if side == PRETENSE_BLUE_SIDE:
return "M 818" return "M 818"
@ -910,6 +1129,26 @@ class PretenseLuaGenerator(LuaGenerator):
lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n" lua_string_ground_groups += " dataCategory = TemplateDB.type.group\n"
lua_string_ground_groups += "}\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 += ( lua_string_ground_groups += (
'TemplateDB.templates["defense-' + side_str + '"] = {\n' '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 += " dataCategory = TemplateDB.type.group\n"
lua_string_ground_groups += "}\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 return lua_string_ground_groups
@staticmethod @staticmethod
@ -1219,7 +1482,6 @@ class PretenseLuaGenerator(LuaGenerator):
+ str(self.game.settings.pretense_maxdistfromfront_distance * 1000) + str(self.game.settings.pretense_maxdistfromfront_distance * 1000)
+ "\n" + "\n"
) )
trigger = TriggerStart(comment="Pretense config") trigger = TriggerStart(comment="Pretense config")
trigger.add_action(DoScript(String(lua_string_config))) trigger.add_action(DoScript(String(lua_string_config)))
self.mission.triggerrules.triggers.append(trigger) self.mission.triggerrules.triggers.append(trigger)
@ -1247,16 +1509,30 @@ class PretenseLuaGenerator(LuaGenerator):
) )
lua_string_zones = "" lua_string_zones = ""
lua_string_carriers = self.generate_pretense_carrier_zones()
for cp in self.game.theater.controlpoints: 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_trimmed = "".join([i for i in cp.name.lower() if i.isalpha()])
cp_name = "".join( cp_name = "".join(
[i for i in cp.name if i.isalnum() or i.isspace() or i == "-"] [i for i in cp.name if i.isalnum() or i.isspace() or i == "-"]
) )
cp_side = 2 if cp.captured else 1 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): for side in range(1, 3):
if cp_name_trimmed not in self.game.pretense_air[cp_side]: if cp_name_trimmed not in self.game.pretense_air[cp_side]:
self.game.pretense_air[side][cp_name_trimmed] = {} self.game.pretense_air[side][cp_name_trimmed] = {}
@ -1306,7 +1582,10 @@ class PretenseLuaGenerator(LuaGenerator):
lua_string_zones += ( lua_string_zones += (
f"zones.{cp_name_trimmed}.keepActive = " + is_keep_active + "\n" 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" 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: if len(cp.connected_points) == 0 and len(cp.shipping_lanes) == 0:
# Also connect carrier and LHA control points to adjacent friendly points # 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 num_of_carrier_connections = 0
for ( for (
other_cp other_cp
@ -1347,7 +1629,19 @@ class PretenseLuaGenerator(LuaGenerator):
for extra_connection in range( for extra_connection in range(
self.game.settings.pretense_extra_zone_connections 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( lua_string_connman += self.generate_pretense_zone_connection(
connected_points, connected_points,
cp.name, cp.name,
@ -1387,9 +1681,9 @@ class PretenseLuaGenerator(LuaGenerator):
lua_string_jtac = "" lua_string_jtac = ""
for jtac in self.mission_data.jtacs: 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 += ( 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") init_body_2_file = open("./resources/plugins/pretense/init_body_2.lua", "r")
@ -1411,6 +1705,7 @@ class PretenseLuaGenerator(LuaGenerator):
+ lua_string_connman + lua_string_connman
+ init_body_2 + init_body_2
+ lua_string_jtac + lua_string_jtac
+ lua_string_carriers
+ init_body_3 + init_body_3
+ lua_string_supply + lua_string_supply
+ init_footer + init_footer

View File

@ -38,6 +38,7 @@ from ..ato.airtaaskingorder import AirTaskingOrder
from ..callsigns import callsign_for_support_unit from ..callsigns import callsign_for_support_unit
from ..dcs.aircrafttype import AircraftType from ..dcs.aircrafttype import AircraftType
from ..missiongenerator import MissionGenerator from ..missiongenerator import MissionGenerator
from ..theater import Airfield
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -104,6 +105,26 @@ class PretenseMissionGenerator(MissionGenerator):
self.generate_ground_conflicts() self.generate_ground_conflicts()
self.generate_air_units(tgo_generator) 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() self.mission.triggerrules.triggers.clear()
PretenseTriggerGenerator(self.mission, self.game).generate() PretenseTriggerGenerator(self.mission, self.game).generate()
ForcedOptionsGenerator(self.mission, self.game).generate() ForcedOptionsGenerator(self.mission, self.game).generate()

View File

@ -8,12 +8,14 @@ create the pydcs groups and statics for those areas and add them to the mission.
from __future__ import annotations from __future__ import annotations
import random import random
import logging
from collections import defaultdict 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 import Mission, Point
from dcs.countries import * from dcs.countries import *
from dcs.country import Country 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.unitgroup import StaticGroup, VehicleGroup
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
@ -23,7 +25,7 @@ from game.dcs.groundunittype import GroundUnitType
from game.missiongenerator.groundforcepainter import ( from game.missiongenerator.groundforcepainter import (
GroundForcePainter, GroundForcePainter,
) )
from game.missiongenerator.missiondata import MissionData from game.missiongenerator.missiondata import MissionData, CarrierInfo
from game.missiongenerator.tgogenerator import ( from game.missiongenerator.tgogenerator import (
TgoGenerator, TgoGenerator,
HelipadGenerator, HelipadGenerator,
@ -33,10 +35,11 @@ from game.missiongenerator.tgogenerator import (
CarrierGenerator, CarrierGenerator,
LhaGenerator, LhaGenerator,
MissileSiteGenerator, MissileSiteGenerator,
GenericCarrierGenerator,
) )
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.radio.radios import RadioRegistry 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.runways import RunwayData
from game.theater import ( from game.theater import (
ControlPoint, ControlPoint,
@ -51,9 +54,11 @@ from game.theater.theatergroundobject import (
MissileSiteGroundObject, MissileSiteGroundObject,
BuildingGroundObject, BuildingGroundObject,
VehicleGroupGroundObject, VehicleGroupGroundObject,
GenericCarrierGroundObject,
) )
from game.theater.theatergroup import TheaterGroup from game.theater.theatergroup import TheaterGroup
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import Heading
from pydcs_extensions import ( from pydcs_extensions import (
Char_M551_Sheridan, Char_M551_Sheridan,
BV410_RBS70, BV410_RBS70,
@ -147,6 +152,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
faction_units = ( faction_units = (
set(coalition.faction.frontline_units) set(coalition.faction.frontline_units)
| set(coalition.faction.artillery_units) | set(coalition.faction.artillery_units)
| set(coalition.faction.air_defense_units)
| set(coalition.faction.logistics_units) | set(coalition.faction.logistics_units)
) )
of_class = list({u for u in faction_units if u.unit_class is unit_class}) 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 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): class PretenseTgoGenerator(TgoGenerator):
"""Creates DCS groups and statics for the theater during mission generation. """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( if isinstance(ground_object, CarrierGroundObject) and isinstance(
cp, NavalControlPoint cp, NavalControlPoint
): ):
generator = CarrierGenerator( generator = PretenseCarrierGenerator(
ground_object, ground_object,
cp, cp,
country, country,
@ -709,7 +881,7 @@ class PretenseTgoGenerator(TgoGenerator):
elif isinstance(ground_object, LhaGroundObject) and isinstance( elif isinstance(ground_object, LhaGroundObject) and isinstance(
cp, NavalControlPoint cp, NavalControlPoint
): ):
generator = LhaGenerator( generator = PretenseLhaGenerator(
ground_object, ground_object,
cp, cp,
country, country,

View File

@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
import random
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, List
from dcs import Point from dcs import Point
@ -29,7 +31,10 @@ from dcs.terrain.syria.airports import Damascus, Khalkhalah
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerCondition, TriggerOnce from dcs.triggers import Event, TriggerCondition, TriggerOnce
from dcs.unit import Skill 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 import Airfield
from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn 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_SUPPLY = 500
TRIGGER_RADIUS_PRETENSE_HELI = 1000 TRIGGER_RADIUS_PRETENSE_HELI = 1000
TRIGGER_RADIUS_PRETENSE_HELI_BUFFER = 500 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_LENGTH_PRETENSE = 2500
TRIGGER_RUNWAY_WIDTH_PRETENSE = 400 TRIGGER_RUNWAY_WIDTH_PRETENSE = 400
SIMPLIFY_RUNS_PRETENSE_CARRIER = 10000
class Silence(Option): class Silence(Option):
Key = 7 Key = 7
@ -221,12 +230,105 @@ class PretenseTriggerGenerator:
self.mission.triggerrules.triggers.append(recapture_trigger) self.mission.triggerrules.triggers.append(recapture_trigger)
def _generate_pretense_zone_triggers(self) -> None: def _generate_pretense_zone_triggers(self) -> None:
"""Creates a pair of triggers for each control point of `cls.capture_zone_types`. """Creates triggger zones for the Pretense campaign. These include:
One for the initial capture of a control point, and one if it is recaptured. - Carrier zones for friendly forces, generated from the navmesh / sea zone intersection
Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua` - 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: 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) trigger_radius = float(TRIGGER_RADIUS_PRETENSE_CARRIER)
elif isinstance(cp, Fob) and cp.has_helipads: elif isinstance(cp, Fob) and cp.has_helipads:
trigger_radius = TRIGGER_RADIUS_PRETENSE_HELI trigger_radius = TRIGGER_RADIUS_PRETENSE_HELI
@ -247,6 +349,8 @@ class PretenseTriggerGenerator:
or isinstance(cp.dcs_airport, Khalkhalah) or isinstance(cp.dcs_airport, Khalkhalah)
or isinstance(cp.dcs_airport, Krasnodar_Pashkovsky) 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) trigger_radius = int(TRIGGER_RADIUS_CAPTURE * 1.8)
else: else:
trigger_radius = TRIGGER_RADIUS_CAPTURE trigger_radius = TRIGGER_RADIUS_CAPTURE

View File

@ -158,6 +158,7 @@ class Settings:
MISSION_RESTRICTIONS_SECTION, MISSION_RESTRICTIONS_SECTION,
default=True, default=True,
) )
easy_communication: Optional[bool] = choices_option( easy_communication: Optional[bool] = choices_option(
"Easy Communication", "Easy Communication",
page=DIFFICULTY_PAGE, page=DIFFICULTY_PAGE,
@ -176,6 +177,20 @@ class Settings:
# Campaign management # Campaign management
# General # 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=(
"<p>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.</p>"
),
)
restrict_weapons_by_date: bool = boolean_option( restrict_weapons_by_date: bool = boolean_option(
"Restrict weapons by date (WIP)", "Restrict weapons by date (WIP)",
page=CAMPAIGN_MANAGEMENT_PAGE, page=CAMPAIGN_MANAGEMENT_PAGE,
@ -831,6 +846,17 @@ class Settings:
"Needed to cold-start some aircraft types. Might have a performance impact." "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_unlimited_fuel: bool = boolean_option(
"AI flights have unlimited fuel", "AI flights have unlimited fuel",
MISSION_GENERATOR_PAGE, MISSION_GENERATOR_PAGE,
@ -994,6 +1020,43 @@ class Settings:
"parts of the economy. Use this to adjust performance." "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( pretense_extra_zone_connections: int = bounded_int_option(
"Extra friendly zone connections", "Extra friendly zone connections",
page=PRETENSE_PAGE, page=PRETENSE_PAGE,
@ -1018,7 +1081,7 @@ class Settings:
"Number of AI SEAD flights per control point / zone", "Number of AI SEAD flights per control point / zone",
page=PRETENSE_PAGE, page=PRETENSE_PAGE,
section=GENERAL_SECTION, section=GENERAL_SECTION,
default=2, default=1,
min=1, min=1,
max=10, max=10,
) )
@ -1026,7 +1089,7 @@ class Settings:
"Number of AI CAS flights per control point / zone", "Number of AI CAS flights per control point / zone",
page=PRETENSE_PAGE, page=PRETENSE_PAGE,
section=GENERAL_SECTION, section=GENERAL_SECTION,
default=2, default=1,
min=1, min=1,
max=10, max=10,
) )
@ -1034,7 +1097,7 @@ class Settings:
"Number of AI BAI flights per control point / zone", "Number of AI BAI flights per control point / zone",
page=PRETENSE_PAGE, page=PRETENSE_PAGE,
section=GENERAL_SECTION, section=GENERAL_SECTION,
default=2, default=1,
min=1, min=1,
max=10, max=10,
) )
@ -1042,7 +1105,7 @@ class Settings:
"Number of AI Strike flights per control point / zone", "Number of AI Strike flights per control point / zone",
page=PRETENSE_PAGE, page=PRETENSE_PAGE,
section=GENERAL_SECTION, section=GENERAL_SECTION,
default=2, default=1,
min=1, min=1,
max=10, max=10,
) )
@ -1050,7 +1113,7 @@ class Settings:
"Number of AI BARCAP flights per control point / zone", "Number of AI BARCAP flights per control point / zone",
page=PRETENSE_PAGE, page=PRETENSE_PAGE,
section=GENERAL_SECTION, section=GENERAL_SECTION,
default=2, default=1,
min=1, min=1,
max=10, max=10,
) )
@ -1066,7 +1129,7 @@ class Settings:
"Number of player flights per aircraft type at each base", "Number of player flights per aircraft type at each base",
page=PRETENSE_PAGE, page=PRETENSE_PAGE,
section=GENERAL_SECTION, section=GENERAL_SECTION,
default=2, default=1,
min=1, min=1,
max=10, max=10,
) )

View File

@ -21,6 +21,12 @@ presets = {
cost = 1500, cost = 1500,
type = 'upgrade', type = 'upgrade',
template = "outpost" template = "outpost"
}),
artyBunker = Preset:new({
display = 'Artillery Bunker',
cost = 2000,
type = 'upgrade',
template = "ammo-depot"
}) })
}, },
attack = { attack = {
@ -36,30 +42,12 @@ presets = {
type = 'upgrade', type = 'upgrade',
template = "ammo-depot" template = "ammo-depot"
}), }),
shipTankerSeawisegiant = Preset:new({ chemTank = Preset:new({
display = 'Tanker Seawise Giant', display='Chemical Tank',
cost = 1500, cost = 2000,
type = 'upgrade', type ='upgrade',
template = "ship-tanker-seawisegiant" template = "chem-tank"
}), }),
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"
})
}, },
supply = { supply = {
fuelCache = Preset:new({ fuelCache = Preset:new({
@ -178,30 +166,6 @@ presets = {
income = 50, income = 50,
template = "tv-tower" 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 = { airdef = {
bunker = Preset:new({ bunker = Preset:new({
@ -226,6 +190,12 @@ presets = {
type='defense', type='defense',
template='infantry-red', template='infantry-red',
}), }),
artillery = Preset:new({
display = 'Artillery',
cost=2500,
type='defense',
template='artillery-red',
}),
shorad = Preset:new({ shorad = Preset:new({
display = 'SHORAD', display = 'SHORAD',
cost=2500, cost=2500,
@ -298,6 +268,12 @@ presets = {
type='defense', type='defense',
template='rapier-red', template='rapier-red',
}), }),
roland = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='roland-red',
}),
irondome = Preset:new({ irondome = Preset:new({
display = 'SAM', display = 'SAM',
cost=20000, cost=20000,
@ -324,6 +300,12 @@ presets = {
type='defense', type='defense',
template='infantry-blue', template='infantry-blue',
}), }),
artillery = Preset:new({
display = 'Artillery',
cost=2500,
type='defense',
template='artillery-blue',
}),
shorad = Preset:new({ shorad = Preset:new({
display = 'SHORAD', display = 'SHORAD',
cost=2500, cost=2500,
@ -396,6 +378,12 @@ presets = {
type='defense', type='defense',
template='rapier-blue', template='rapier-blue',
}), }),
roland = Preset:new({
display = 'SAM',
cost=3000,
type='defense',
template='roland-blue',
}),
irondome = Preset:new({ irondome = Preset:new({
display = 'SAM', display = 'SAM',
cost=20000, cost=20000,