diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 0c4bec24..ea0331c0 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, Iterable, Optional +from typing import Any, Dict, Iterable, Optional, Set from dcs.mapping import Point from dcs.task import CAP, CAS, PinpointStrike @@ -35,9 +35,11 @@ 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.airdefensegroupgenerator import AirDefenseRange from gen.sam.sam_group_generator import ( - LONG_RANGE_SAMS, MEDIUM_RANGE_SAMS, generate_anti_air_group, - generate_ewr_group, generate_shorad_group, + generate_anti_air_group, + generate_ewr_group, + generate_shorad_group, ) from . import ( ConflictTheater, @@ -585,9 +587,16 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): """ presets = self.control_point.preset_locations for position in presets.required_long_range_sams: - self.generate_aa_at(position, filter_names=LONG_RANGE_SAMS) + self.generate_aa_at(position, ranges=[ + {AirDefenseRange.Long}, + {AirDefenseRange.Medium}, + {AirDefenseRange.Short}, + ]) for position in presets.required_medium_range_sams: - self.generate_aa_at(position, filter_names=MEDIUM_RANGE_SAMS) + self.generate_aa_at(position, ranges=[ + {AirDefenseRange.Medium}, + {AirDefenseRange.Short}, + ]) return (len(presets.required_long_range_sams) + len(presets.required_medium_range_sams)) @@ -629,25 +638,23 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): position = self.location_finder.location_for(LocationType.Sam) if position is None: return - self.generate_aa_at(position) + self.generate_aa_at(position, ranges=[ + # Prefer to use proper SAMs, but fall back to SHORADs if needed. + {AirDefenseRange.Long, AirDefenseRange.Medium}, + {AirDefenseRange.Short}, + ]) - def generate_aa_at(self, position: Point, - filter_names: Optional[Iterable[str]] = None) -> None: + def generate_aa_at( + self, position: Point, + ranges: Iterable[Set[AirDefenseRange]]) -> 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, - filter_names) + group = generate_anti_air_group(self.game, g, self.faction, ranges) if group is None: - location = f"{g.name} at {self.control_point}" - if filter_names is not None: - logging.warning( - "Could not generate SAM group for %s from types: %s", - location, ", ".join(filter_names) - ) - else: - logging.error("Could not generate SAM group for %s", location) + logging.error("Could not generate air defense group for %s at %s", + g.name, self.control_point) return 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 343a462d..3b678467 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -1,5 +1,5 @@ import random -from typing import Dict, Iterable, List, Optional, Type +from typing import Dict, Iterable, List, Optional, Sequence, Set, Type from dcs.unitgroup import VehicleGroup from dcs.vehicles import AirDefence @@ -13,6 +13,10 @@ from gen.sam.aaa_flak import FlakGenerator from gen.sam.aaa_flak18 import Flak18Generator from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator +from gen.sam.airdefensegroupgenerator import ( + AirDefenseGroupGenerator, + AirDefenseRange, +) from gen.sam.cold_war_flak import ( ColdWarFlakGenerator, EarlyColdWarFlakGenerator, @@ -30,7 +34,6 @@ from gen.sam.ewrs import ( TallRackGenerator, ) from gen.sam.freya_ewr import FreyaGenerator -from gen.sam.airdefensegroupgenerator import AirDefenseGroupGenerator from gen.sam.group_generator import GroupGenerator from gen.sam.sam_avenger import AvengerGenerator from gen.sam.sam_chaparral import ChaparralGenerator @@ -98,17 +101,6 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = { "AllyWW2FlakGenerator": AllyWW2FlakGenerator } -#: Used to fill the long-range required SAM locations in the campaign. -LONG_RANGE_SAMS = { - "SA10Generator", - "PatriotGenerator", - "Tier2SA10Generator", - "Tier3SA10Generator", -} - -#: 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, @@ -159,16 +151,12 @@ EWR_MAP = { def get_faction_possible_sams_generator( - faction: Faction, - filter_names: Optional[Iterable[str]] = None -) -> List[Type[GroupGenerator]]: + faction: Faction) -> List[Type[AirDefenseGroupGenerator]]: """ 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] + return [SAM_MAP[s] for s in faction.sams] def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGenerator]]: @@ -179,23 +167,53 @@ def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGene return [EWR_MAP[s] for s in faction.ewrs] +def _generate_anti_air_from( + generators: Sequence[Type[AirDefenseGroupGenerator]], game: Game, + ground_object: SamGroundObject) -> Optional[VehicleGroup]: + if not generators: + return None + sam_generator_class = random.choice(generators) + generator = sam_generator_class(game, ground_object) + generator.generate() + return generator.get_generated_group() + + def generate_anti_air_group( - game: Game, ground_object: TheaterGroundObject, faction: Faction, - filter_names: Optional[Iterable[str]] = None) -> Optional[VehicleGroup]: + game: Game, ground_object: SamGroundObject, faction: Faction, + ranges: Optional[Iterable[Set[AirDefenseRange]]] = 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. + :param ranges: Optional list of preferred ranges of the air defense to + create. If None, any generator may be used. Otherwise generators + matching the given ranges will be used in order of preference. For + example, when given `[{Long, Medium}, {Short}]`, long and medium range + air defenses will be tried first with no bias, and short range air + defenses will be used if no long or medium range generators are + available to the faction. If instead `[{Long}, {Medium}, {Short}]` had + been used, long range systems would take precedence over medium range + systems. If instead `[{Long, Medium, Short}]` had been used, all types + would be considered with equal preference. :return: The generated group, or None if one could not be generated. """ - 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() + generators = get_faction_possible_sams_generator(faction) + if ranges is None: + ranges = [{ + AirDefenseRange.Long, + AirDefenseRange.Medium, + AirDefenseRange.Short, + }] + + for range_options in ranges: + generators_for_range = [g for g in generators if + g.range() in range_options] + group = _generate_anti_air_from(generators_for_range, game, + ground_object) + if group is not None: + return group return None