Merge branch 'develop' into helipads

# Conflicts:
#	game/theater/conflicttheater.py
#	gen/flights/flightplan.py
This commit is contained in:
Khopa
2021-06-06 15:46:30 +02:00
103 changed files with 1992 additions and 3914 deletions

View File

@@ -10,7 +10,6 @@ from dcs.vehicles import AirDefence, Armor
from game import db
from game.db import PRICES
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
STRENGTH_AA_ASSEMBLE_MIN = 0.2
PLANES_SCRAMBLE_MIN_BASE = 2
@@ -25,6 +24,7 @@ class Base:
def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {}
# TODO: Appears unused.
self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1
@@ -47,10 +47,6 @@ class Base:
logging.exception(f"No price found for {unit_type.id}")
return total
@property
def total_frontline_aa(self) -> int:
return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD])
@property
def total_aa(self) -> int:
return sum(self.aa.values())

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import itertools
import logging
import math
from dataclasses import dataclass
from functools import cached_property
@@ -40,10 +39,6 @@ from dcs.unitgroup import (
VehicleGroup,
)
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from .latlon import LatLon
from ..helipad import Helipad
from ..scenery_group import SceneryGroup
from pyproj import CRS, Transformer
from shapely import geometry, ops
@@ -58,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
@@ -88,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
@@ -131,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
@@ -211,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
@@ -330,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
@@ -441,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)
)
@@ -574,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)
)

View File

@@ -3,11 +3,11 @@ 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
from enum import Enum
from functools import total_ordering
from enum import Enum, unique, auto, IntEnum
from functools import total_ordering, cached_property
from typing import (
Any,
Dict,
@@ -33,24 +33,19 @@ from dcs.ships import (
)
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit
from dcs.unittype import FlyingType
from dcs.unittype import FlyingType, VehicleType
from game import db
from game.point_with_heading import PointWithHeading
from game.scenery_group import SceneryGroup
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
from gen.ground_forces.combat_stance import CombatStance
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 ..helipad import Helipad
@@ -60,6 +55,7 @@ from ..weather import Conditions
if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType
from ..transfers import PendingTransfers
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
@@ -79,149 +75,133 @@ 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:
present: int
ordered: int
transferring: int
class AircraftAllocations:
present: dict[Type[FlyingType], int]
ordered: dict[Type[FlyingType], int]
transferring: dict[Type[FlyingType], int]
@property
def total_value(self) -> int:
total: int = 0
for unit_type, count in self.present.items():
total += PRICES[unit_type] * count
for unit_type, count in self.ordered.items():
total += PRICES[unit_type] * count
for unit_type, count in self.transferring.items():
total += PRICES[unit_type] * count
return total
@property
def total(self) -> int:
return self.present + self.ordered + self.transferring
return self.total_present + self.total_ordered + self.total_transferring
@property
def total_present(self) -> int:
return sum(self.present.values())
@property
def total_ordered(self) -> int:
return sum(self.ordered.values())
@property
def total_transferring(self) -> int:
return sum(self.transferring.values())
@dataclass(frozen=True)
class GroundUnitAllocations:
present: dict[Type[VehicleType], int]
ordered: dict[Type[VehicleType], int]
transferring: dict[Type[VehicleType], int]
@property
def all(self) -> dict[Type[VehicleType], int]:
combined: dict[Type[VehicleType], int] = defaultdict(int)
for unit_type, count in itertools.chain(
self.present.items(), self.ordered.items(), self.transferring.items()
):
combined[unit_type] += count
return dict(combined)
@property
def total_value(self) -> int:
total: int = 0
for unit_type, count in self.present.items():
total += PRICES[unit_type] * count
for unit_type, count in self.ordered.items():
total += PRICES[unit_type] * count
for unit_type, count in self.transferring.items():
total += PRICES[unit_type] * count
return total
@cached_property
def total(self) -> int:
return self.total_present + self.total_ordered + self.total_transferring
@cached_property
def total_present(self) -> int:
return sum(self.present.values())
@cached_property
def total_ordered(self) -> int:
return sum(self.ordered.values())
@cached_property
def total_transferring(self) -> int:
return sum(self.transferring.values())
@dataclass
@@ -285,6 +265,13 @@ class GroundUnitDestination:
return self.total_value < other.total_value
@unique
class ControlPointStatus(IntEnum):
Functional = auto()
Damaged = auto()
Destroyed = auto()
class ControlPoint(MissionTarget, ABC):
position = None # type: Point
@@ -315,7 +302,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[Helipad] = []
@@ -344,7 +330,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 +539,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()
@@ -625,7 +593,7 @@ class ControlPoint(MissionTarget, ABC):
max_retreat_distance = nautical_miles(200)
# Skip the first airbase because that's the airbase we're retreating
# from.
airfields = list(closest.airfields_within(max_retreat_distance))[1:]
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
for airbase in airfields:
if not airbase.can_operate(airframe):
continue
@@ -655,11 +623,17 @@ class ControlPoint(MissionTarget, ABC):
airframe, count = self.base.aircraft.popitem()
self._retreat_air_units(game, airframe, count)
def depopulate_uncapturable_tgos(self) -> None:
for tgo in self.connected_objectives:
if not tgo.capturable:
tgo.clear()
# TODO: Should be Airbase specific.
def capture(self, game: Game, for_player: bool) -> None:
self.pending_unit_deliveries.refund_all(game)
self.retreat_ground_units(game)
self.retreat_air_units(game)
self.depopulate_uncapturable_tgos()
if for_player:
self.captured = True
@@ -668,46 +642,29 @@ 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:
...
def aircraft_transferring(self, game: Game) -> int:
def aircraft_transferring(self, game: Game) -> dict[Type[FlyingType], int]:
if self.captured:
ato = game.blue_ato
else:
ato = game.red_ato
total = 0
transferring: defaultdict[Type[FlyingType], int] = defaultdict(int)
for package in ato.packages:
for flight in package.flights:
if flight.departure == flight.arrival:
continue
if flight.departure == self:
total -= flight.count
transferring[flight.unit_type] -= flight.count
elif flight.arrival == self:
total += flight.count
return total
def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
on_order = 0
for unit_bought in self.pending_unit_deliveries.units:
if issubclass(unit_bought, FlyingType):
on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy(
self.base.total_aircraft, on_order, self.aircraft_transferring(game)
)
transferring[flight.unit_type] += flight.count
return transferring
def unclaimed_parking(self, game: Game) -> int:
return (
self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
)
return self.total_aircraft_parking - self.allocated_aircraft(game).total
@abstractmethod
def active_runway(
@@ -757,47 +714,34 @@ class ControlPoint(MissionTarget, ABC):
u.position.x = u.position.x + delta.x
u.position.y = u.position.y + delta.y
@property
def pending_frontline_aa_deliveries_count(self):
"""
Get number of pending frontline aa units
"""
if self.pending_unit_deliveries:
return sum(
[
v
for k, v in self.pending_unit_deliveries.units.items()
if k in TYPE_SHORAD
]
)
else:
return 0
@property
def pending_deliveries_count(self):
"""
Get number of pending units
"""
if self.pending_unit_deliveries:
return sum([v for k, v in self.pending_unit_deliveries.units.items()])
else:
return 0
@property
def expected_ground_units_next_turn(self) -> PendingOccupancy:
on_order = 0
for unit_bought in self.pending_unit_deliveries.units:
def allocated_aircraft(self, game: Game) -> AircraftAllocations:
on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items():
if issubclass(unit_bought, FlyingType):
continue
if unit_bought in TYPE_SHORAD:
continue
on_order += self.pending_unit_deliveries.units[unit_bought]
on_order[unit_bought] = count
return PendingOccupancy(
self.base.total_armor,
return AircraftAllocations(
self.base.aircraft, on_order, self.aircraft_transferring(game)
)
def allocated_ground_units(
self, transfers: PendingTransfers
) -> GroundUnitAllocations:
on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items():
if issubclass(unit_bought, VehicleType):
on_order[unit_bought] = count
transferring: dict[Type[VehicleType], int] = defaultdict(int)
for transfer in transfers:
if transfer.destination == self:
for unit_type, count in transfer.units.items():
transferring[unit_type] += count
return GroundUnitAllocations(
self.base.armor,
on_order,
# Ground unit transfers not yet implemented.
transferring=0,
transferring,
)
@property
@@ -816,18 +760,27 @@ class ControlPoint(MissionTarget, ABC):
@property
def frontline_unit_count_limit(self) -> int:
tally_connected_ammo_depots = 0
for cp_objective in self.connected_objectives:
if cp_objective.category == "ammo" and not cp_objective.is_dead:
tally_connected_ammo_depots += 1
return (
FREE_FRONTLINE_UNIT_SUPPLY
+ tally_connected_ammo_depots * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION
+ self.active_ammo_depots_count * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION
)
@property
def active_ammo_depots_count(self) -> int:
"""Return the number of available ammo depots"""
return len(
[
obj
for obj in self.connected_objectives
if obj.category == "ammo" and not obj.is_dead
]
)
@property
def total_ammo_depots_count(self) -> int:
"""Return the number of ammo depots, including dead ones"""
return len([obj for obj in self.connected_objectives if obj.category == "ammo"])
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return []
@@ -837,6 +790,11 @@ class ControlPoint(MissionTarget, ABC):
def category(self) -> str:
...
@property
@abstractmethod
def status(self) -> ControlPointStatus:
...
class Airfield(ControlPoint):
def __init__(
@@ -921,6 +879,15 @@ class Airfield(ControlPoint):
def category(self) -> str:
return "airfield"
@property
def status(self) -> ControlPointStatus:
runway_staus = self.runway_status
if runway_staus.needs_repair:
return ControlPointStatus.Destroyed
elif runway_staus.damaged:
return ControlPointStatus.Damaged
return ControlPointStatus.Functional
class NavalControlPoint(ControlPoint, ABC):
@property
@@ -945,20 +912,24 @@ class NavalControlPoint(ControlPoint, ABC):
def heading(self) -> int:
return 0 # TODO compute heading
def find_main_tgo(self) -> TheaterGroundObject:
for g in self.ground_objects:
if g.dcs_identifier in ["CARRIER", "LHA"]:
return g
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
def runway_is_operational(self) -> bool:
# Necessary because it's possible for the carrier itself to have sunk
# while its escorts are still alive.
for g in self.ground_objects:
if g.dcs_identifier in ["CARRIER", "LHA"]:
for group in g.groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
CV_1143_5_Admiral_Kuznetsov,
Type_071_Amphibious_Transport_Dock,
]:
return True
for group in self.find_main_tgo().groups:
for u in group.units:
if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
CV_1143_5_Admiral_Kuznetsov,
Type_071_Amphibious_Transport_Dock,
]:
return True
return False
def active_runway(
@@ -984,6 +955,14 @@ class NavalControlPoint(ControlPoint, ABC):
def can_deploy_ground_units(self) -> bool:
return False
@property
def status(self) -> ControlPointStatus:
if not self.runway_is_operational():
return ControlPointStatus.Destroyed
if self.find_main_tgo().dead_units:
return ControlPointStatus.Damaged
return ControlPointStatus.Functional
class Carrier(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int):
@@ -1113,6 +1092,10 @@ class OffMapSpawn(ControlPoint):
def category(self) -> str:
return "offmap"
@property
def status(self) -> ControlPointStatus:
return ControlPointStatus.Functional
class Fob(ControlPoint):
def __init__(self, name: str, at: Point, cp_id: int):
@@ -1176,3 +1159,7 @@ class Fob(ControlPoint):
@property
def category(self) -> str:
return "fob"
@property
def status(self) -> ControlPointStatus:
return ControlPointStatus.Functional

View File

@@ -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:
@@ -517,7 +307,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id,
position,
self.control_point,
for_airbase=False,
)
group = generate_armor_group(self.faction_name, self.game, g)
@@ -531,14 +320,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 +332,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 +341,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 +388,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 +405,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:
@@ -676,7 +415,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id,
position,
self.control_point,
for_airbase=False,
)
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
if not groups:
@@ -689,12 +427,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()
@@ -703,7 +435,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id,
position,
self.control_point,
for_airbase=False,
)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
@@ -750,18 +481,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 +498,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 +520,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
@@ -873,7 +579,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
unit["heading"],
self.control_point,
unit["type"],
airbase_group=True,
is_fob_structure=True,
)
self.control_point.connected_objectives.append(g)

View File

@@ -12,7 +12,6 @@ from dcs.unittype import VehicleType
from .. import db
from ..data.radar_db import (
UNITS_WITH_RADAR,
TRACK_RADARS,
TELARS,
LAUNCHER_TRACKER_PAIRS,
@@ -58,7 +57,6 @@ class TheaterGroundObject(MissionTarget):
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
airbase_group: bool,
sea_object: bool,
) -> None:
super().__init__(name, position)
@@ -67,7 +65,6 @@ class TheaterGroundObject(MissionTarget):
self.heading = heading
self.control_point = control_point
self.dcs_identifier = dcs_identifier
self.airbase_group = airbase_group
self.sea_object = sea_object
self.groups: List[Group] = []
@@ -193,6 +190,21 @@ class TheaterGroundObject(MissionTarget):
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return self.units
@property
def mark_locations(self) -> Iterator[Point]:
yield self.position
def clear(self) -> None:
self.groups = []
@property
def capturable(self) -> bool:
raise NotImplementedError
@property
def purchasable(self) -> bool:
raise NotImplementedError
class BuildingGroundObject(TheaterGroundObject):
def __init__(
@@ -205,7 +217,7 @@ class BuildingGroundObject(TheaterGroundObject):
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
airbase_group=False,
is_fob_structure=False,
) -> None:
super().__init__(
name=name,
@@ -215,9 +227,9 @@ class BuildingGroundObject(TheaterGroundObject):
heading=heading,
control_point=control_point,
dcs_identifier=dcs_identifier,
airbase_group=airbase_group,
sea_object=False,
)
self.is_fob_structure = is_fob_structure
self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but
# buildings don't have groups assigned to the TGO.
@@ -250,6 +262,23 @@ class BuildingGroundObject(TheaterGroundObject):
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return list(self.iter_building_group())
@property
def mark_locations(self) -> Iterator[Point]:
for building in self.iter_building_group():
yield building.position
@property
def is_control_point(self) -> bool:
return self.is_fob_structure
@property
def capturable(self) -> bool:
return True
@property
def purchasable(self) -> bool:
return False
class SceneryGroundObject(BuildingGroundObject):
def __init__(
@@ -272,7 +301,7 @@ class SceneryGroundObject(BuildingGroundObject):
heading=0,
control_point=control_point,
dcs_identifier=dcs_identifier,
airbase_group=False,
is_fob_structure=False,
)
self.zone = zone
try:
@@ -305,7 +334,7 @@ class FactoryGroundObject(BuildingGroundObject):
heading=heading,
control_point=control_point,
dcs_identifier="Workshop A",
airbase_group=False,
is_fob_structure=False,
)
@@ -321,6 +350,14 @@ class NavalGroundObject(TheaterGroundObject):
def might_have_aa(self) -> bool:
return True
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return False
class GenericCarrierGroundObject(NavalGroundObject):
@property
@@ -339,7 +376,6 @@ class CarrierGroundObject(GenericCarrierGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="CARRIER",
airbase_group=True,
sea_object=True,
)
@@ -361,7 +397,6 @@ class LhaGroundObject(GenericCarrierGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="LHA",
airbase_group=True,
sea_object=True,
)
@@ -384,10 +419,17 @@ class MissileSiteGroundObject(TheaterGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return False
class CoastalSiteGroundObject(TheaterGroundObject):
def __init__(
@@ -406,26 +448,28 @@ class CoastalSiteGroundObject(TheaterGroundObject):
heading=heading,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
class BaseDefenseGroundObject(TheaterGroundObject):
"""Base type for all base defenses."""
@property
def purchasable(self) -> bool:
return False
# TODO: Differentiate types.
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# be split into their own types.
class SamGroundObject(BaseDefenseGroundObject):
class SamGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__(
name=name,
@@ -435,7 +479,6 @@ class SamGroundObject(BaseDefenseGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False,
)
# Set by the SAM unit generator if the generated group is compatible
@@ -495,15 +538,22 @@ class SamGroundObject(BaseDefenseGroundObject):
else:
return max(max_tel_range, max_telar_range, max_non_radar)
@property
def capturable(self) -> bool:
return False
class VehicleGroupGroundObject(BaseDefenseGroundObject):
@property
def purchasable(self) -> bool:
return True
class VehicleGroupGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__(
name=name,
@@ -513,19 +563,25 @@ class VehicleGroupGroundObject(BaseDefenseGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
class EwrGroundObject(BaseDefenseGroundObject):
@property
def purchasable(self) -> bool:
return True
class EwrGroundObject(TheaterGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__(
name=name,
@@ -535,7 +591,6 @@ class EwrGroundObject(BaseDefenseGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="EWR",
airbase_group=for_airbase,
sea_object=False,
)
@@ -555,6 +610,14 @@ class EwrGroundObject(BaseDefenseGroundObject):
def might_have_aa(self) -> bool:
return True
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return True
class ShipGroundObject(NavalGroundObject):
def __init__(
@@ -568,7 +631,6 @@ class ShipGroundObject(NavalGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=True,
)