diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 74c0b77b..7ae6938b 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -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(): diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index ef689c03..54759d3c 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -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]: diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index c5232a32..95398aec 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -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) diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index b5855750..12614692 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -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) diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index 7f6c934f..b1eabf7a 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -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)