diff --git a/changelog.md b/changelog.md index 89c33498..d042e094 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,7 @@ Saves from 2.3 are not compatible with 2.4. * **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats. * **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical. * **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy. +* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior. # 2.3.3 diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index f9ea1d47..118861d1 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -import math import pickle import random from dataclasses import dataclass @@ -15,7 +14,6 @@ from dcs.vehicles import AirDefence from game import Game, db from game.factions.faction import Faction from game.theater import Carrier, Lha, LocationType -from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW from game.theater.theatergroundobject import ( BuildingGroundObject, CarrierGroundObject, @@ -479,11 +477,11 @@ 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) - if group is None: + groups = generate_anti_air_group(self.game, g, self.faction) + if not groups: logging.error(f"Could not generate SAM at {self.control_point}") return - g.groups.append(group) + g.groups = groups self.control_point.base_defenses.append(g) def generate_shorad(self) -> None: @@ -497,13 +495,13 @@ 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, - ranges=[{AirDefenseRange.Short}]) - if group is None: + groups = generate_anti_air_group(self.game, g, self.faction, + ranges=[{AirDefenseRange.Short}]) + if not groups: logging.error( f"Could not generate SHORAD group at {self.control_point}") return - g.groups.append(group) + g.groups = groups self.control_point.base_defenses.append(g) @@ -642,12 +640,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): 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, ranges) - if group is None: + groups = generate_anti_air_group(self.game, g, self.faction, ranges) + if not groups: logging.error("Could not generate air defense group for %s at %s", g.name, self.control_point) return - g.groups = [group] + g.groups = groups self.control_point.connected_objectives.append(g) def generate_missile_sites(self) -> None: diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 64b5e19e..94f44276 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -91,7 +91,6 @@ class TheaterGroundObject(MissionTarget): self.airbase_group = airbase_group self.sea_object = sea_object self.is_dead = False - # TODO: There is never more than one group. self.groups: List[Group] = [] @property diff --git a/gen/sam/airdefensegroupgenerator.py b/gen/sam/airdefensegroupgenerator.py index f58efdf4..20096046 100644 --- a/gen/sam/airdefensegroupgenerator.py +++ b/gen/sam/airdefensegroupgenerator.py @@ -1,5 +1,9 @@ +import logging from abc import ABC, abstractmethod from enum import Enum +from typing import Iterator, List + +from dcs.unitgroup import VehicleGroup from game import Game from gen.sam.group_generator import GroupGenerator @@ -21,6 +25,25 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC): ground_object.skynet_capable = True super().__init__(game, ground_object) + self.auxiliary_groups: List[VehicleGroup] = [] + + def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup: + group = VehicleGroup(self.game.next_group_id(), + "|".join([self.go.group_name, name_suffix])) + self.auxiliary_groups.append(group) + return group + + def get_generated_group(self) -> VehicleGroup: + raise RuntimeError( + "Deprecated call to AirDefenseGroupGenerator.get_generated_group " + "misses auxiliary groups. Use AirDefenseGroupGenerator.groups " + "instead.") + + @property + def groups(self) -> Iterator[VehicleGroup]: + yield self.vg + yield from self.auxiliary_groups + @classmethod @abstractmethod def range(cls) -> AirDefenseRange: diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py index 9422e793..be63b777 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -1,11 +1,13 @@ from __future__ import annotations + import math import random from typing import TYPE_CHECKING, Type from dcs import unitgroup +from dcs.mapping import Point from dcs.point import PointAction -from dcs.unit import Vehicle, Ship +from dcs.unit import Ship, Vehicle from dcs.unittype import VehicleType from game.factions.faction import Faction @@ -40,12 +42,17 @@ class GroupGenerator: def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float, pos_y: float, heading: int) -> Vehicle: + return self.add_unit_to_group(self.vg, unit_type, name, + Point(pos_x, pos_y), heading) + + def add_unit_to_group(self, group: unitgroup.VehicleGroup, + unit_type: Type[VehicleType], name: str, + position: Point, heading: int) -> Vehicle: unit = Vehicle(self.game.next_unit_id(), - f"{self.go.group_name}|{name}", unit_type.id) - unit.position.x = pos_x - unit.position.y = pos_y + f"{group.name}|{name}", unit_type.id) + unit.position = position unit.heading = heading - self.vg.add_unit(unit) + group.add_unit(unit) return unit def get_circular_position(self, num_units, launcher_distance, coverage=90): diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 68e72444..be1f627d 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -171,19 +171,19 @@ def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGene def _generate_anti_air_from( generators: Sequence[Type[AirDefenseGroupGenerator]], game: Game, - ground_object: SamGroundObject) -> Optional[VehicleGroup]: + ground_object: SamGroundObject) -> List[VehicleGroup]: if not generators: - return None + return [] sam_generator_class = random.choice(generators) generator = sam_generator_class(game, ground_object) generator.generate() - return generator.get_generated_group() + return list(generator.groups) def generate_anti_air_group( game: Game, ground_object: SamGroundObject, faction: Faction, ranges: Optional[Iterable[Set[AirDefenseRange]]] = None -) -> Optional[VehicleGroup]: +) -> List[VehicleGroup]: """ This generate a SAM group :param game: The Game. @@ -212,11 +212,11 @@ def generate_anti_air_group( 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 + groups = _generate_anti_air_from(generators_for_range, game, + ground_object) + if groups: + return groups + return [] def generate_ewr_group(game: Game, ground_object: TheaterGroundObject, diff --git a/gen/sam/sam_hawk.py b/gen/sam/sam_hawk.py index 382c4b69..0d526301 100644 --- a/gen/sam/sam_hawk.py +++ b/gen/sam/sam_hawk.py @@ -1,5 +1,6 @@ import random +from dcs.mapping import Point from dcs.vehicles import AirDefence from gen.sam.airdefensegroupgenerator import ( @@ -22,7 +23,9 @@ class HawkGenerator(AirDefenseGroupGenerator): self.add_unit(AirDefence.SAM_Hawk_TR_AN_MPQ_46, "TR", self.position.x + 40, self.position.y, self.heading) # Triple A for close range defense - self.add_unit(AirDefence.AAA_Vulcan_M163, "AAA", self.position.x + 20, self.position.y+30, self.heading) + aa_group = self.add_auxiliary_group("AA") + self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, "AAA", + self.position + Point(20, 30), self.heading) num_launchers = random.randint(3, 6) positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180) diff --git a/gen/sam/sam_hq7.py b/gen/sam/sam_hq7.py index 76951e9a..0fd1d97e 100644 --- a/gen/sam/sam_hq7.py +++ b/gen/sam/sam_hq7.py @@ -1,5 +1,6 @@ import random +from dcs.mapping import Point from dcs.vehicles import AirDefence from gen.sam.airdefensegroupgenerator import ( @@ -21,8 +22,13 @@ class HQ7Generator(AirDefenseGroupGenerator): self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading) # Triple A for close range defense - self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "AAA1", self.position.x + 20, self.position.y+30, self.heading) - self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "AAA2", self.position.x - 20, self.position.y-30, self.heading) + aa_group = self.add_auxiliary_group("AA") + self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375, + "AAA1", self.position + Point(20, 30), + self.heading) + self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375, + "AAA2", self.position.x - Point(20, 30), + self.heading) num_launchers = random.randint(0, 3) if num_launchers > 0: diff --git a/gen/sam/sam_patriot.py b/gen/sam/sam_patriot.py index 14108083..45fcce1a 100644 --- a/gen/sam/sam_patriot.py +++ b/gen/sam/sam_patriot.py @@ -1,5 +1,6 @@ import random +from dcs.mapping import Point from dcs.vehicles import AirDefence from gen.sam.airdefensegroupgenerator import ( @@ -30,10 +31,12 @@ class PatriotGenerator(AirDefenseGroupGenerator): self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2]) # Short range protection for high value site + aa_group = self.add_auxiliary_group("AA") num_launchers = random.randint(3, 4) positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2]) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, + f"SPAAA#{i}", Point(x, y), heading) @classmethod def range(cls) -> AirDefenseRange: diff --git a/gen/sam/sam_sa10.py b/gen/sam/sam_sa10.py index 371bdb5d..2a31816f 100644 --- a/gen/sam/sam_sa10.py +++ b/gen/sam/sam_sa10.py @@ -1,5 +1,6 @@ import random +from dcs.mapping import Point from dcs.vehicles import AirDefence from gen.sam.airdefensegroupgenerator import ( @@ -49,47 +50,46 @@ class SA10Generator(AirDefenseGroupGenerator): def generate_defensive_groups(self) -> None: # AAA for defending against close targets. + aa_group = self.add_auxiliary_group("AA") num_launchers = random.randint(6, 8) positions = self.get_circular_position( num_launchers, launcher_distance=210, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i), - position[0], position[1], position[2]) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(aa_group, AirDefence.SPAAA_ZSU_23_4_Shilka, + f"AA#{i}", Point(x, y), heading) class Tier2SA10Generator(SA10Generator): def generate_defensive_groups(self) -> None: + # Create AAA the way the main group does. + super().generate_defensive_groups() + # SA-15 for both shorter range targets and point defense. + pd_group = self.add_auxiliary_group("PD") num_launchers = random.randint(2, 4) positions = self.get_circular_position( num_launchers, launcher_distance=140, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i), - position[0], position[1], position[2]) - - # AAA for defending against close targets. - num_launchers = random.randint(6, 8) - positions = self.get_circular_position( - num_launchers, launcher_distance=210, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i), - position[0], position[1], position[2]) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331, + f"PD#{i}", Point(x, y), heading) class Tier3SA10Generator(SA10Generator): def generate_defensive_groups(self) -> None: - # SA-15 for both shorter range targets and point defense. - num_launchers = random.randint(2, 4) - positions = self.get_circular_position( - num_launchers, launcher_distance=140, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i), - position[0], position[1], position[2]) - # AAA for defending against close targets. + aa_group = self.add_auxiliary_group("AA") num_launchers = random.randint(6, 8) positions = self.get_circular_position( num_launchers, launcher_distance=210, coverage=360) - for i, position in enumerate(positions): - self.add_unit(AirDefence.SAM_SA_19_Tunguska_2S6, "AA#" + str(i), - position[0], position[1], position[2]) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(aa_group, AirDefence.SAM_SA_19_Tunguska_2S6, + f"AA#{i}", Point(x, y), heading) + + # SA-15 for both shorter range targets and point defense. + pd_group = self.add_auxiliary_group("PD") + num_launchers = random.randint(2, 4) + positions = self.get_circular_position( + num_launchers, launcher_distance=140, coverage=360) + for i, (x, y, heading) in enumerate(positions): + self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331, + f"PD#{i}", Point(x, y), heading) diff --git a/qt_ui/windows/groundobject/QGroundObjectMenu.py b/qt_ui/windows/groundobject/QGroundObjectMenu.py index d6907950..79f2250a 100644 --- a/qt_ui/windows/groundobject/QGroundObjectMenu.py +++ b/qt_ui/windows/groundobject/QGroundObjectMenu.py @@ -355,8 +355,7 @@ class QBuyGroupForGroundObjectDialog(QDialog): # Generate SAM generator = sam_generator(self.game, self.ground_object) generator.generate() - generated_group = generator.get_generated_group() - self.ground_object.groups = [generated_group] + self.ground_object.groups = generator.groups GameUpdateSignal.get_instance().updateBudget(self.game)