diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 7ae6938b..a7ce062f 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -39,7 +39,7 @@ from dcs.unitgroup import ( StaticGroup, VehicleGroup, ) -from dcs.vehicles import AirDefence, Armor, MissilesSS +from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed from gen.flights.flight import FlightType from .controlpoint import ( @@ -49,6 +49,7 @@ from .controlpoint import ( Lha, MissionTarget, OffMapSpawn, + Fob, ) from .landmap import Landmap, load_landmap, poly_contains from ..utils import nm_to_meter @@ -87,6 +88,8 @@ class MizCampaignLoader: LHA_UNIT_TYPE = LHA_1_Tarawa.id FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id + FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id + EWR_UNIT_TYPE = AirDefence.EWR_55G6.id SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id @@ -177,6 +180,11 @@ class MizCampaignLoader: for group in self.country(blue).ship_group: if group.units[0].type == self.LHA_UNIT_TYPE: yield group + + def fobs(self, blue: bool) -> Iterator[VehicleGroup]: + for group in self.country(blue).vehicle_group: + if group.units[0].type == self.FOB_UNIT_TYPE: + yield group @property def ships(self) -> Iterator[ShipGroup]: @@ -261,6 +269,13 @@ class MizCampaignLoader: control_point.captured = blue control_point.captured_invert = group.late_activation control_points[control_point.id] = control_point + for group in self.fobs(blue): + control_point = Fob( + str(group.name), group.position, next(self.control_point_id) + ) + control_point.captured = blue + control_point.captured_invert = group.late_activation + control_points[control_point.id] = control_point return control_points @@ -279,14 +294,14 @@ class MizCampaignLoader: # final waypoint at the destination CP. Intermediate waypoints # define the curve of the front line. waypoints = [p.position for p in group.points] - origin = self.mission.terrain.nearest_airport(waypoints[0]) + origin = self.theater.closest_control_point(waypoints[0]) if origin is None: raise RuntimeError( - f"No airport near the first waypoint of {group.name}") - destination = self.mission.terrain.nearest_airport(waypoints[-1]) + f"No control point near the first waypoint of {group.name}") + destination = self.theater.closest_control_point(waypoints[-1]) if destination is None: raise RuntimeError( - f"No airport near the final waypoint of {group.name}") + f"No control point near the final waypoint of {group.name}") # Snap the begin and end points to the control points. waypoints[0] = origin.position diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 54759d3c..da1c7afb 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -623,3 +623,51 @@ class OffMapSpawn(ControlPoint): @property def runway_status(self) -> RunwayStatus: return RunwayStatus() + + +class Fob(ControlPoint): + + def __init__(self, name: str, at: Point, cp_id: int): + import game.theater.conflicttheater + super().__init__(cp_id, name, at, at, + game.theater.conflicttheater.SIZE_SMALL, 1, + has_frontline=True, cptype=ControlPointType.FOB) + self.name = name + + def runway_is_operational(self) -> bool: + return False + + def active_runway(self, conditions: Conditions, + dynamic_runways: Dict[str, RunwayData]) -> RunwayData: + logging.warning("TODO: FOBs have no runways.") + return RunwayData(self.full_name, runway_heading=0, runway_name="") + + @property + def runway_status(self) -> RunwayStatus: + return RunwayStatus() + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from gen.flights.flight import FlightType + if self.is_friendly(for_player): + yield from [ + FlightType.BARCAP, + # TODO: FlightType.LOGISTICS + ] + else: + yield from [ + FlightType.STRIKE, + FlightType.SWEEP, + FlightType.ESCORT, + FlightType.SEAD, + ] + + @property + def total_aircraft_parking(self) -> int: + return 0 + + def can_operate(self, aircraft: FlyingType) -> bool: + return False + + @property + def heading(self) -> int: + return 0 diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 123b1c87..8dbbda0a 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -45,6 +45,7 @@ from . import ( ControlPoint, ControlPointType, OffMapSpawn, + Fob, ) GroundObjectTemplates = Dict[str, Dict[str, Any]] @@ -537,7 +538,26 @@ class BaseDefenseGenerator: return g.groups.append(group) 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, game: Game, control_point: ControlPoint, @@ -679,6 +699,35 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): self.control_point.connected_objectives.append(g) return +class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): + def generate(self) -> bool: + self.generate_fob() + FobDefenseGenerator(self.game, self.control_point).generate() + return True + + def generate_fob(self) -> None: + try: + category = self.faction.building_set[self.faction.building_set.index('fob')] + except IndexError: + logging.exception("Faction has no fob buildings defined") + return + + obj_name = self.control_point.name + template = random.choice(list(self.templates[category].values())) + point = self.control_point.position + # Pick from preset locations + object_id = 0 + group_id = self.game.next_group_id() + + # TODO: Create only one TGO per objective, each with multiple units. + for unit in template: + object_id += 1 + + template_point = Point(unit["offset"].x, unit["offset"].y) + g = BuildingGroundObject( + obj_name, category, group_id, object_id, point + template_point, + unit["heading"], self.control_point, unit["type"], airbase_group=True) + self.control_point.connected_objectives.append(g) class GroundObjectGenerator: def __init__(self, game: Game) -> None: @@ -702,6 +751,9 @@ class GroundObjectGenerator: generator = LhaGroundObjectGenerator(self.game, control_point) elif isinstance(control_point, OffMapSpawn): generator = NoOpGroundObjectGenerator(self.game, control_point) + elif isinstance(control_point, Fob): + generator = FobGroundObjectGenerator(self.game, control_point, + self.templates) else: generator = AirbaseGroundObjectGenerator(self.game, control_point, self.templates) diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py index 7f3f44a9..be5c0a18 100644 --- a/game/theater/theatergroundobject.py +++ b/game/theater/theatergroundobject.py @@ -140,7 +140,7 @@ class TheaterGroundObject(MissionTarget): class BuildingGroundObject(TheaterGroundObject): def __init__(self, name: str, category: str, group_id: int, object_id: int, position: Point, heading: int, control_point: ControlPoint, - dcs_identifier: str) -> None: + dcs_identifier: str, airbase_group=False) -> None: super().__init__( name=name, category=category, @@ -149,7 +149,7 @@ class BuildingGroundObject(TheaterGroundObject): heading=heading, control_point=control_point, dcs_identifier=dcs_identifier, - airbase_group=False, + airbase_group=airbase_group, sea_object=False ) self.object_id = object_id