Differentiate required long and medium range SAMs.

To improve IADS design in campaigns, this differentiates required long
and medium range SAMs. SAMs that must be long range SAMs are defined by
SA-10 or Patriot launchers, while medium range SAMs are defined by SA-2,
SA-3, or Hawk launchers.

Long range SAMs positions will only be populated by long range SAMs
(Patriots and SA-10s), and not all factions have those available. Medium
range SAMs currently comprise all air defenses that are not long range
SAMs, so if the faction includes flak guns in their `sams` property then
flak guns may be spawned at medium range SAM locations.

Base defenses and random SAM locations continue to use either type of
SAM.
This commit is contained in:
Dan Albert 2020-11-26 19:42:20 -08:00
parent 3ad57d995b
commit 0e807d84c2
5 changed files with 88 additions and 52 deletions

View File

@ -7,7 +7,7 @@ from dataclasses import dataclass
from functools import cached_property
from itertools import tee
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, cast
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
from dcs import Mission
from dcs.countries import (
@ -96,17 +96,17 @@ class MizCampaignLoader:
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id
# Multiple options for the required SAMs so campaign designers can more
# easily see the coverage of their IADS. Designers focused on campaigns that
# will primarily use SA-2s can place SA-2 launchers to ensure that they will
# have adequate coverage, and designers focused on campaigns that will
# primarily use SA-10s can do the same.
REQUIRED_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192,
AirDefence.SAM_Patriot_LN_M901,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C,
AirDefence.SAM_SA_10_S_300PS_LN_5P85D,
AirDefence.SAM_SA_2_LN_SM_90,
AirDefence.SAM_SA_3_S_125_LN_5P73,
# accurately see the coverage of their IADS for the expected type.
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Patriot_LN_M901.id,
AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id,
AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id,
}
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192.id,
AirDefence.SAM_SA_2_LN_SM_90.id,
AirDefence.SAM_SA_3_S_125_LN_5P73.id,
}
BASE_DEFENSE_RADIUS = nm_to_meter(2)
@ -221,9 +221,15 @@ class MizCampaignLoader:
yield group
@property
def required_sams(self) -> Iterator[VehicleGroup]:
def required_long_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type == self.REQUIRED_SAM_UNIT_TYPES:
if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES:
yield group
@property
def required_medium_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
yield group
@cached_property
@ -335,9 +341,17 @@ class MizCampaignLoader:
closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append(group.position)
for group in self.required_sams:
for group in self.required_long_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_sams.append(group.position)
closest.preset_locations.required_long_range_sams.append(
group.position
)
for group in self.required_medium_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.required_medium_range_sams.append(
group.position
)
def populate_theater(self) -> None:
for control_point in self.control_points.values():

View File

@ -95,8 +95,11 @@ class PresetLocations:
#: Locations used by missile sites like scuds and V-2s.
missile_sites: List[Point] = field(default_factory=list)
#: Locations of SAMs which should always be spawned.
required_sams: List[Point] = field(default_factory=list)
#: Locations of long range SAMs which should always be spawned.
required_long_range_sams: List[Point] = field(default_factory=list)
#: Locations of medium range SAMs which should always be spawned.
required_medium_range_sams: List[Point] = field(default_factory=list)
@staticmethod
def _random_from(points: List[Point]) -> Optional[Point]:

View File

@ -4,7 +4,7 @@ import logging
import math
import pickle
import random
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
@ -36,7 +36,7 @@ from gen.fleet.ship_group_generator import (
from gen.locations.preset_location_finder import MizDataLocationFinder
from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.sam_group_generator import (
generate_anti_air_group,
LONG_RANGE_SAMS, MEDIUM_RANGE_SAMS, generate_anti_air_group,
generate_ewr_group, generate_shorad_group,
)
from . import (
@ -268,7 +268,6 @@ class LocationFinder:
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on
ground)
:param theater: Theater object
:param min_range: Minimal range from point
:param max_range: Max range from point
:param is_base_defense: True if the location is for base defense.
@ -459,7 +458,7 @@ class BaseDefenseGenerator:
g = EwrGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point)
group = generate_ewr_group(self.game, g, self.faction_name)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
return
@ -507,7 +506,7 @@ class BaseDefenseGenerator:
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=True)
group = generate_anti_air_group(self.game, g, self.faction_name)
group = generate_anti_air_group(self.game, g, self.faction)
if group is not None:
g.groups.append(group)
self.control_point.base_defenses.append(g)
@ -523,7 +522,7 @@ class BaseDefenseGenerator:
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=True)
group = generate_shorad_group(self.game, g, self.faction_name)
group = generate_shorad_group(self.game, g, self.faction)
if group is not None:
g.groups.append(group)
self.control_point.base_defenses.append(g)
@ -575,10 +574,13 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
Returns:
The number of AA sites that were generated.
"""
sams = self.control_point.preset_locations.required_sams
for position in sams:
self.generate_aa_at(position)
return len(sams)
presets = self.control_point.preset_locations
for position in presets.required_long_range_sams:
self.generate_aa_at(position, filter_names=LONG_RANGE_SAMS)
for position in presets.required_medium_range_sams:
self.generate_aa_at(position, filter_names=MEDIUM_RANGE_SAMS)
return (len(presets.required_long_range_sams) +
len(presets.required_medium_range_sams))
def generate_ground_point(self) -> None:
try:
@ -620,12 +622,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return
self.generate_aa_at(position)
def generate_aa_at(self, position: Point) -> None:
def generate_aa_at(self, position: Point,
filter_names: Optional[List[str]] = None) -> None:
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=False)
group = generate_anti_air_group(self.game, g, self.faction_name)
group = generate_anti_air_group(self.game, g, self.faction,
filter_names)
if group is not None:
g.groups = [group]
self.control_point.connected_objectives.append(g)

View File

@ -5,6 +5,7 @@ from dcs.unitgroup import VehicleGroup
from dcs.vehicles import AirDefence
from game import Game, db
from game.factions.faction import Faction
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import SamGroundObject
from gen.sam.aaa_bofors import BoforsGenerator
@ -90,6 +91,16 @@ SAM_MAP = {
"AllyWW2FlakGenerator": AllyWW2FlakGenerator
}
#: Used to fill the long-range required SAM locations in the campaign.
LONG_RANGE_SAMS = {
"SA10Generator",
"PatriotGenerator",
}
#: Used to fill the medium-range required SAM location in the campaign.
MEDIUM_RANGE_SAMS = SAM_MAP.keys() - LONG_RANGE_SAMS
SAM_PRICES = {
AirDefence.SAM_Hawk_PCP: 35,
AirDefence.AAA_ZU_23_Emplacement: 10,
@ -138,34 +149,40 @@ EWR_MAP = {
}
def get_faction_possible_sams_generator(faction: str) -> List[Type[GroupGenerator]]:
def get_faction_possible_sams_generator(
faction: Faction,
filter_names: Optional[List[str]] = None) -> List[Type[GroupGenerator]]:
"""
Return the list of possible SAM generator for the given faction
:param faction: Faction name to search units for
:param filter_names: Optional list of names to filter allowed SAMs by.
"""
return [SAM_MAP[s] for s in faction.sams if
filter_names is None or s in filter_names]
def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGenerator]]:
"""
Return the list of possible SAM generator for the given faction
:param faction: Faction name to search units for
"""
return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP]
return [EWR_MAP[s] for s in faction.ewrs]
def get_faction_possible_ewrs_generator(faction: str) -> List[Type[GroupGenerator]]:
"""
Return the list of possible SAM generator for the given faction
:param faction: Faction name to search units for
"""
return [EWR_MAP[s] for s in db.FACTIONS[faction].ewrs if s in EWR_MAP]
def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject,
faction: str) -> Optional[VehicleGroup]:
def generate_anti_air_group(
game: Game, ground_object: TheaterGroundObject, faction: Faction,
filter_names: Optional[List[str]] = None) -> Optional[VehicleGroup]:
"""
This generate a SAM group
:param game: The Game.
:param ground_object: The ground object which will own the sam group.
:param faction: Owner faction.
:param filter_names: Optional list of names to filter allowed SAMs by.
:return: The generated group, or None if one could not be generated.
"""
possible_sams_generators = get_faction_possible_sams_generator(faction)
if len(possible_sams_generators) > 0:
sam_generator_class = random.choice(possible_sams_generators)
generators = get_faction_possible_sams_generator(faction, filter_names)
if len(generators) > 0:
sam_generator_class = random.choice(generators)
generator = sam_generator_class(game, ground_object)
generator.generate()
return generator.get_generated_group()
@ -173,7 +190,7 @@ def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject,
def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
faction: str) -> Optional[VehicleGroup]:
faction: Faction) -> Optional[VehicleGroup]:
"""Generates an early warning radar group.
:param game: The Game.
@ -191,13 +208,11 @@ def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
def generate_shorad_group(game: Game, ground_object: SamGroundObject,
faction_name: str) -> Optional[VehicleGroup]:
faction = db.FACTIONS[faction_name]
faction: Faction) -> Optional[VehicleGroup]:
if len(faction.shorads) > 0:
sam = random.choice(faction.shorads)
generator = SAM_MAP[sam](game, ground_object)
generator.generate()
return generator.get_generated_group()
else:
return generate_anti_air_group(game, ground_object, faction_name)
return generate_anti_air_group(game, ground_object, faction)

View File

@ -248,7 +248,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.init_ui()
def init_ui(self):
faction = self.game.player_name
faction = self.game.player_faction
# Sams
@ -268,7 +268,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
# Armored units
armored_units = db.find_unittype(PinpointStrike, faction) # Todo : refactor this legacy nonsense
armored_units = db.find_unittype(PinpointStrike, faction.name) # Todo : refactor this legacy nonsense
for unit in set(armored_units):
self.buyArmorCombo.addItem(db.unit_type_name_2(unit) + " [$" + str(db.PRICES[unit]) + "M]", userData=unit)
self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged)