diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index e62a39f0..867a7c5b 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -1,7 +1,6 @@ from __future__ import annotations import itertools -import logging import math from dataclasses import dataclass from functools import cached_property @@ -40,9 +39,6 @@ from dcs.unitgroup import ( VehicleGroup, ) from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed - -from .latlon import LatLon -from ..scenery_group import SceneryGroup from pyproj import CRS, Transformer from shapely import geometry, ops @@ -57,10 +53,12 @@ from .controlpoint import ( ) from .frontline import FrontLine from .landmap import Landmap, load_landmap, poly_contains +from .latlon import LatLon from .projections import TransverseMercator from ..point_with_heading import PointWithHeading from ..profiling import logged_duration -from ..utils import Distance, meters, nautical_miles +from ..scenery_group import SceneryGroup +from ..utils import Distance, meters SIZE_TINY = 150 SIZE_SMALL = 600 @@ -87,42 +85,39 @@ class MizCampaignLoader: FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id FARP_HELIPAD = "SINGLE_HELIPAD" - EWR_UNIT_TYPE = AirDefence.EWR_55G6.id - SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id - GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_Grison.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id - # Multiple options for the required SAMs so campaign designers can more - # accurately see the coverage of their IADS for the expected type. - REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = { + # Multiple options for air defenses so campaign designers can more accurately see + # the coverage of their IADS for the expected type. + LONG_RANGE_SAM_UNIT_TYPES = { AirDefence.SAM_Patriot_LN.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id, } - REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = { + MEDIUM_RANGE_SAM_UNIT_TYPES = { AirDefence.SAM_Hawk_LN_M192.id, AirDefence.SAM_SA_2_S_75_Guideline_LN.id, AirDefence.SAM_SA_3_S_125_Goa_LN.id, } - REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES = { + SHORT_RANGE_SAM_UNIT_TYPES = { AirDefence.SAM_Avenger__Stinger.id, AirDefence.SAM_Rapier_LN.id, AirDefence.SAM_SA_19_Tunguska_Grison.id, AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id, } - REQUIRED_AAA_UNIT_TYPES = { + AAA_UNIT_TYPES = { AirDefence.AAA_8_8cm_Flak_18.id, AirDefence.SPAAA_Vulcan_M163.id, AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id, } - REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id + EWR_UNIT_TYPE = AirDefence.EWR_1L13.id ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id @@ -130,9 +125,7 @@ class MizCampaignLoader: AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id - REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id - - BASE_DEFENSE_RADIUS = nautical_miles(2) + STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id def __init__(self, miz: Path, theater: ConflictTheater) -> None: self.theater = theater @@ -210,98 +203,56 @@ class MizCampaignLoader: @property def ships(self) -> Iterator[ShipGroup]: - for group in self.blue.ship_group: - if group.units[0].type == self.SHIP_UNIT_TYPE: - yield group - - @property - def required_ships(self) -> Iterator[ShipGroup]: for group in self.red.ship_group: if group.units[0].type == self.SHIP_UNIT_TYPE: yield group - @property - def ewrs(self) -> Iterator[VehicleGroup]: - for group in self.blue.vehicle_group: - if group.units[0].type == self.EWR_UNIT_TYPE: - yield group - - @property - def sams(self) -> Iterator[VehicleGroup]: - for group in self.blue.vehicle_group: - if group.units[0].type == self.SAM_UNIT_TYPE: - yield group - - @property - def garrisons(self) -> Iterator[VehicleGroup]: - for group in self.blue.vehicle_group: - if group.units[0].type == self.GARRISON_UNIT_TYPE: - yield group - @property def offshore_strike_targets(self) -> Iterator[StaticGroup]: - for group in self.blue.static_group: - if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE: - yield group - - @property - def required_offshore_strike_targets(self) -> Iterator[StaticGroup]: for group in self.red.static_group: if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE: yield group @property def missile_sites(self) -> Iterator[VehicleGroup]: - for group in self.blue.vehicle_group: - if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE: - yield group - - @property - def required_missile_sites(self) -> Iterator[VehicleGroup]: for group in self.red.vehicle_group: if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE: yield group @property def coastal_defenses(self) -> Iterator[VehicleGroup]: - for group in self.blue.vehicle_group: - if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE: - yield group - - @property - def required_coastal_defenses(self) -> Iterator[VehicleGroup]: for group in self.red.vehicle_group: if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE: yield group @property - def required_long_range_sams(self) -> Iterator[VehicleGroup]: + def long_range_sams(self) -> Iterator[VehicleGroup]: for group in self.red.vehicle_group: - if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES: + if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES: yield group @property - def required_medium_range_sams(self) -> Iterator[VehicleGroup]: + def 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: + if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES: yield group @property - def required_short_range_sams(self) -> Iterator[VehicleGroup]: + def short_range_sams(self) -> Iterator[VehicleGroup]: for group in self.red.vehicle_group: - if group.units[0].type in self.REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES: + if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES: yield group @property - def required_aaa(self) -> Iterator[VehicleGroup]: + def aaa(self) -> Iterator[VehicleGroup]: for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group): - if group.units[0].type in self.REQUIRED_AAA_UNIT_TYPES: + if group.units[0].type in self.AAA_UNIT_TYPES: yield group @property - def required_ewrs(self) -> Iterator[VehicleGroup]: + def ewrs(self) -> Iterator[VehicleGroup]: for group in self.red.vehicle_group: - if group.units[0].type in self.REQUIRED_EWR_UNIT_TYPE: + if group.units[0].type in self.EWR_UNIT_TYPE: yield group @property @@ -329,9 +280,9 @@ class MizCampaignLoader: yield group @property - def required_strike_targets(self) -> Iterator[StaticGroup]: + def strike_targets(self) -> Iterator[StaticGroup]: for group in itertools.chain(self.blue.static_group, self.red.static_group): - if group.units[0].type in self.REQUIRED_STRIKE_TARGET_UNIT_TYPE: + if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE: yield group @property @@ -440,112 +391,57 @@ class MizCampaignLoader: return closest, distance def add_preset_locations(self) -> None: - for group in self.garrisons: - closest, distance = self.objective_info(group) - if distance < self.BASE_DEFENSE_RADIUS: - closest.preset_locations.base_garrisons.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - else: - logging.warning(f"Found garrison unit too far from base: {group.name}") - - for group in self.sams: - closest, distance = self.objective_info(group) - if distance < self.BASE_DEFENSE_RADIUS: - closest.preset_locations.base_air_defense.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - else: - closest.preset_locations.strike_locations.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - - for group in self.ewrs: - closest, distance = self.objective_info(group) - if distance < self.BASE_DEFENSE_RADIUS: - closest.preset_locations.base_ewrs.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - else: - closest.preset_locations.ewrs.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - for group in self.offshore_strike_targets: closest, distance = self.objective_info(group) closest.preset_locations.offshore_strike_locations.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_offshore_strike_targets: - closest, distance = self.objective_info(group) - closest.preset_locations.required_offshore_strike_locations.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - for group in self.ships: closest, distance = self.objective_info(group) closest.preset_locations.ships.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_ships: - closest, distance = self.objective_info(group) - closest.preset_locations.required_ships.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - for group in self.missile_sites: closest, distance = self.objective_info(group) closest.preset_locations.missile_sites.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_missile_sites: - closest, distance = self.objective_info(group) - closest.preset_locations.required_missile_sites.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - for group in self.coastal_defenses: closest, distance = self.objective_info(group) closest.preset_locations.coastal_defenses.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_coastal_defenses: + for group in self.long_range_sams: closest, distance = self.objective_info(group) - closest.preset_locations.required_coastal_defenses.append( + closest.preset_locations.long_range_sams.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_long_range_sams: + for group in self.medium_range_sams: closest, distance = self.objective_info(group) - closest.preset_locations.required_long_range_sams.append( + closest.preset_locations.medium_range_sams.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_medium_range_sams: + for group in self.short_range_sams: closest, distance = self.objective_info(group) - closest.preset_locations.required_medium_range_sams.append( + closest.preset_locations.short_range_sams.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_short_range_sams: + for group in self.aaa: closest, distance = self.objective_info(group) - closest.preset_locations.required_short_range_sams.append( + closest.preset_locations.aaa.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_aaa: + for group in self.ewrs: closest, distance = self.objective_info(group) - closest.preset_locations.required_aaa.append( - PointWithHeading.from_point(group.position, group.units[0].heading) - ) - - for group in self.required_ewrs: - closest, distance = self.objective_info(group) - closest.preset_locations.required_ewrs.append( + closest.preset_locations.ewrs.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) @@ -573,9 +469,9 @@ class MizCampaignLoader: PointWithHeading.from_point(group.position, group.units[0].heading) ) - for group in self.required_strike_targets: + for group in self.strike_targets: closest, distance = self.objective_info(group) - closest.preset_locations.required_strike_locations.append( + closest.preset_locations.strike_locations.append( PointWithHeading.from_point(group.position, group.units[0].heading) ) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index c3b0c235..05f35d72 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -3,7 +3,6 @@ from __future__ import annotations import heapq import itertools import logging -import random from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass, field @@ -44,12 +43,8 @@ from gen.runways import RunwayAssigner, RunwayData from .base import Base from .missiontarget import MissionTarget from .theatergroundobject import ( - BaseDefenseGroundObject, - EwrGroundObject, GenericCarrierGroundObject, - SamGroundObject, TheaterGroundObject, - VehicleGroupGroundObject, ) from ..db import PRICES from ..utils import nautical_miles @@ -78,139 +73,55 @@ class ControlPointType(Enum): OFF_MAP = 6 -class LocationType(Enum): - BaseAirDefense = "base air defense" - Coastal = "coastal defense" - Ewr = "EWR" - BaseEwr = "Base EWR" - Garrison = "garrison" - MissileSite = "missile site" - OffshoreStrikeTarget = "offshore strike target" - Sam = "SAM" - Ship = "ship" - Shorad = "SHORAD" - StrikeTarget = "strike target" - - @dataclass class PresetLocations: """Defines the preset locations loaded from the campaign mission file.""" - #: Locations used for spawning ground defenses for bases. - base_garrisons: List[PointWithHeading] = field(default_factory=list) - - #: Locations used for spawning air defenses for bases. Used by SAMs, AAA, - #: and SHORADs. - base_air_defense: List[PointWithHeading] = field(default_factory=list) - - #: Locations used by EWRs. - ewrs: List[PointWithHeading] = field(default_factory=list) - - #: Locations used by Base EWRs. - base_ewrs: List[PointWithHeading] = field(default_factory=list) - - #: Locations used by non-carrier ships. Carriers and LHAs are not random. + #: Locations used by non-carrier ships that will be spawned unless the faction has + #: no navy or the player has disabled ship generation for the owning side. ships: List[PointWithHeading] = field(default_factory=list) - #: Locations used by non-carrier ships that will be spawned unless the faction has - #: no navy or the player has disable ship generation for the original owning side. - required_ships: List[PointWithHeading] = field(default_factory=list) - - #: Locations used by coastal defenses. + #: Locations used by coastal defenses that are generated if the faction is capable. coastal_defenses: List[PointWithHeading] = field(default_factory=list) - #: Locations used by coastal defenses that are always generated if the faction is - #: capable. - required_coastal_defenses: List[PointWithHeading] = field(default_factory=list) - #: Locations used by ground based strike objectives. strike_locations: List[PointWithHeading] = field(default_factory=list) - #: Locations used by ground based strike objectives that will always be spawned. - required_strike_locations: List[PointWithHeading] = field(default_factory=list) - #: Locations used by offshore strike objectives. offshore_strike_locations: List[PointWithHeading] = field(default_factory=list) - #: Locations used by offshore strike objectives that will always be spawned. - required_offshore_strike_locations: List[PointWithHeading] = field( - default_factory=list - ) - - #: Locations used by missile sites like scuds and V-2s. + #: Locations used by missile sites like scuds and V-2s that are generated if the + #: faction is capable. missile_sites: List[PointWithHeading] = field(default_factory=list) - #: Locations used by missile sites like scuds and V-2s that are always generated if - #: the faction is capable. - required_missile_sites: List[PointWithHeading] = field(default_factory=list) + #: Locations of long range SAMs. + long_range_sams: List[PointWithHeading] = field(default_factory=list) - #: Locations of long range SAMs which should always be spawned. - required_long_range_sams: List[PointWithHeading] = field(default_factory=list) + #: Locations of medium range SAMs. + medium_range_sams: List[PointWithHeading] = field(default_factory=list) - #: Locations of medium range SAMs which should always be spawned. - required_medium_range_sams: List[PointWithHeading] = field(default_factory=list) + #: Locations of short range SAMs. + short_range_sams: List[PointWithHeading] = field(default_factory=list) - #: Locations of short range SAMs which should always be spawned. - required_short_range_sams: List[PointWithHeading] = field(default_factory=list) + #: Locations of AAA groups. + aaa: List[PointWithHeading] = field(default_factory=list) - #: Locations of AAA groups which should always be spawned. - required_aaa: List[PointWithHeading] = field(default_factory=list) - - #: Locations of EWRs which should always be spawned. - required_ewrs: List[PointWithHeading] = field(default_factory=list) + #: Locations of EWRs. + ewrs: List[PointWithHeading] = field(default_factory=list) #: Locations of map scenery to create zones for. scenery: List[SceneryGroup] = field(default_factory=list) - #: Locations of factories for producing ground units. These will always be spawned. + #: Locations of factories for producing ground units. factories: List[PointWithHeading] = field(default_factory=list) - #: Locations of ammo depots for controlling number of units on the front line at a control point. + #: Locations of ammo depots for controlling number of units on the front line at a + #: control point. ammunition_depots: List[PointWithHeading] = field(default_factory=list) - #: Locations of stationary armor groups. These will always be spawned. + #: Locations of stationary armor groups. armor_groups: List[PointWithHeading] = field(default_factory=list) - @staticmethod - def _random_from(points: List[PointWithHeading]) -> Optional[PointWithHeading]: - """Finds, removes, and returns a random position from the given list.""" - if not points: - return None - point = random.choice(points) - points.remove(point) - return point - - def random_for(self, location_type: LocationType) -> Optional[PointWithHeading]: - """Returns a position suitable for the given location type. - - The location, if found, will be claimed by the caller and not available - to subsequent calls. - """ - if location_type == LocationType.BaseAirDefense: - return self._random_from(self.base_air_defense) - if location_type == LocationType.Coastal: - return self._random_from(self.coastal_defenses) - if location_type == LocationType.Ewr: - return self._random_from(self.ewrs) - if location_type == LocationType.BaseEwr: - return self._random_from(self.base_ewrs) - if location_type == LocationType.Garrison: - return self._random_from(self.base_garrisons) - if location_type == LocationType.MissileSite: - return self._random_from(self.missile_sites) - if location_type == LocationType.OffshoreStrikeTarget: - return self._random_from(self.offshore_strike_locations) - if location_type == LocationType.Sam: - return self._random_from(self.strike_locations) - if location_type == LocationType.Ship: - return self._random_from(self.ships) - if location_type == LocationType.Shorad: - return self._random_from(self.base_garrisons) - if location_type == LocationType.StrikeTarget: - return self._random_from(self.strike_locations) - logging.error(f"Unknown location type: {location_type}") - return None - @dataclass(frozen=True) class PendingOccupancy: @@ -338,7 +249,6 @@ class ControlPoint(MissionTarget, ABC): self.full_name = name self.at = at self.connected_objectives: List[TheaterGroundObject] = [] - self.base_defenses: List[BaseDefenseGroundObject] = [] self.preset_locations = PresetLocations() self.helipads: List[PointWithHeading] = [] @@ -367,7 +277,7 @@ class ControlPoint(MissionTarget, ABC): @property def ground_objects(self) -> List[TheaterGroundObject]: - return list(itertools.chain(self.connected_objectives, self.base_defenses)) + return list(self.connected_objectives) @property @abstractmethod @@ -553,24 +463,6 @@ class ControlPoint(MissionTarget, ABC): def is_friendly_to(self, control_point: ControlPoint) -> bool: return control_point.is_friendly(self.captured) - # TODO: Should be Airbase specific. - def clear_base_defenses(self) -> None: - for base_defense in self.base_defenses: - p = PointWithHeading.from_point(base_defense.position, base_defense.heading) - if isinstance(base_defense, EwrGroundObject): - self.preset_locations.base_ewrs.append(p) - elif isinstance(base_defense, SamGroundObject): - self.preset_locations.base_air_defense.append(p) - elif isinstance(base_defense, VehicleGroupGroundObject): - self.preset_locations.base_garrisons.append(p) - else: - logging.error( - "Could not determine preset location type for " - f"{base_defense}. Assuming garrison type." - ) - self.preset_locations.base_garrisons.append(p) - self.base_defenses = [] - def capture_equipment(self, game: Game) -> None: total = self.base.total_armor_value self.base.armor.clear() @@ -668,11 +560,6 @@ class ControlPoint(MissionTarget, ABC): self.base.set_strength_to_minimum() - self.clear_base_defenses() - from .start_generator import BaseDefenseGenerator - - BaseDefenseGenerator(game, self).generate() - @abstractmethod def can_operate(self, aircraft: Type[FlyingType]) -> bool: ... diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 88ad2bcd..37b08bca 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -1,12 +1,11 @@ from __future__ import annotations -from game.scenery_group import SceneryGroup import logging import pickle import random from dataclasses import dataclass from datetime import datetime -from typing import Any, Dict, Iterable, List, Optional, Set +from typing import Any, Dict, Iterable, List, Set from dcs.mapping import Point from dcs.task import CAP, CAS, PinpointStrike @@ -14,7 +13,8 @@ from dcs.vehicles import AirDefence from game import Game, db from game.factions.faction import Faction -from game.theater import Carrier, Lha, LocationType, PointWithHeading +from game.scenery_group import SceneryGroup +from game.theater import Carrier, Lha, PointWithHeading from game.theater.theatergroundobject import ( BuildingGroundObject, CarrierGroundObject, @@ -39,8 +39,8 @@ from gen.fleet.ship_group_generator import ( ) from gen.missiles.missiles_group_generator import generate_missile_group from gen.sam.airdefensegroupgenerator import AirDefenseRange -from gen.sam.sam_group_generator import generate_anti_air_group from gen.sam.ewr_group_generator import generate_ewr_group +from gen.sam.sam_group_generator import generate_anti_air_group from . import ( ConflictTheater, ControlPoint, @@ -145,24 +145,6 @@ class GameGenerator: cp.captured = True -class LocationFinder: - def __init__(self, control_point: ControlPoint) -> None: - self.control_point = control_point - - def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]: - position = self.control_point.preset_locations.random_for(location_type) - if position is not None: - logging.warning( - f"Campaign relies on random generation of %s at %s. Support for random " - "objectives will be removed soon.", - location_type.value, - self.control_point, - ) - return position - - return None - - class ControlPointGroundObjectGenerator: def __init__( self, @@ -173,7 +155,6 @@ class ControlPointGroundObjectGenerator: self.game = game self.generator_settings = generator_settings self.control_point = control_point - self.location_finder = LocationFinder(control_point) @property def faction_name(self) -> str: @@ -203,19 +184,9 @@ class ControlPointGroundObjectGenerator: if not self.control_point.captured and skip_enemy_navy: return - self.generate_required_ships() - for _ in range(self.faction.navy_group_count): - self.generate_ship() - - def generate_required_ships(self) -> None: - for position in self.control_point.preset_locations.required_ships: + for position in self.control_point.preset_locations.ships: self.generate_ship_at(position) - def generate_ship(self) -> None: - point = self.location_finder.location_for(LocationType.Ship) - if point is not None: - self.generate_ship_at(point) - def generate_ship_at(self, position: PointWithHeading) -> None: group_id = self.game.next_group_id() @@ -289,159 +260,6 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator): return True -class BaseDefenseGenerator: - def __init__(self, game: Game, control_point: ControlPoint) -> None: - self.game = game - self.control_point = control_point - self.location_finder = LocationFinder(control_point) - - @property - def faction_name(self) -> str: - if self.control_point.captured: - return self.game.player_name - else: - return self.game.enemy_name - - @property - def faction(self) -> Faction: - return db.FACTIONS[self.faction_name] - - def generate(self) -> None: - self.generate_ewr() - self.generate_garrison() - self.generate_base_defenses() - - def generate_ewr(self) -> None: - position = self.location_finder.location_for(LocationType.BaseEwr) - if position is None: - return - - group_id = self.game.next_group_id() - - g = EwrGroundObject( - namegen.random_objective_name(), - group_id, - position, - self.control_point, - True, - ) - - group = generate_ewr_group(self.game, g, self.faction) - if group is None: - logging.error(f"Could not generate EWR at {self.control_point}") - return - - g.groups = [group] - self.control_point.base_defenses.append(g) - - def generate_base_defenses(self) -> None: - # First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD, - # and a 1/6 chance of a garrison. - # - # Further groups have a 1/3 chance of being SHORAD and 2/3 chance of - # being a garrison. - for i in range(random.randint(2, 5)): - if i == 0 and random.randint(0, 1) == 0: - self.generate_sam() - elif random.randint(0, 2) == 1: - self.generate_shorad() - else: - self.generate_garrison() - - def generate_garrison(self) -> None: - position = self.location_finder.location_for(LocationType.Garrison) - if position is None: - return - - group_id = self.game.next_group_id() - - g = VehicleGroupGroundObject( - namegen.random_objective_name(), - group_id, - position, - self.control_point, - for_airbase=True, - ) - - group = generate_armor_group(self.faction_name, self.game, g) - if group is None: - logging.error(f"Could not generate garrison at {self.control_point}") - return - g.groups.append(group) - self.control_point.base_defenses.append(g) - - def generate_sam(self) -> None: - position = self.location_finder.location_for(LocationType.BaseAirDefense) - if position is None: - return - - group_id = self.game.next_group_id() - - g = SamGroundObject( - namegen.random_objective_name(), - group_id, - position, - self.control_point, - for_airbase=True, - ) - - 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 = groups - self.control_point.base_defenses.append(g) - - def generate_shorad(self) -> None: - position = self.location_finder.location_for(LocationType.BaseAirDefense) - if position is None: - return - - group_id = self.game.next_group_id() - - g = SamGroundObject( - namegen.random_objective_name(), - group_id, - position, - self.control_point, - for_airbase=True, - ) - - groups = generate_anti_air_group( - self.game, - g, - self.faction, - ranges=[{AirDefenseRange.Short, AirDefenseRange.AAA}], - ) - if not groups: - logging.error(f"Could not generate SHORAD group at {self.control_point}") - return - g.groups = groups - self.control_point.base_defenses.append(g) - - -class FobDefenseGenerator(BaseDefenseGenerator): - def generate(self) -> None: - self.generate_garrison() - self.generate_fob_defenses() - - def generate_fob_defenses(self): - # First group has a 1/2 chance of being a SHORAD, - # and a 1/2 chance of a garrison. - # - # Further groups have a 1/3 chance of being SHORAD and 2/3 chance of - # being a garrison. - for i in range(random.randint(2, 5)): - if i == 0 and random.randint(0, 1) == 0: - self.generate_shorad() - elif i == 0 and random.randint(0, 1) == 0: - self.generate_garrison() - elif random.randint(0, 2) == 1: - self.generate_shorad() - else: - self.generate_garrison() - - class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): def __init__( self, @@ -457,16 +275,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): if not super().generate(): return False - BaseDefenseGenerator(self.game, self.control_point).generate() self.generate_ground_points() - return True def generate_ground_points(self) -> None: """Generate ground objects and AA sites for the control point.""" self.generate_armor_groups() - skip_sams = self.generate_required_aa() - skip_ewrs = self.generate_required_ewr() + self.generate_aa() + self.generate_ewrs() self.generate_scenery_sites() self.generate_strike_targets() self.generate_offshore_strike_targets() @@ -475,35 +291,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): if self.faction.missiles: self.generate_missile_sites() - self.generate_required_missile_sites() if self.faction.coastal_defenses: self.generate_coastal_sites() - self.generate_required_coastal_sites() - - if self.control_point.is_global: - return - - # Always generate at least one AA point. - self.generate_aa_site() - - # And between 2 and 7 other objectives. - amount = random.randrange(2, 7) - for i in range(amount): - # 1 in 4 additional objectives are AA. - if random.randint(0, 3) == 0: - if skip_sams > 0: - skip_sams -= 1 - else: - self.generate_aa_site() - # 1 in 4 additional objectives are EWR. - elif random.randint(0, 3) == 0: - if skip_ewrs > 0: - skip_ewrs -= 1 - else: - self.generate_ewr_site() - else: - self.generate_ground_point() def generate_armor_groups(self) -> None: for position in self.control_point.preset_locations.armor_groups: @@ -531,14 +321,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): g.groups = [group] self.control_point.connected_objectives.append(g) - def generate_required_aa(self) -> int: - """Generates the AA sites that are required by the campaign. - - Returns: - The number of AA sites that were generated. - """ + def generate_aa(self) -> None: presets = self.control_point.preset_locations - for position in presets.required_long_range_sams: + for position in presets.long_range_sams: self.generate_aa_at( position, ranges=[ @@ -548,7 +333,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): {AirDefenseRange.AAA}, ], ) - for position in presets.required_medium_range_sams: + for position in presets.medium_range_sams: self.generate_aa_at( position, ranges=[ @@ -557,52 +342,21 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): {AirDefenseRange.AAA}, ], ) - for position in presets.required_short_range_sams: + for position in presets.short_range_sams: self.generate_aa_at( position, ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}], ) - for position in presets.required_aaa: + for position in presets.aaa: self.generate_aa_at( position, ranges=[{AirDefenseRange.AAA}], ) - return ( - len(presets.required_long_range_sams) - + len(presets.required_medium_range_sams) - + len(presets.required_short_range_sams) - + len(presets.required_aaa) - ) - def generate_required_ewr(self) -> int: - """Generates the EWR sites that are required by the campaign. - - Returns: - The number of EWR sites that were generated. - """ + def generate_ewrs(self) -> None: presets = self.control_point.preset_locations - for position in presets.required_ewrs: + for position in presets.ewrs: self.generate_ewr_at(position) - return len(presets.required_ewrs) - - def generate_ground_point(self) -> None: - try: - category = random.choice(self.faction.building_set) - except IndexError: - logging.exception("Faction has no buildings defined") - return - - if category == "oil": - location_type = LocationType.OffshoreStrikeTarget - else: - location_type = LocationType.StrikeTarget - - # Pick from preset locations - point = self.location_finder.location_for(location_type) - if point is None: - return - - self.generate_strike_target_at(category, point) def generate_strike_target_at(self, category: str, position: Point) -> None: @@ -635,7 +389,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): self.generate_strike_target_at(category="ammo", position=position) def generate_factories(self) -> None: - """Generates the factories that are required by the campaign.""" for position in self.control_point.preset_locations.factories: self.generate_factory_at(position) @@ -653,19 +406,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): self.control_point.connected_objectives.append(g) - def generate_aa_site(self) -> None: - position = self.location_finder.location_for(LocationType.Sam) - if position is None: - return - 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, ranges: Iterable[Set[AirDefenseRange]] ) -> None: @@ -689,12 +429,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): g.groups = groups self.control_point.connected_objectives.append(g) - def generate_ewr_site(self) -> None: - position = self.location_finder.location_for(LocationType.Ewr) - if position is None: - return - self.generate_ewr_at(position) - def generate_ewr_at(self, position: PointWithHeading) -> None: group_id = self.game.next_group_id() @@ -750,18 +484,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): return - def generate_required_missile_sites(self) -> None: - for position in self.control_point.preset_locations.required_missile_sites: - self.generate_missile_site_at(position) - def generate_missile_sites(self) -> None: - for i in range(self.faction.missiles_group_count): - self.generate_missile_site() - - def generate_missile_site(self) -> None: - position = self.location_finder.location_for(LocationType.MissileSite) - if position is not None: - return self.generate_missile_site_at(position) + for position in self.control_point.preset_locations.missile_sites: + self.generate_missile_site_at(position) def generate_missile_site_at(self, position: PointWithHeading) -> None: group_id = self.game.next_group_id() @@ -776,17 +501,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): self.control_point.connected_objectives.append(g) return - def generate_required_coastal_sites(self) -> None: - for position in self.control_point.preset_locations.required_coastal_defenses: - self.generate_coastal_site_at(position) - def generate_coastal_sites(self) -> None: - for i in range(self.faction.coastal_group_count): - self.generate_coastal_site() - - def generate_coastal_site(self) -> None: - position = self.location_finder.location_for(LocationType.Coastal) - if position is not None: + for position in self.control_point.preset_locations.coastal_defenses: self.generate_coastal_site_at(position) def generate_coastal_site_at(self, position: PointWithHeading) -> None: @@ -807,46 +523,39 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): return def generate_strike_targets(self) -> None: - """Generates the strike targets that are required by the campaign.""" building_set = list(set(self.faction.building_set) - {"oil"}) if not building_set: logging.error("Faction has no buildings defined") return - for position in self.control_point.preset_locations.required_strike_locations: + for position in self.control_point.preset_locations.strike_locations: category = random.choice(building_set) self.generate_strike_target_at(category, position) def generate_offshore_strike_targets(self) -> None: - """Generates the offshore strike targets that are required by the campaign.""" if "oil" not in self.faction.building_set: logging.error("Faction does not support offshore strike targets") return - for ( - position - ) in self.control_point.preset_locations.required_offshore_strike_locations: + for position in self.control_point.preset_locations.offshore_strike_locations: self.generate_strike_target_at("oil", position) class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): def generate(self) -> bool: self.generate_fob() - FobDefenseGenerator(self.game, self.control_point).generate() self.generate_armor_groups() self.generate_factories() self.generate_ammunition_depots() - self.generate_required_aa() - self.generate_required_ewr() + self.generate_aa() + self.generate_ewrs() self.generate_scenery_sites() self.generate_strike_targets() self.generate_offshore_strike_targets() if self.faction.missiles: self.generate_missile_sites() - self.generate_required_missile_sites() if self.faction.coastal_defenses: self.generate_coastal_sites() - self.generate_required_coastal_sites() return True diff --git a/game/version.py b/game/version.py index 564c43e3..1a22c118 100644 --- a/game/version.py +++ b/game/version.py @@ -75,10 +75,16 @@ VERSION = _build_version_string() #: * SPAAA_ZSU_23_4_Shilka_Gun_Dish, #: #: Version 5.0 -#: * Ammunition Depots objective locations are now predetermined using the "Ammunition Depot" -#: Warehouse object, and through trigger zone based scenery objects. -#: * The number of alive Ammunition Depot objective buildings connected to a control point -#: directly influences how many ground units can be supported on the front line. -#: * The number of supported ground units at any control point is artificially capped at 50, -#: even if the number of alive Ammunition Depot objectives can support more. -CAMPAIGN_FORMAT_VERSION = (5, 0) +#: * Ammunition Depots objective locations are now predetermined using the "Ammunition +# Depot" Warehouse object, and through trigger zone based scenery objects. +#: * The number of alive Ammunition Depot objective buildings connected to a control +#: point directly influences how many ground units can be supported on the front +#: line. +#: * The number of supported ground units at any control point is artificially +#: capped at 50, even if the number of alive Ammunition Depot objectives can +#: support more. +#: +#: Version 6.0 +#: * Random objective generation no is longer supported. Fixed objective locations were +#: added in 4.1. +CAMPAIGN_FORMAT_VERSION = (6, 0) diff --git a/resources/campaigns/battle_of_abu_dhabi.json b/resources/campaigns/battle_of_abu_dhabi.json index 505d19af..a551607b 100644 --- a/resources/campaigns/battle_of_abu_dhabi.json +++ b/resources/campaigns/battle_of_abu_dhabi.json @@ -7,5 +7,5 @@ "description": "
You have managed to establish a foothold at Khasab. Continue pushing south.
", "miz": "battle_of_abu_dhabi.miz", "performance": 2, - "version": "5.0" + "version": "6.0" } \ No newline at end of file diff --git a/resources/campaigns/inherent_resolve.json b/resources/campaigns/inherent_resolve.json index 110736b7..66f74d5e 100644 --- a/resources/campaigns/inherent_resolve.json +++ b/resources/campaigns/inherent_resolve.json @@ -5,7 +5,7 @@ "recommended_player_faction": "USA 2005", "recommended_enemy_faction": "Insurgents (Hard)", "description": "In this scenario, you start from Jordan, and have to fight your way through eastern Syria.
", - "version": "5.0", + "version": "6.0", "miz": "inherent_resolve.miz", "performance": 2 } \ No newline at end of file