From 8345063e84292470732929b9f65984eb399c8bf3 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 17 Nov 2020 18:11:33 -0800 Subject: [PATCH] Move theater into game. --- .github/workflows/build.yml | 5 - .github/workflows/release.yml | 5 - game/game.py | 7 +- game/theater/__init__.py | 5 + game/theater/base.py | 192 +++++++ game/theater/conflicttheater.py | 515 +++++++++++++++++ game/theater/controlpoint.py | 262 +++++++++ game/theater/frontline.py | 1 + {theater => game/theater}/landmap.py | 0 {theater => game/theater}/missiontarget.py | 0 {theater => game/theater}/start_generator.py | 4 +- game/theater/theatergroundobject.py | 336 ++++++++++++ gen/aircraft.py | 2 +- gen/ato.py | 2 +- gen/briefinggen.py | 11 +- gen/fleet/cn_dd_group.py | 2 +- gen/fleet/dd_group.py | 2 +- gen/fleet/ru_dd_group.py | 2 +- gen/flights/ai_flight_planner.py | 2 +- gen/flights/flight.py | 2 +- gen/flights/flightplan.py | 2 +- gen/groundobjectsgen.py | 2 +- gen/sam/genericsam_group_generator.py | 2 +- gen/sam/group_generator.py | 4 +- gen/sam/sam_group_generator.py | 15 +- qt_ui/dialogs.py | 2 +- qt_ui/models.py | 2 +- qt_ui/uiconstants.py | 3 +- .../widgets/combos/QOriginAirfieldSelector.py | 2 +- qt_ui/widgets/map/QLiberationMap.py | 4 +- qt_ui/widgets/map/QMapObject.py | 2 +- qt_ui/windows/mission/QPackageDialog.py | 2 +- qt_ui/windows/mission/QPlannedFlightsView.py | 2 +- qt_ui/windows/newgame/QNewGameWizard.py | 2 +- theater/__init__.py | 7 +- theater/base.py | 194 +------ theater/conflicttheater.py | 517 +----------------- theater/controlpoint.py | 264 +-------- theater/frontline.py | 5 +- theater/theatergroundobject.py | 338 +----------- theater/weatherforecast.py | 4 - 41 files changed, 1363 insertions(+), 1369 deletions(-) create mode 100644 game/theater/__init__.py create mode 100644 game/theater/base.py create mode 100644 game/theater/conflicttheater.py create mode 100644 game/theater/controlpoint.py create mode 100644 game/theater/frontline.py rename {theater => game/theater}/landmap.py (100%) rename {theater => game/theater}/missiontarget.py (100%) rename {theater => game/theater}/start_generator.py (99%) create mode 100644 game/theater/theatergroundobject.py delete mode 100644 theater/weatherforecast.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60cbf719..0ac6fc4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,11 +36,6 @@ jobs: run: | ./venv/scripts/activate mypy gen - - - name: mypy theater - run: | - ./venv/scripts/activate - mypy theater - name: update build number run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca8a238e..f8346069 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -43,11 +43,6 @@ jobs: ./venv/scripts/activate mypy gen - - name: mypy theater - run: | - ./venv/scripts/activate - mypy theater - - name: Build binaries run: | ./venv/scripts/activate diff --git a/game/game.py b/game/game.py index a78ec5e4..d69f9cc8 100644 --- a/game/game.py +++ b/game/game.py @@ -1,5 +1,4 @@ import logging -import math import random import sys from datetime import date, datetime, timedelta @@ -7,8 +6,7 @@ from typing import Dict, List from dcs.action import Coalition from dcs.mapping import Point -from dcs.task import CAP, CAS, PinpointStrike, Task -from dcs.unittype import UnitType +from dcs.task import CAP, CAS, PinpointStrike from dcs.vehicles import AirDefence from game import db @@ -21,8 +19,6 @@ from gen.conflictgen import Conflict from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.flights.closestairfields import ObjectiveDistanceCache from gen.ground_forces.ai_ground_planner import GroundPlanner -from theater import ConflictTheater, ControlPoint -from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW from . import persistency from .debriefing import Debriefing from .event.event import Event, UnitsDeliveryEvent @@ -30,6 +26,7 @@ from .event.frontlineattack import FrontlineAttackEvent from .factions.faction import Faction from .infos.information import Information from .settings import Settings +from .theater import ConflictTheater, ControlPoint from .weather import Conditions, TimeOfDay COMMISION_UNIT_VARIETY = 4 diff --git a/game/theater/__init__.py b/game/theater/__init__.py new file mode 100644 index 00000000..c5b83a16 --- /dev/null +++ b/game/theater/__init__.py @@ -0,0 +1,5 @@ +from .base import * +from .conflicttheater import * +from .controlpoint import * +from .missiontarget import MissionTarget +from .theatergroundobject import SamGroundObject diff --git a/game/theater/base.py b/game/theater/base.py new file mode 100644 index 00000000..47b3580e --- /dev/null +++ b/game/theater/base.py @@ -0,0 +1,192 @@ +import itertools +import logging +import math +import typing +from typing import Dict, Type + +from dcs.planes import PlaneType +from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task +from dcs.unittype import UnitType, VehicleType +from dcs.vehicles import AirDefence, Armor + +from game import db + +STRENGTH_AA_ASSEMBLE_MIN = 0.2 +PLANES_SCRAMBLE_MIN_BASE = 2 +PLANES_SCRAMBLE_MAX_BASE = 8 +PLANES_SCRAMBLE_FACTOR = 0.3 + +BASE_MAX_STRENGTH = 1 +BASE_MIN_STRENGTH = 0 + + +class Base: + aircraft = {} # type: typing.Dict[PlaneType, int] + armor = {} # type: typing.Dict[VehicleType, int] + aa = {} # type: typing.Dict[AirDefence, int] + strength = 1 # type: float + + def __init__(self): + self.aircraft = {} + self.armor = {} + self.aa = {} + self.commision_points: Dict[Type, float] = {} + self.strength = 1 + + @property + def total_planes(self) -> int: + return sum(self.aircraft.values()) + + @property + def total_armor(self) -> int: + return sum(self.armor.values()) + + @property + def total_aa(self) -> int: + return sum(self.aa.values()) + + def total_units(self, task: Task) -> int: + return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]]) + + def total_units_of_type(self, unit_type) -> int: + return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type]) + + @property + def all_units(self): + return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) + + def _find_best_unit(self, available_units: Dict[UnitType, int], + for_type: Task, count: int) -> Dict[UnitType, int]: + if count <= 0: + logging.warning("{}: no units for {}".format(self, for_type)) + return {} + + sorted_units = [key for key in available_units if + key in db.UNIT_BY_TASK[for_type]] + sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True) + + result: Dict[UnitType, int] = {} + for unit_type in sorted_units: + existing_count = available_units[unit_type] # type: int + if not existing_count: + continue + + if count <= 0: + break + + result_unit_count = min(count, existing_count) + count -= result_unit_count + + assert result_unit_count > 0 + result[unit_type] = result.get(unit_type, 0) + result_unit_count + + logging.info("{} for {} ({}): {}".format(self, for_type, count, result)) + return result + + def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]: + return self._find_best_unit(self.aircraft, for_type, count) + + def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]: + return self._find_best_unit(self.armor, for_type, count) + + def append_commision_points(self, for_type, points: float) -> int: + self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points + points = self.commision_points[for_type] + if points >= 1: + self.commision_points[for_type] = points - math.floor(points) + return int(math.floor(points)) + + return 0 + + def filter_units(self, applicable_units: typing.Collection): + self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units} + self.armor = {k: v for k, v in self.armor.items() if k in applicable_units} + + def commision_units(self, units: typing.Dict[typing.Any, int]): + for value in units.values(): + assert value > 0 + assert value == math.floor(value) + + for unit_type, unit_count in units.items(): + for_task = db.unit_task(unit_type) + + target_dict = None + if for_task == CAS or for_task == CAP or for_task == Embarking: + target_dict = self.aircraft + elif for_task == PinpointStrike: + target_dict = self.armor + elif for_task == AirDefence: + target_dict = self.aa + + assert target_dict is not None + target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count + + def commit_losses(self, units_lost: typing.Dict[typing.Any, int]): + + for unit_type, count in units_lost.items(): + + if unit_type in self.aircraft: + target_array = self.aircraft + elif unit_type in self.armor: + target_array = self.armor + else: + print("Base didn't find event type {}".format(unit_type)) + continue + + if unit_type not in target_array: + print("Base didn't find event type {}".format(unit_type)) + continue + + target_array[unit_type] = max(target_array[unit_type] - count, 0) + if target_array[unit_type] == 0: + del target_array[unit_type] + + def affect_strength(self, amount): + self.strength += amount + if self.strength > BASE_MAX_STRENGTH: + self.strength = BASE_MAX_STRENGTH + elif self.strength <= 0: + self.strength = BASE_MIN_STRENGTH + + def set_strength_to_minimum(self) -> None: + self.strength = BASE_MIN_STRENGTH + + def scramble_count(self, multiplier: float, task: Task = None) -> int: + if task: + count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task]) + else: + count = self.total_planes + + count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength)) + return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count) + + def assemble_count(self): + return int(self.total_armor * 0.5) + + def assemble_aa_count(self) -> int: + # previous logic removed because we always want the full air defense capabilities. + return self.total_aa + + def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]: + return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) + + def scramble_last_defense(self): + # return as many CAP-capable aircraft as we can since this is the last defense of the base + # (but not more than 20 - that's just nuts) + return self._find_best_planes(CAP, min(self.total_planes, 20)) + + def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]: + return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS)) + + def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]: + return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) + + def assemble_attack(self) -> typing.Dict[Armor, int]: + return self._find_best_armor(PinpointStrike, self.assemble_count()) + + def assemble_defense(self) -> typing.Dict[Armor, int]: + count = int(self.total_armor * min(self.strength + 0.5, 1)) + return self._find_best_armor(PinpointStrike, count) + + def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]: + return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count()) diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py new file mode 100644 index 00000000..c0b373ce --- /dev/null +++ b/game/theater/conflicttheater.py @@ -0,0 +1,515 @@ +from __future__ import annotations + +import logging +import json +from dataclasses import dataclass +from itertools import tee +from pathlib import Path +from typing import Any, Dict, Iterator, List, Optional, Tuple, Union + +from dcs.mapping import Point +from dcs.terrain import ( + caucasus, + nevada, + normandy, + persiangulf, + syria, + thechannel, +) +from dcs.terrain.terrain import Terrain + +from gen.flights.flight import FlightType +from .controlpoint import ControlPoint, MissionTarget +from .landmap import Landmap, load_landmap, poly_contains + +Numeric = Union[int, float] + +SIZE_TINY = 150 +SIZE_SMALL = 600 +SIZE_REGULAR = 1000 +SIZE_BIG = 2000 +SIZE_LARGE = 3000 + +IMPORTANCE_LOW = 1 +IMPORTANCE_MEDIUM = 1.2 +IMPORTANCE_HIGH = 1.4 + +""" +ALL_RADIALS = [0, 45, 90, 135, 180, 225, 270, 315, ] +COAST_NS_E = [45, 90, 135, ] +COAST_EW_N = [315, 0, 45, ] +COAST_NSEW_E = [225, 270, 315, ] +COAST_NSEW_W = [45, 90, 135, ] + +COAST_NS_W = [225, 270, 315, ] +COAST_EW_S = [135, 180, 225, ] +""" + +LAND = [0, 45, 90, 135, 180, 225, 270, 315, ] + +COAST_V_E = [0, 45, 90, 135, 180] +COAST_V_W = [180, 225, 270, 315, 0] + +COAST_A_W = [315, 0, 45, 135, 180, 225, 270] +COAST_A_E = [0, 45, 90, 135, 180, 225, 315] + +COAST_H_N = [270, 315, 0, 45, 90] +COAST_H_S = [90, 135, 180, 225, 270] + +COAST_DL_E = [45, 90, 135, 180, 225] +COAST_DL_W = [225, 270, 315, 0, 45] +COAST_DR_E = [315, 0, 45, 90, 135] +COAST_DR_W = [135, 180, 225, 315] + +FRONTLINE_MIN_CP_DISTANCE = 5000 + +def pairwise(iterable): + """ + itertools recipe + s -> (s0,s1), (s1,s2), (s2, s3), ... + """ + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +class ConflictTheater: + terrain: Terrain + + reference_points: Dict[Tuple[float, float], Tuple[float, float]] + overview_image: str + landmap: Optional[Landmap] + """ + land_poly = None # type: Polygon + """ + daytime_map: Dict[str, Tuple[int, int]] + frontline_data: Optional[Dict[str, ComplexFrontLine]] = None + + def __init__(self): + self.controlpoints: List[ControlPoint] = [] + self.frontline_data = FrontLine.load_json_frontlines(self) + """ + self.land_poly = geometry.Polygon(self.landmap[0][0]) + for x in self.landmap[1]: + self.land_poly = self.land_poly.difference(geometry.Polygon(x)) + """ + + def add_controlpoint(self, point: ControlPoint, + connected_to: Optional[List[ControlPoint]] = None): + if connected_to is None: + connected_to = [] + for connected_point in connected_to: + point.connect(to=connected_point) + + self.controlpoints.append(point) + + def find_ground_objects_by_obj_name(self, obj_name): + found = [] + for cp in self.controlpoints: + for g in cp.ground_objects: + if g.obj_name == obj_name: + found.append(g) + return found + + def is_in_sea(self, point: Point) -> bool: + if not self.landmap: + return False + + if self.is_on_land(point): + return False + + for sea in self.landmap[2]: + if poly_contains(point.x, point.y, sea): + return True + + return False + + def is_on_land(self, point: Point) -> bool: + if not self.landmap: + return True + + is_point_included = False + for inclusion_zone in self.landmap[0]: + if poly_contains(point.x, point.y, inclusion_zone): + is_point_included = True + + if not is_point_included: + return False + + for exclusion_zone in self.landmap[1]: + if poly_contains(point.x, point.y, exclusion_zone): + return False + + return True + + def player_points(self) -> List[ControlPoint]: + return [point for point in self.controlpoints if point.captured] + + def conflicts(self, from_player=True) -> Iterator[FrontLine]: + for cp in [x for x in self.controlpoints if x.captured == from_player]: + for connected_point in [x for x in cp.connected_points if x.captured != from_player]: + yield FrontLine(cp, connected_point, self) + + def enemy_points(self) -> List[ControlPoint]: + return [point for point in self.controlpoints if not point.captured] + + def add_json_cp(self, theater, p: dict) -> ControlPoint: + + if p["type"] == "airbase": + + airbase = theater.terrain.airports[p["id"]].__class__ + + if "radials" in p.keys(): + radials = p["radials"] + else: + radials = LAND + + if "size" in p.keys(): + size = p["size"] + else: + size = SIZE_REGULAR + + if "importance" in p.keys(): + importance = p["importance"] + else: + importance = IMPORTANCE_MEDIUM + + cp = ControlPoint.from_airport(airbase, radials, size, importance) + elif p["type"] == "carrier": + cp = ControlPoint.carrier("carrier", Point(p["x"], p["y"]), p["id"]) + else: + cp = ControlPoint.lha("lha", Point(p["x"], p["y"]), p["id"]) + + if "captured_invert" in p.keys(): + cp.captured_invert = p["captured_invert"] + else: + cp.captured_invert = False + + return cp + + @staticmethod + def from_json(data: Dict[str, Any]) -> ConflictTheater: + theaters = { + "Caucasus": CaucasusTheater, + "Nevada": NevadaTheater, + "Persian Gulf": PersianGulfTheater, + "Normandy": NormandyTheater, + "The Channel": TheChannelTheater, + "Syria": SyriaTheater, + } + theater = theaters[data["theater"]] + t = theater() + cps = {} + for p in data["player_points"]: + cp = t.add_json_cp(theater, p) + cp.captured = True + cps[p["id"]] = cp + t.add_controlpoint(cp) + + for p in data["enemy_points"]: + cp = t.add_json_cp(theater, p) + cps[p["id"]] = cp + t.add_controlpoint(cp) + + for l in data["links"]: + cps[l[0]].connect(cps[l[1]]) + cps[l[1]].connect(cps[l[0]]) + + return t + + +class CaucasusTheater(ConflictTheater): + terrain = caucasus.Caucasus() + overview_image = "caumap.gif" + reference_points = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4), + (-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), } + + landmap = load_landmap("resources\\caulandmap.p") + daytime_map = { + "dawn": (6, 9), + "day": (9, 18), + "dusk": (18, 20), + "night": (0, 5), + } + + +class PersianGulfTheater(ConflictTheater): + terrain = persiangulf.PersianGulf() + overview_image = "persiangulf.gif" + reference_points = { + (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( + 772, -1970), + (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } + landmap = load_landmap("resources\\gulflandmap.p") + daytime_map = { + "dawn": (6, 8), + "day": (8, 16), + "dusk": (16, 18), + "night": (0, 5), + } + +class NevadaTheater(ConflictTheater): + terrain = nevada.Nevada() + overview_image = "nevada.gif" + reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45 * 2, -360 * 2), + (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440 * 2, 80 * 2), } + landmap = load_landmap("resources\\nevlandmap.p") + daytime_map = { + "dawn": (4, 6), + "day": (6, 17), + "dusk": (17, 18), + "night": (0, 5), + } + +class NormandyTheater(ConflictTheater): + terrain = normandy.Normandy() + overview_image = "normandy.gif" + reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000), + (normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)} + landmap = load_landmap("resources\\normandylandmap.p") + daytime_map = { + "dawn": (6, 8), + "day": (10, 17), + "dusk": (17, 18), + "night": (0, 5), + } + +class TheChannelTheater(ConflictTheater): + terrain = thechannel.TheChannel() + overview_image = "thechannel.gif" + reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100), + (thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)} + landmap = load_landmap("resources\\channellandmap.p") + daytime_map = { + "dawn": (6, 8), + "day": (10, 17), + "dusk": (17, 18), + "night": (0, 5), + } + +class SyriaTheater(ConflictTheater): + terrain = syria.Syria() + overview_image = "syria.gif" + reference_points = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380), + (syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)} + landmap = load_landmap("resources\\syrialandmap.p") + daytime_map = { + "dawn": (6, 8), + "day": (8, 16), + "dusk": (16, 18), + "night": (0, 5), + } + +@dataclass +class ComplexFrontLine: + """ + Stores data necessary for building a multi-segment frontline. + "points" should be ordered from closest to farthest distance originating from start_cp.position + """ + + start_cp: ControlPoint + points: List[Point] + + +@dataclass +class FrontLineSegment: + """ + Describes a line segment of a FrontLine + """ + + point_a: Point + point_b: Point + + @property + def attack_heading(self) -> Numeric: + """The heading of the frontline segment from player to enemy control point""" + return self.point_a.heading_between_point(self.point_b) + + @property + def attack_distance(self) -> Numeric: + """Length of the segment""" + return self.point_a.distance_to_point(self.point_b) + + +class FrontLine(MissionTarget): + """Defines a front line location between two control points. + Front lines are the area where ground combat happens. + Overwrites the entirety of MissionTarget __init__ method to allow for + dynamic position calculation. + """ + + def __init__( + self, + control_point_a: ControlPoint, + control_point_b: ControlPoint, + theater: ConflictTheater + ) -> None: + self.control_point_a = control_point_a + self.control_point_b = control_point_b + self.segments: List[FrontLineSegment] = [] + self.theater = theater + self._build_segments() + self.name = f"Front line {control_point_a}/{control_point_b}" + + def is_friendly(self, to_player: bool) -> bool: + """Returns True if the objective is in friendly territory.""" + return False + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + yield from [ + FlightType.CAS, + # TODO: FlightType.TROOP_TRANSPORT + # TODO: FlightType.EVAC + ] + yield from super().mission_types(for_player) + + @property + def position(self): + """ + The position where the conflict should occur + according to the current strength of each control point. + """ + return self.point_from_a(self._position_distance) + + @property + def control_points(self) -> Tuple[ControlPoint, ControlPoint]: + """Returns a tuple of the two control points.""" + return self.control_point_a, self.control_point_b + + @property + def middle_point(self): + self.point_from_a(self.attack_distance / 2) + + @property + def attack_distance(self): + """The total distance of all segments""" + return sum(i.attack_distance for i in self.segments) + + @property + def attack_heading(self): + """The heading of the active attack segment from player to enemy control point""" + return self.active_segment.attack_heading + + @property + def active_segment(self) -> FrontLineSegment: + """The FrontLine segment where there can be an active conflict""" + if self._position_distance <= self.segments[0].attack_distance: + return self.segments[0] + + remaining_dist = self._position_distance + for segment in self.segments: + if remaining_dist <= segment.attack_distance: + return segment + else: + remaining_dist -= segment.attack_distance + logging.error( + "Frontline attack distance is greater than the sum of its segments" + ) + return self.segments[0] + + def point_from_a(self, distance: Numeric) -> Point: + """ + Returns a point {distance} away from control_point_a along the frontline segments. + """ + if distance < self.segments[0].attack_distance: + return self.control_point_a.position.point_from_heading( + self.segments[0].attack_heading, distance + ) + remaining_dist = distance + for segment in self.segments: + if remaining_dist < segment.attack_distance: + return segment.point_a.point_from_heading( + segment.attack_heading, remaining_dist + ) + else: + remaining_dist -= segment.attack_distance + + @property + def _position_distance(self) -> float: + """ + The distance from point "a" where the conflict should occur + according to the current strength of each control point + """ + total_strength = ( + self.control_point_a.base.strength + self.control_point_b.base.strength + ) + if self.control_point_a.base.strength == 0: + return self._adjust_for_min_dist(0) + if self.control_point_b.base.strength == 0: + return self._adjust_for_min_dist(self.attack_distance) + strength_pct = self.control_point_a.base.strength / total_strength + return self._adjust_for_min_dist(strength_pct * self.attack_distance) + + def _adjust_for_min_dist(self, distance: Numeric) -> Numeric: + """ + Ensures the frontline conflict is never located within the minimum distance + constant of either end control point. + """ + if (distance > self.attack_distance / 2) and ( + distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance + ): + distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE + elif (distance < self.attack_distance / 2) and ( + distance < FRONTLINE_MIN_CP_DISTANCE + ): + distance = FRONTLINE_MIN_CP_DISTANCE + return distance + + def _build_segments(self) -> None: + """Create line segments for the frontline""" + control_point_ids = "|".join( + [str(self.control_point_a.id), str(self.control_point_b.id)] + ) # from_cp.id|to_cp.id + reversed_cp_ids = "|".join( + [str(self.control_point_b.id), str(self.control_point_a.id)] + ) + complex_frontlines = self.theater.frontline_data + if (complex_frontlines) and ( + (control_point_ids in complex_frontlines) + or (reversed_cp_ids in complex_frontlines) + ): + # The frontline segments must be stored in the correct order for the distance algorithms to work. + # The points in the frontline are ordered from the id before the | to the id after. + # First, check if control point id pair matches in order, and create segments if a match is found. + if control_point_ids in complex_frontlines: + point_pairs = pairwise(complex_frontlines[control_point_ids].points) + for i in point_pairs: + self.segments.append(FrontLineSegment(i[0], i[1])) + # Check the reverse order and build in reverse if found. + elif reversed_cp_ids in complex_frontlines: + point_pairs = pairwise( + reversed(complex_frontlines[reversed_cp_ids].points) + ) + for i in point_pairs: + self.segments.append(FrontLineSegment(i[0], i[1])) + # If no complex frontline has been configured, fall back to the old straight line method. + else: + self.segments.append( + FrontLineSegment( + self.control_point_a.position, self.control_point_b.position + ) + ) + + + @staticmethod + def load_json_frontlines( + theater: ConflictTheater + ) -> Optional[Dict[str, ComplexFrontLine]]: + """Load complex frontlines from json""" + try: + path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json") + with open(path, "r") as file: + logging.debug(f"Loading frontline from {path}...") + data = json.load(file) + return { + frontline: ComplexFrontLine( + data[frontline]["start_cp"], + [Point(i[0], i[1]) for i in data[frontline]["points"]], + ) + for frontline in data + } + except OSError: + logging.warning( + f"Unable to load preset frontlines for {theater.terrain.name}" + ) + return None diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py new file mode 100644 index 00000000..46ac7e00 --- /dev/null +++ b/game/theater/controlpoint.py @@ -0,0 +1,262 @@ +from __future__ import annotations + +import itertools +import re +from enum import Enum +from typing import Dict, Iterator, List, TYPE_CHECKING + +from dcs.mapping import Point +from dcs.ships import ( + CVN_74_John_C__Stennis, + CV_1143_5_Admiral_Kuznetsov, + LHA_1_Tarawa, + Type_071_Amphibious_Transport_Dock, +) +from dcs.terrain.terrain import Airport + +from game import db +from gen.ground_forces.combat_stance import CombatStance +from .base import Base +from .missiontarget import MissionTarget +from .theatergroundobject import ( + BaseDefenseGroundObject, + TheaterGroundObject, +) + +if TYPE_CHECKING: + from game import Game + from gen.flights.flight import FlightType + + +class ControlPointType(Enum): + AIRBASE = 0 # An airbase with slots for everything + AIRCRAFT_CARRIER_GROUP = 1 # A group with a Stennis type carrier (F/A-18, F-14 compatible) + LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier) + FARP = 4 # A FARP, with slots for helicopters + FOB = 5 # A FOB (ground units only) + + +class ControlPoint(MissionTarget): + + position = None # type: Point + name = None # type: str + + captured = False + has_frontline = True + frontline_offset = 0.0 + + alt = 0 + + def __init__(self, id: int, name: str, position: Point, + at: db.StartingPosition, radials: List[int], size: int, + importance: float, has_frontline=True, + cptype=ControlPointType.AIRBASE): + super().__init__(" ".join(re.split(r" |-", name)[:2]), position) + self.id = id + self.full_name = name + self.at = at + self.connected_objectives: List[TheaterGroundObject] = [] + self.base_defenses: List[BaseDefenseGroundObject] = [] + + self.size = size + self.importance = importance + self.captured = False + self.captured_invert = False + self.has_frontline = has_frontline + self.radials = radials + self.connected_points: List[ControlPoint] = [] + self.base: Base = Base() + self.cptype = cptype + self.stances: Dict[int, CombatStance] = {} + self.airport = None + + @property + def ground_objects(self) -> List[TheaterGroundObject]: + return list( + itertools.chain(self.connected_objectives, self.base_defenses)) + + @classmethod + def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True): + assert airport + obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE) + obj.airport = airport() + return obj + + @classmethod + def carrier(cls, name: str, at: Point, id: int): + import game.theater.conflicttheater + cp = cls(id, name, at, at, game.theater.conflicttheater.LAND, game.theater.conflicttheater.SIZE_SMALL, 1, + has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP) + return cp + + @classmethod + def lha(cls, name: str, at: Point, id: int): + import game.theater.conflicttheater + cp = cls(id, name, at, at, game.theater.conflicttheater.LAND, game.theater.conflicttheater.SIZE_SMALL, 1, + has_frontline=False, cptype=ControlPointType.LHA_GROUP) + return cp + + @property + def heading(self): + if self.cptype == ControlPointType.AIRBASE: + return self.airport.runways[0].heading + elif self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]: + return 0 # TODO compute heading + else: + return 0 + + def __str__(self): + return self.name + + @property + def is_global(self): + return not self.connected_points + + @property + def is_carrier(self): + """ + :return: Whether this control point is an aircraft carrier + """ + return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] + + @property + def is_fleet(self): + """ + :return: Whether this control point is a boat (mobile) + """ + return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] + + @property + def is_lha(self): + """ + :return: Whether this control point is an LHA + """ + return self.cptype in [ControlPointType.LHA_GROUP] + + @property + def sea_radials(self) -> List[int]: + # TODO: fix imports + all_radials = [0, 45, 90, 135, 180, 225, 270, 315, ] + result = [] + for r in all_radials: + if r not in self.radials: + result.append(r) + return result + + @property + def available_aircraft_slots(self): + """ + :return: The maximum number of aircraft that can be stored in this control point + """ + if self.cptype == ControlPointType.AIRBASE: + return len(self.airport.parking_slots) + elif self.is_lha: + return 20 + elif self.is_carrier: + return 90 + else: + return 0 + + def connect(self, to): + self.connected_points.append(to) + self.stances[to.id] = CombatStance.DEFENSIVE + + def has_runway(self): + """ + Check whether this control point can have aircraft taking off or landing. + :return: + """ + if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] : + 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 + return False + elif self.cptype in [ControlPointType.AIRBASE, ControlPointType.FARP]: + return True + else: + return True + + def get_carrier_group_name(self): + """ + Get the carrier group name if the airbase is a carrier + :return: Carrier group name + """ + if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] : + for g in self.ground_objects: + if g.dcs_identifier == "CARRIER": + for group in g.groups: + for u in group.units: + if db.unit_type_from_name(u.type) in [CVN_74_John_C__Stennis, CV_1143_5_Admiral_Kuznetsov]: + return group.name + elif g.dcs_identifier == "LHA": + for group in g.groups: + for u in group.units: + if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]: + return group.name + return None + + def is_connected(self, to) -> bool: + return to in self.connected_points + + def find_radial(self, heading: int, ignored_radial: int = None) -> int: + closest_radial = 0 + closest_radial_delta = 360 + for radial in [x for x in self.radials if x != ignored_radial]: + delta = abs(radial - heading) + if delta < closest_radial_delta: + closest_radial = radial + closest_radial_delta = delta + + return closest_radial + + def find_ground_objects_by_obj_name(self, obj_name): + found = [] + for g in self.ground_objects: + if g.obj_name == obj_name: + found.append(g) + return found + + def is_friendly(self, to_player: bool) -> bool: + return self.captured == to_player + + def capture(self, game: Game, for_player: bool) -> None: + if for_player: + self.captured = True + else: + self.captured = False + + self.base.set_strength_to_minimum() + + self.base.aircraft = {} + self.base.armor = {} + + # Handle cyclic dependency. + from .start_generator import BaseDefenseGenerator + self.base_defenses = [] + BaseDefenseGenerator(game, self).generate() + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + yield from super().mission_types(for_player) + if self.is_friendly(for_player): + if self.is_fleet: + yield from [ + # TODO: FlightType.INTERCEPTION + # TODO: Buddy tanking for the A-4? + # TODO: Rescue chopper? + # TODO: Inter-ship logistics? + ] + else: + yield from [ + # TODO: FlightType.INTERCEPTION + # TODO: FlightType.LOGISTICS + ] + else: + if self.is_fleet: + yield FlightType.ANTISHIP + else: + yield from [ + # TODO: FlightType.STRIKE + ] diff --git a/game/theater/frontline.py b/game/theater/frontline.py new file mode 100644 index 00000000..3b57f9b6 --- /dev/null +++ b/game/theater/frontline.py @@ -0,0 +1 @@ +"""Only here to keep compatibility for save games generated in version 2.2.0""" diff --git a/theater/landmap.py b/game/theater/landmap.py similarity index 100% rename from theater/landmap.py rename to game/theater/landmap.py diff --git a/theater/missiontarget.py b/game/theater/missiontarget.py similarity index 100% rename from theater/missiontarget.py rename to game/theater/missiontarget.py diff --git a/theater/start_generator.py b/game/theater/start_generator.py similarity index 99% rename from theater/start_generator.py rename to game/theater/start_generator.py index 816aabb4..95bc1c69 100644 --- a/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -34,8 +34,8 @@ from theater import ( ControlPointType, TheaterGroundObject, ) -from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW -from theater.theatergroundobject import ( +from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW +from game.theater.theatergroundobject import ( EwrGroundObject, SamGroundObject, BuildingGroundObject, diff --git a/game/theater/theatergroundobject.py b/game/theater/theatergroundobject.py new file mode 100644 index 00000000..2a829de2 --- /dev/null +++ b/game/theater/theatergroundobject.py @@ -0,0 +1,336 @@ +from __future__ import annotations + +import itertools +from typing import Iterator, List, TYPE_CHECKING + +from dcs.mapping import Point +from dcs.unit import Unit +from dcs.unitgroup import Group + +if TYPE_CHECKING: + from .controlpoint import ControlPoint + from gen.flights.flight import FlightType + +from .missiontarget import MissionTarget + +NAME_BY_CATEGORY = { + "power": "Power plant", + "ammo": "Ammo depot", + "fuel": "Fuel depot", + "aa": "AA Defense Site", + "ware": "Warehouse", + "farp": "FARP", + "fob": "FOB", + "factory": "Factory", + "comms": "Comms. tower", + "oil": "Oil platform", + "derrick": "Derrick", + "ww2bunker": "Bunker", + "village": "Village", + "allycamp": "Camp", + "EWR":"EWR", +} + +ABBREV_NAME = { + "power": "PLANT", + "ammo": "AMMO", + "fuel": "FUEL", + "aa": "AA", + "ware": "WARE", + "farp": "FARP", + "fob": "FOB", + "factory": "FACTORY", + "comms": "COMMST", + "oil": "OILP", + "derrick": "DERK", + "ww2bunker": "BUNK", + "village": "VLG", + "allycamp": "CMP", +} + +CATEGORY_MAP = { + + # Special cases + "CARRIER": ["CARRIER"], + "LHA": ["LHA"], + "aa": ["AA"], + + # Buildings + "power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"], + "ware": ["Warehouse", "Hangar A"], + "fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"], + "ammo": [".Ammunition depot", "Hangar B"], + "farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"], + "fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"], + "factory": ["Tech combine", "Tech hangar A"], + "comms": ["TV tower", "Comms tower M"], + "oil": ["Oil platform"], + "derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"], + "ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"], + "village": ["Small house 1B", "Small House 1A", "Small warehouse 1"], + "allycamp": [], +} + + +class TheaterGroundObject(MissionTarget): + + def __init__(self, name: str, category: str, group_id: int, position: Point, + heading: int, control_point: ControlPoint, dcs_identifier: str, + airbase_group: bool, sea_object: bool) -> None: + super().__init__(name, position) + self.category = category + self.group_id = group_id + self.heading = heading + self.control_point = control_point + self.dcs_identifier = dcs_identifier + self.airbase_group = airbase_group + self.sea_object = sea_object + self.is_dead = False + # TODO: There is never more than one group. + self.groups: List[Group] = [] + + @property + def units(self) -> List[Unit]: + """ + :return: all the units at this location + """ + return list(itertools.chain.from_iterable([g.units for g in self.groups])) + + @property + def group_name(self) -> str: + """The name of the unit group.""" + return f"{self.category}|{self.group_id}" + + def __str__(self) -> str: + return NAME_BY_CATEGORY[self.category] + + def is_same_group(self, identifier: str) -> bool: + return self.group_id == identifier + + @property + def obj_name(self) -> str: + return self.name + + @property + def faction_color(self) -> str: + return "BLUE" if self.control_point.captured else "RED" + + def is_friendly(self, to_player: bool) -> bool: + return self.control_point.is_friendly(to_player) + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from gen.flights.flight import FlightType + if self.is_friendly(for_player): + yield from [ + # TODO: FlightType.LOGISTICS + # TODO: FlightType.TROOP_TRANSPORT + ] + else: + yield from [ + FlightType.STRIKE, + FlightType.BAI, + ] + yield from super().mission_types(for_player) + + +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: + super().__init__( + name=name, + category=category, + group_id=group_id, + position=position, + heading=heading, + control_point=control_point, + dcs_identifier=dcs_identifier, + airbase_group=False, + sea_object=False + ) + self.object_id = object_id + + @property + def group_name(self) -> str: + """The name of the unit group.""" + return f"{self.category}|{self.group_id}|{self.object_id}" + + +class NavalGroundObject(TheaterGroundObject): + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from gen.flights.flight import FlightType + if not self.is_friendly(for_player): + yield FlightType.ANTISHIP + yield from super().mission_types(for_player) + + +class GenericCarrierGroundObject(NavalGroundObject): + pass + + +# TODO: Why is this both a CP and a TGO? +class CarrierGroundObject(GenericCarrierGroundObject): + def __init__(self, name: str, group_id: int, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="CARRIER", + group_id=group_id, + position=control_point.position, + heading=0, + control_point=control_point, + dcs_identifier="CARRIER", + airbase_group=True, + sea_object=True + ) + + @property + def group_name(self) -> str: + # Prefix the group names with the side color so Skynet can find them, + # add to EWR. + return f"{self.faction_color}|EWR|{super().group_name}" + + +# TODO: Why is this both a CP and a TGO? +class LhaGroundObject(GenericCarrierGroundObject): + def __init__(self, name: str, group_id: int, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="LHA", + group_id=group_id, + position=control_point.position, + heading=0, + control_point=control_point, + dcs_identifier="LHA", + airbase_group=True, + sea_object=True + ) + + @property + def group_name(self) -> str: + # Prefix the group names with the side color so Skynet can find them, + # add to EWR. + return f"{self.faction_color}|EWR|{super().group_name}" + + +class MissileSiteGroundObject(TheaterGroundObject): + def __init__(self, name: str, group_id: int, position: Point, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="aa", + group_id=group_id, + position=position, + heading=0, + control_point=control_point, + dcs_identifier="AA", + airbase_group=False, + sea_object=False + ) + + +class BaseDefenseGroundObject(TheaterGroundObject): + """Base type for all base defenses.""" + + +# TODO: Differentiate types. +# This type gets used both for AA sites (SAM, AAA, or SHORAD) but also for the +# armor garrisons at airbases. These should each be split into their own types. +class SamGroundObject(BaseDefenseGroundObject): + def __init__(self, name: str, group_id: int, position: Point, + control_point: ControlPoint, for_airbase: bool) -> None: + super().__init__( + name=name, + category="aa", + group_id=group_id, + position=position, + 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 + # with Skynet. + self.skynet_capable = False + + @property + def group_name(self) -> str: + if self.skynet_capable: + # Prefix the group names of SAM sites with the side color so Skynet + # can find them. + return f"{self.faction_color}|SAM|{self.group_id}" + else: + return super().group_name + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from gen.flights.flight import FlightType + if not self.is_friendly(for_player): + yield FlightType.DEAD + yield from super().mission_types(for_player) + + +class VehicleGroupGroundObject(BaseDefenseGroundObject): + def __init__(self, name: str, group_id: int, position: Point, + control_point: ControlPoint, for_airbase: bool) -> None: + super().__init__( + name=name, + category="aa", + group_id=group_id, + position=position, + heading=0, + control_point=control_point, + dcs_identifier="AA", + airbase_group=for_airbase, + sea_object=False + ) + + +class EwrGroundObject(BaseDefenseGroundObject): + def __init__(self, name: str, group_id: int, position: Point, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="EWR", + group_id=group_id, + position=position, + heading=0, + control_point=control_point, + dcs_identifier="EWR", + airbase_group=True, + sea_object=False + ) + + @property + def group_name(self) -> str: + # Prefix the group names with the side color so Skynet can find them. + return f"{self.faction_color}|{super().group_name}" + + def mission_types(self, for_player: bool) -> Iterator[FlightType]: + from gen.flights.flight import FlightType + if not self.is_friendly(for_player): + yield FlightType.DEAD + yield from super().mission_types(for_player) + + +class ShipGroundObject(NavalGroundObject): + def __init__(self, name: str, group_id: int, position: Point, + control_point: ControlPoint) -> None: + super().__init__( + name=name, + category="aa", + group_id=group_id, + position=position, + heading=0, + control_point=control_point, + dcs_identifier="AA", + airbase_group=False, + sea_object=True + ) + + @property + def group_name(self) -> str: + # Prefix the group names with the side color so Skynet can find them, + # add to EWR. + return f"{self.faction_color}|EWR|{super().group_name}" diff --git a/gen/aircraft.py b/gen/aircraft.py index 151162ae..798091a5 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -84,7 +84,7 @@ from gen.flights.flight import ( from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.runways import RunwayData from theater import TheaterGroundObject -from theater.controlpoint import ControlPoint, ControlPointType +from game.theater.controlpoint import ControlPoint, ControlPointType from .conflictgen import Conflict from .flights.flightplan import ( CasFlightPlan, diff --git a/gen/ato.py b/gen/ato.py index a21563dc..7927423b 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -16,7 +16,7 @@ from typing import Dict, List, Optional from dcs.mapping import Point -from theater.missiontarget import MissionTarget +from game.theater.missiontarget import MissionTarget from .flights.flight import Flight, FlightType from .flights.flightplan import FormationFlightPlan diff --git a/gen/briefinggen.py b/gen/briefinggen.py index b35a587d..55028635 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -2,19 +2,18 @@ Briefing generation logic """ from __future__ import annotations + import os -import random -import logging from dataclasses import dataclass -from theater import FrontLine -from typing import List, Dict, TYPE_CHECKING -from jinja2 import Environment, FileSystemLoader, select_autoescape +from typing import Dict, List, TYPE_CHECKING from dcs.mission import Mission +from jinja2 import Environment, FileSystemLoader, select_autoescape + +from game.theater import ControlPoint, FrontLine from .aircraft import FlightData from .airsupportgen import AwacsInfo, TankerInfo from .armor import JtacInfo -from theater import ControlPoint from .ground_forces.combat_stance import CombatStance from .radios import RadioFrequency from .runways import RunwayData diff --git a/gen/fleet/cn_dd_group.py b/gen/fleet/cn_dd_group.py index 020f68c2..1485ea6c 100644 --- a/gen/fleet/cn_dd_group.py +++ b/gen/fleet/cn_dd_group.py @@ -14,7 +14,7 @@ from dcs.ships import ( from game.factions.faction import Faction from gen.fleet.dd_group import DDGroupGenerator from gen.sam.group_generator import ShipGroupGenerator -from theater.theatergroundobject import TheaterGroundObject +from game.theater.theatergroundobject import TheaterGroundObject if TYPE_CHECKING: from game.game import Game diff --git a/gen/fleet/dd_group.py b/gen/fleet/dd_group.py index b11de653..c6a3e115 100644 --- a/gen/fleet/dd_group.py +++ b/gen/fleet/dd_group.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import TYPE_CHECKING from game.factions.faction import Faction -from theater.theatergroundobject import TheaterGroundObject +from game.theater.theatergroundobject import TheaterGroundObject from gen.sam.group_generator import ShipGroupGenerator from dcs.unittype import ShipType diff --git a/gen/fleet/ru_dd_group.py b/gen/fleet/ru_dd_group.py index 0948991a..b08acdf4 100644 --- a/gen/fleet/ru_dd_group.py +++ b/gen/fleet/ru_dd_group.py @@ -16,7 +16,7 @@ from dcs.ships import ( from gen.fleet.dd_group import DDGroupGenerator from gen.sam.group_generator import ShipGroupGenerator from game.factions.faction import Faction -from theater.theatergroundobject import TheaterGroundObject +from game.theater.theatergroundobject import TheaterGroundObject if TYPE_CHECKING: diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 27e7b5dd..ee458908 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -55,7 +55,7 @@ from theater import ( ) # Avoid importing some types that cause circular imports unless type checking. -from theater.theatergroundobject import ( +from game.theater.theatergroundobject import ( EwrGroundObject, NavalGroundObject, VehicleGroupGroundObject, ) diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 4ddc4003..2b5e35ea 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -9,7 +9,7 @@ from dcs.point import MovingPoint, PointAction from dcs.unittype import FlyingType from game import db -from theater.controlpoint import ControlPoint, MissionTarget +from game.theater.controlpoint import ControlPoint, MissionTarget if TYPE_CHECKING: from gen.ato import Package diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 49d4b9f3..3b5c1e40 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -27,7 +27,7 @@ from theater import ( SamGroundObject, TheaterGroundObject, ) -from theater.theatergroundobject import EwrGroundObject +from game.theater.theatergroundobject import EwrGroundObject from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .traveltime import GroundSpeed, TravelTime diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 78cf29d7..edd58e6d 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -28,7 +28,7 @@ from game import db from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID from game.db import unit_type_from_name from theater import ControlPoint, TheaterGroundObject -from theater.theatergroundobject import ( +from game.theater.theatergroundobject import ( BuildingGroundObject, CarrierGroundObject, GenericCarrierGroundObject, LhaGroundObject, ShipGroundObject, diff --git a/gen/sam/genericsam_group_generator.py b/gen/sam/genericsam_group_generator.py index 8a35e51b..d7c2cdf1 100644 --- a/gen/sam/genericsam_group_generator.py +++ b/gen/sam/genericsam_group_generator.py @@ -2,7 +2,7 @@ from abc import ABC from game import Game from gen.sam.group_generator import GroupGenerator -from theater.theatergroundobject import SamGroundObject +from game.theater.theatergroundobject import SamGroundObject class GenericSamGroupGenerator(GroupGenerator, ABC): diff --git a/gen/sam/group_generator.py b/gen/sam/group_generator.py index 94738eef..cb879692 100644 --- a/gen/sam/group_generator.py +++ b/gen/sam/group_generator.py @@ -1,7 +1,7 @@ from __future__ import annotations import math import random -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from dcs import unitgroup from dcs.point import PointAction @@ -9,7 +9,7 @@ from dcs.unit import Vehicle, Ship from dcs.unittype import VehicleType from game.factions.faction import Faction -from theater.theatergroundobject import TheaterGroundObject +from game.theater.theatergroundobject import TheaterGroundObject if TYPE_CHECKING: from game.game import Game diff --git a/gen/sam/sam_group_generator.py b/gen/sam/sam_group_generator.py index 1ff77cde..b5855750 100644 --- a/gen/sam/sam_group_generator.py +++ b/gen/sam/sam_group_generator.py @@ -1,18 +1,21 @@ import random from typing import List, Optional, Type -from dcs.vehicles import AirDefence from dcs.unitgroup import VehicleGroup +from dcs.vehicles import AirDefence from game import Game, db +from game.theater import TheaterGroundObject +from game.theater.theatergroundobject import SamGroundObject from gen.sam.aaa_bofors import BoforsGenerator from gen.sam.aaa_flak import FlakGenerator from gen.sam.aaa_flak18 import Flak18Generator from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator -from gen.sam.cold_war_flak import EarlyColdWarFlakGenerator, ColdWarFlakGenerator - - +from gen.sam.cold_war_flak import ( + ColdWarFlakGenerator, + EarlyColdWarFlakGenerator, +) from gen.sam.ewrs import ( BigBirdGenerator, BoxSpringGenerator, @@ -25,6 +28,7 @@ from gen.sam.ewrs import ( StraightFlushGenerator, TallRackGenerator, ) +from gen.sam.freya_ewr import FreyaGenerator from gen.sam.group_generator import GroupGenerator from gen.sam.sam_avenger import AvengerGenerator from gen.sam.sam_chaparral import ChaparralGenerator @@ -50,9 +54,6 @@ from gen.sam.sam_zsu23 import ZSU23Generator from gen.sam.sam_zu23 import ZU23Generator from gen.sam.sam_zu23_ural import ZU23UralGenerator from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator -from gen.sam.freya_ewr import FreyaGenerator -from theater import TheaterGroundObject -from theater.theatergroundobject import SamGroundObject SAM_MAP = { "HawkGenerator": HawkGenerator, diff --git a/qt_ui/dialogs.py b/qt_ui/dialogs.py index 36ca6890..263bfb62 100644 --- a/qt_ui/dialogs.py +++ b/qt_ui/dialogs.py @@ -2,7 +2,7 @@ from typing import Optional from gen.flights.flight import Flight -from theater.missiontarget import MissionTarget +from game.theater.missiontarget import MissionTarget from .models import GameModel, PackageModel from .windows.mission.QEditFlightDialog import QEditFlightDialog from .windows.mission.QPackageDialog import ( diff --git a/qt_ui/models.py b/qt_ui/models.py index 07b990d6..ed5086e0 100644 --- a/qt_ui/models.py +++ b/qt_ui/models.py @@ -16,7 +16,7 @@ from gen.ato import AirTaskingOrder, Package from gen.flights.flight import Flight from gen.flights.traveltime import TotEstimator from qt_ui.uiconstants import AIRCRAFT_ICONS -from theater.missiontarget import MissionTarget +from game.theater.missiontarget import MissionTarget class DeletableChildModelManager: diff --git a/qt_ui/uiconstants.py b/qt_ui/uiconstants.py index b256705d..24fde874 100644 --- a/qt_ui/uiconstants.py +++ b/qt_ui/uiconstants.py @@ -1,10 +1,9 @@ import os from typing import Dict -from pathlib import Path from PySide2.QtGui import QColor, QFont, QPixmap -from theater.theatergroundobject import CATEGORY_MAP +from game.theater.theatergroundobject import CATEGORY_MAP from .liberation_theme import get_theme_icons diff --git a/qt_ui/widgets/combos/QOriginAirfieldSelector.py b/qt_ui/widgets/combos/QOriginAirfieldSelector.py index b0530efc..35df76d4 100644 --- a/qt_ui/widgets/combos/QOriginAirfieldSelector.py +++ b/qt_ui/widgets/combos/QOriginAirfieldSelector.py @@ -5,7 +5,7 @@ from PySide2.QtWidgets import QComboBox from dcs.planes import PlaneType from game.inventory import GlobalAircraftInventory -from theater.controlpoint import ControlPoint +from game.theater.controlpoint import ControlPoint class QOriginAirfieldSelector(QComboBox): diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 04e2d5d6..fb5802c3 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -40,8 +40,8 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from theater import ControlPoint -from theater.conflicttheater import FrontLine -from theater.theatergroundobject import ( +from game.theater.conflicttheater import FrontLine +from game.theater.theatergroundobject import ( EwrGroundObject, MissileSiteGroundObject, TheaterGroundObject, diff --git a/qt_ui/widgets/map/QMapObject.py b/qt_ui/widgets/map/QMapObject.py index fa28c333..a3c57c19 100644 --- a/qt_ui/widgets/map/QMapObject.py +++ b/qt_ui/widgets/map/QMapObject.py @@ -13,7 +13,7 @@ from PySide2.QtWidgets import ( from qt_ui.dialogs import Dialog from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog -from theater.missiontarget import MissionTarget +from game.theater.missiontarget import MissionTarget class QMapObject(QGraphicsRectItem): diff --git a/qt_ui/windows/mission/QPackageDialog.py b/qt_ui/windows/mission/QPackageDialog.py index 30a9caf0..2cee9ba6 100644 --- a/qt_ui/windows/mission/QPackageDialog.py +++ b/qt_ui/windows/mission/QPackageDialog.py @@ -24,7 +24,7 @@ from qt_ui.uiconstants import EVENT_ICONS from qt_ui.widgets.ato import QFlightList from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator -from theater.missiontarget import MissionTarget +from game.theater.missiontarget import MissionTarget class QPackageDialog(QDialog): diff --git a/qt_ui/windows/mission/QPlannedFlightsView.py b/qt_ui/windows/mission/QPlannedFlightsView.py index 2c602d56..1ca6e845 100644 --- a/qt_ui/windows/mission/QPlannedFlightsView.py +++ b/qt_ui/windows/mission/QPlannedFlightsView.py @@ -4,7 +4,7 @@ from PySide2.QtWidgets import QAbstractItemView, QListView from qt_ui.models import GameModel from qt_ui.windows.mission.QFlightItem import QFlightItem -from theater.controlpoint import ControlPoint +from game.theater.controlpoint import ControlPoint class QPlannedFlightsView(QListView): diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index e82357dc..eaaad876 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -15,7 +15,7 @@ from qt_ui.windows.newgame.QCampaignList import ( QCampaignList, load_campaigns, ) -from theater.start_generator import GameGenerator +from game.theater.start_generator import GameGenerator jinja_env = Environment( loader=FileSystemLoader("resources/ui/templates"), diff --git a/theater/__init__.py b/theater/__init__.py index c5b83a16..f6b256d8 100644 --- a/theater/__init__.py +++ b/theater/__init__.py @@ -1,5 +1,2 @@ -from .base import * -from .conflicttheater import * -from .controlpoint import * -from .missiontarget import MissionTarget -from .theatergroundobject import SamGroundObject +# For save game compatibility. Remove before 2.3. +from game.theater import * diff --git a/theater/base.py b/theater/base.py index 47b3580e..fc28c91b 100644 --- a/theater/base.py +++ b/theater/base.py @@ -1,192 +1,2 @@ -import itertools -import logging -import math -import typing -from typing import Dict, Type - -from dcs.planes import PlaneType -from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task -from dcs.unittype import UnitType, VehicleType -from dcs.vehicles import AirDefence, Armor - -from game import db - -STRENGTH_AA_ASSEMBLE_MIN = 0.2 -PLANES_SCRAMBLE_MIN_BASE = 2 -PLANES_SCRAMBLE_MAX_BASE = 8 -PLANES_SCRAMBLE_FACTOR = 0.3 - -BASE_MAX_STRENGTH = 1 -BASE_MIN_STRENGTH = 0 - - -class Base: - aircraft = {} # type: typing.Dict[PlaneType, int] - armor = {} # type: typing.Dict[VehicleType, int] - aa = {} # type: typing.Dict[AirDefence, int] - strength = 1 # type: float - - def __init__(self): - self.aircraft = {} - self.armor = {} - self.aa = {} - self.commision_points: Dict[Type, float] = {} - self.strength = 1 - - @property - def total_planes(self) -> int: - return sum(self.aircraft.values()) - - @property - def total_armor(self) -> int: - return sum(self.armor.values()) - - @property - def total_aa(self) -> int: - return sum(self.aa.values()) - - def total_units(self, task: Task) -> int: - return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t in db.UNIT_BY_TASK[task]]) - - def total_units_of_type(self, unit_type) -> int: - return sum([c for t, c in itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) if t == unit_type]) - - @property - def all_units(self): - return itertools.chain(self.aircraft.items(), self.armor.items(), self.aa.items()) - - def _find_best_unit(self, available_units: Dict[UnitType, int], - for_type: Task, count: int) -> Dict[UnitType, int]: - if count <= 0: - logging.warning("{}: no units for {}".format(self, for_type)) - return {} - - sorted_units = [key for key in available_units if - key in db.UNIT_BY_TASK[for_type]] - sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True) - - result: Dict[UnitType, int] = {} - for unit_type in sorted_units: - existing_count = available_units[unit_type] # type: int - if not existing_count: - continue - - if count <= 0: - break - - result_unit_count = min(count, existing_count) - count -= result_unit_count - - assert result_unit_count > 0 - result[unit_type] = result.get(unit_type, 0) + result_unit_count - - logging.info("{} for {} ({}): {}".format(self, for_type, count, result)) - return result - - def _find_best_planes(self, for_type: Task, count: int) -> typing.Dict[PlaneType, int]: - return self._find_best_unit(self.aircraft, for_type, count) - - def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]: - return self._find_best_unit(self.armor, for_type, count) - - def append_commision_points(self, for_type, points: float) -> int: - self.commision_points[for_type] = self.commision_points.get(for_type, 0) + points - points = self.commision_points[for_type] - if points >= 1: - self.commision_points[for_type] = points - math.floor(points) - return int(math.floor(points)) - - return 0 - - def filter_units(self, applicable_units: typing.Collection): - self.aircraft = {k: v for k, v in self.aircraft.items() if k in applicable_units} - self.armor = {k: v for k, v in self.armor.items() if k in applicable_units} - - def commision_units(self, units: typing.Dict[typing.Any, int]): - for value in units.values(): - assert value > 0 - assert value == math.floor(value) - - for unit_type, unit_count in units.items(): - for_task = db.unit_task(unit_type) - - target_dict = None - if for_task == CAS or for_task == CAP or for_task == Embarking: - target_dict = self.aircraft - elif for_task == PinpointStrike: - target_dict = self.armor - elif for_task == AirDefence: - target_dict = self.aa - - assert target_dict is not None - target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count - - def commit_losses(self, units_lost: typing.Dict[typing.Any, int]): - - for unit_type, count in units_lost.items(): - - if unit_type in self.aircraft: - target_array = self.aircraft - elif unit_type in self.armor: - target_array = self.armor - else: - print("Base didn't find event type {}".format(unit_type)) - continue - - if unit_type not in target_array: - print("Base didn't find event type {}".format(unit_type)) - continue - - target_array[unit_type] = max(target_array[unit_type] - count, 0) - if target_array[unit_type] == 0: - del target_array[unit_type] - - def affect_strength(self, amount): - self.strength += amount - if self.strength > BASE_MAX_STRENGTH: - self.strength = BASE_MAX_STRENGTH - elif self.strength <= 0: - self.strength = BASE_MIN_STRENGTH - - def set_strength_to_minimum(self) -> None: - self.strength = BASE_MIN_STRENGTH - - def scramble_count(self, multiplier: float, task: Task = None) -> int: - if task: - count = sum([v for k, v in self.aircraft.items() if db.unit_task(k) == task]) - else: - count = self.total_planes - - count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength)) - return min(min(max(count, PLANES_SCRAMBLE_MIN_BASE), int(PLANES_SCRAMBLE_MAX_BASE * multiplier)), count) - - def assemble_count(self): - return int(self.total_armor * 0.5) - - def assemble_aa_count(self) -> int: - # previous logic removed because we always want the full air defense capabilities. - return self.total_aa - - def scramble_sweep(self, multiplier: float) -> typing.Dict[PlaneType, int]: - return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) - - def scramble_last_defense(self): - # return as many CAP-capable aircraft as we can since this is the last defense of the base - # (but not more than 20 - that's just nuts) - return self._find_best_planes(CAP, min(self.total_planes, 20)) - - def scramble_cas(self, multiplier: float) -> typing.Dict[PlaneType, int]: - return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS)) - - def scramble_interceptors(self, multiplier: float) -> typing.Dict[PlaneType, int]: - return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP)) - - def assemble_attack(self) -> typing.Dict[Armor, int]: - return self._find_best_armor(PinpointStrike, self.assemble_count()) - - def assemble_defense(self) -> typing.Dict[Armor, int]: - count = int(self.total_armor * min(self.strength + 0.5, 1)) - return self._find_best_armor(PinpointStrike, count) - - def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]: - return self._find_best_unit(self.aa, AirDefence, count and min(count, self.total_aa) or self.assemble_aa_count()) +# For save compat. Remove in 2.3. +from game.theater.base import * diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index c0b373ce..e1566178 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,515 +1,2 @@ -from __future__ import annotations - -import logging -import json -from dataclasses import dataclass -from itertools import tee -from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional, Tuple, Union - -from dcs.mapping import Point -from dcs.terrain import ( - caucasus, - nevada, - normandy, - persiangulf, - syria, - thechannel, -) -from dcs.terrain.terrain import Terrain - -from gen.flights.flight import FlightType -from .controlpoint import ControlPoint, MissionTarget -from .landmap import Landmap, load_landmap, poly_contains - -Numeric = Union[int, float] - -SIZE_TINY = 150 -SIZE_SMALL = 600 -SIZE_REGULAR = 1000 -SIZE_BIG = 2000 -SIZE_LARGE = 3000 - -IMPORTANCE_LOW = 1 -IMPORTANCE_MEDIUM = 1.2 -IMPORTANCE_HIGH = 1.4 - -""" -ALL_RADIALS = [0, 45, 90, 135, 180, 225, 270, 315, ] -COAST_NS_E = [45, 90, 135, ] -COAST_EW_N = [315, 0, 45, ] -COAST_NSEW_E = [225, 270, 315, ] -COAST_NSEW_W = [45, 90, 135, ] - -COAST_NS_W = [225, 270, 315, ] -COAST_EW_S = [135, 180, 225, ] -""" - -LAND = [0, 45, 90, 135, 180, 225, 270, 315, ] - -COAST_V_E = [0, 45, 90, 135, 180] -COAST_V_W = [180, 225, 270, 315, 0] - -COAST_A_W = [315, 0, 45, 135, 180, 225, 270] -COAST_A_E = [0, 45, 90, 135, 180, 225, 315] - -COAST_H_N = [270, 315, 0, 45, 90] -COAST_H_S = [90, 135, 180, 225, 270] - -COAST_DL_E = [45, 90, 135, 180, 225] -COAST_DL_W = [225, 270, 315, 0, 45] -COAST_DR_E = [315, 0, 45, 90, 135] -COAST_DR_W = [135, 180, 225, 315] - -FRONTLINE_MIN_CP_DISTANCE = 5000 - -def pairwise(iterable): - """ - itertools recipe - s -> (s0,s1), (s1,s2), (s2, s3), ... - """ - a, b = tee(iterable) - next(b, None) - return zip(a, b) - - -class ConflictTheater: - terrain: Terrain - - reference_points: Dict[Tuple[float, float], Tuple[float, float]] - overview_image: str - landmap: Optional[Landmap] - """ - land_poly = None # type: Polygon - """ - daytime_map: Dict[str, Tuple[int, int]] - frontline_data: Optional[Dict[str, ComplexFrontLine]] = None - - def __init__(self): - self.controlpoints: List[ControlPoint] = [] - self.frontline_data = FrontLine.load_json_frontlines(self) - """ - self.land_poly = geometry.Polygon(self.landmap[0][0]) - for x in self.landmap[1]: - self.land_poly = self.land_poly.difference(geometry.Polygon(x)) - """ - - def add_controlpoint(self, point: ControlPoint, - connected_to: Optional[List[ControlPoint]] = None): - if connected_to is None: - connected_to = [] - for connected_point in connected_to: - point.connect(to=connected_point) - - self.controlpoints.append(point) - - def find_ground_objects_by_obj_name(self, obj_name): - found = [] - for cp in self.controlpoints: - for g in cp.ground_objects: - if g.obj_name == obj_name: - found.append(g) - return found - - def is_in_sea(self, point: Point) -> bool: - if not self.landmap: - return False - - if self.is_on_land(point): - return False - - for sea in self.landmap[2]: - if poly_contains(point.x, point.y, sea): - return True - - return False - - def is_on_land(self, point: Point) -> bool: - if not self.landmap: - return True - - is_point_included = False - for inclusion_zone in self.landmap[0]: - if poly_contains(point.x, point.y, inclusion_zone): - is_point_included = True - - if not is_point_included: - return False - - for exclusion_zone in self.landmap[1]: - if poly_contains(point.x, point.y, exclusion_zone): - return False - - return True - - def player_points(self) -> List[ControlPoint]: - return [point for point in self.controlpoints if point.captured] - - def conflicts(self, from_player=True) -> Iterator[FrontLine]: - for cp in [x for x in self.controlpoints if x.captured == from_player]: - for connected_point in [x for x in cp.connected_points if x.captured != from_player]: - yield FrontLine(cp, connected_point, self) - - def enemy_points(self) -> List[ControlPoint]: - return [point for point in self.controlpoints if not point.captured] - - def add_json_cp(self, theater, p: dict) -> ControlPoint: - - if p["type"] == "airbase": - - airbase = theater.terrain.airports[p["id"]].__class__ - - if "radials" in p.keys(): - radials = p["radials"] - else: - radials = LAND - - if "size" in p.keys(): - size = p["size"] - else: - size = SIZE_REGULAR - - if "importance" in p.keys(): - importance = p["importance"] - else: - importance = IMPORTANCE_MEDIUM - - cp = ControlPoint.from_airport(airbase, radials, size, importance) - elif p["type"] == "carrier": - cp = ControlPoint.carrier("carrier", Point(p["x"], p["y"]), p["id"]) - else: - cp = ControlPoint.lha("lha", Point(p["x"], p["y"]), p["id"]) - - if "captured_invert" in p.keys(): - cp.captured_invert = p["captured_invert"] - else: - cp.captured_invert = False - - return cp - - @staticmethod - def from_json(data: Dict[str, Any]) -> ConflictTheater: - theaters = { - "Caucasus": CaucasusTheater, - "Nevada": NevadaTheater, - "Persian Gulf": PersianGulfTheater, - "Normandy": NormandyTheater, - "The Channel": TheChannelTheater, - "Syria": SyriaTheater, - } - theater = theaters[data["theater"]] - t = theater() - cps = {} - for p in data["player_points"]: - cp = t.add_json_cp(theater, p) - cp.captured = True - cps[p["id"]] = cp - t.add_controlpoint(cp) - - for p in data["enemy_points"]: - cp = t.add_json_cp(theater, p) - cps[p["id"]] = cp - t.add_controlpoint(cp) - - for l in data["links"]: - cps[l[0]].connect(cps[l[1]]) - cps[l[1]].connect(cps[l[0]]) - - return t - - -class CaucasusTheater(ConflictTheater): - terrain = caucasus.Caucasus() - overview_image = "caumap.gif" - reference_points = {(-317948.32727306, 635639.37385346): (278.5 * 4, 319 * 4), - (-355692.3067714, 617269.96285781): (263 * 4, 352 * 4), } - - landmap = load_landmap("resources\\caulandmap.p") - daytime_map = { - "dawn": (6, 9), - "day": (9, 18), - "dusk": (18, 20), - "night": (0, 5), - } - - -class PersianGulfTheater(ConflictTheater): - terrain = persiangulf.PersianGulf() - overview_image = "persiangulf.gif" - reference_points = { - (persiangulf.Shiraz_International_Airport.position.x, persiangulf.Shiraz_International_Airport.position.y): ( - 772, -1970), - (persiangulf.Liwa_Airbase.position.x, persiangulf.Liwa_Airbase.position.y): (1188, 78), } - landmap = load_landmap("resources\\gulflandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } - -class NevadaTheater(ConflictTheater): - terrain = nevada.Nevada() - overview_image = "nevada.gif" - reference_points = {(nevada.Mina_Airport_3Q0.position.x, nevada.Mina_Airport_3Q0.position.y): (45 * 2, -360 * 2), - (nevada.Laughlin_Airport.position.x, nevada.Laughlin_Airport.position.y): (440 * 2, 80 * 2), } - landmap = load_landmap("resources\\nevlandmap.p") - daytime_map = { - "dawn": (4, 6), - "day": (6, 17), - "dusk": (17, 18), - "night": (0, 5), - } - -class NormandyTheater(ConflictTheater): - terrain = normandy.Normandy() - overview_image = "normandy.gif" - reference_points = {(normandy.Needs_Oar_Point.position.x, normandy.Needs_Oar_Point.position.y): (-170, -1000), - (normandy.Evreux.position.x, normandy.Evreux.position.y): (2020, 500)} - landmap = load_landmap("resources\\normandylandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (10, 17), - "dusk": (17, 18), - "night": (0, 5), - } - -class TheChannelTheater(ConflictTheater): - terrain = thechannel.TheChannel() - overview_image = "thechannel.gif" - reference_points = {(thechannel.Abbeville_Drucat.position.x, thechannel.Abbeville_Drucat.position.y): (2400, 4100), - (thechannel.Detling.position.x, thechannel.Detling.position.y): (1100, 2000)} - landmap = load_landmap("resources\\channellandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (10, 17), - "dusk": (17, 18), - "night": (0, 5), - } - -class SyriaTheater(ConflictTheater): - terrain = syria.Syria() - overview_image = "syria.gif" - reference_points = {(syria.Eyn_Shemer.position.x, syria.Eyn_Shemer.position.y): (1300, 1380), - (syria.Tabqa.position.x, syria.Tabqa.position.y): (2060, 570)} - landmap = load_landmap("resources\\syrialandmap.p") - daytime_map = { - "dawn": (6, 8), - "day": (8, 16), - "dusk": (16, 18), - "night": (0, 5), - } - -@dataclass -class ComplexFrontLine: - """ - Stores data necessary for building a multi-segment frontline. - "points" should be ordered from closest to farthest distance originating from start_cp.position - """ - - start_cp: ControlPoint - points: List[Point] - - -@dataclass -class FrontLineSegment: - """ - Describes a line segment of a FrontLine - """ - - point_a: Point - point_b: Point - - @property - def attack_heading(self) -> Numeric: - """The heading of the frontline segment from player to enemy control point""" - return self.point_a.heading_between_point(self.point_b) - - @property - def attack_distance(self) -> Numeric: - """Length of the segment""" - return self.point_a.distance_to_point(self.point_b) - - -class FrontLine(MissionTarget): - """Defines a front line location between two control points. - Front lines are the area where ground combat happens. - Overwrites the entirety of MissionTarget __init__ method to allow for - dynamic position calculation. - """ - - def __init__( - self, - control_point_a: ControlPoint, - control_point_b: ControlPoint, - theater: ConflictTheater - ) -> None: - self.control_point_a = control_point_a - self.control_point_b = control_point_b - self.segments: List[FrontLineSegment] = [] - self.theater = theater - self._build_segments() - self.name = f"Front line {control_point_a}/{control_point_b}" - - def is_friendly(self, to_player: bool) -> bool: - """Returns True if the objective is in friendly territory.""" - return False - - def mission_types(self, for_player: bool) -> Iterator[FlightType]: - yield from [ - FlightType.CAS, - # TODO: FlightType.TROOP_TRANSPORT - # TODO: FlightType.EVAC - ] - yield from super().mission_types(for_player) - - @property - def position(self): - """ - The position where the conflict should occur - according to the current strength of each control point. - """ - return self.point_from_a(self._position_distance) - - @property - def control_points(self) -> Tuple[ControlPoint, ControlPoint]: - """Returns a tuple of the two control points.""" - return self.control_point_a, self.control_point_b - - @property - def middle_point(self): - self.point_from_a(self.attack_distance / 2) - - @property - def attack_distance(self): - """The total distance of all segments""" - return sum(i.attack_distance for i in self.segments) - - @property - def attack_heading(self): - """The heading of the active attack segment from player to enemy control point""" - return self.active_segment.attack_heading - - @property - def active_segment(self) -> FrontLineSegment: - """The FrontLine segment where there can be an active conflict""" - if self._position_distance <= self.segments[0].attack_distance: - return self.segments[0] - - remaining_dist = self._position_distance - for segment in self.segments: - if remaining_dist <= segment.attack_distance: - return segment - else: - remaining_dist -= segment.attack_distance - logging.error( - "Frontline attack distance is greater than the sum of its segments" - ) - return self.segments[0] - - def point_from_a(self, distance: Numeric) -> Point: - """ - Returns a point {distance} away from control_point_a along the frontline segments. - """ - if distance < self.segments[0].attack_distance: - return self.control_point_a.position.point_from_heading( - self.segments[0].attack_heading, distance - ) - remaining_dist = distance - for segment in self.segments: - if remaining_dist < segment.attack_distance: - return segment.point_a.point_from_heading( - segment.attack_heading, remaining_dist - ) - else: - remaining_dist -= segment.attack_distance - - @property - def _position_distance(self) -> float: - """ - The distance from point "a" where the conflict should occur - according to the current strength of each control point - """ - total_strength = ( - self.control_point_a.base.strength + self.control_point_b.base.strength - ) - if self.control_point_a.base.strength == 0: - return self._adjust_for_min_dist(0) - if self.control_point_b.base.strength == 0: - return self._adjust_for_min_dist(self.attack_distance) - strength_pct = self.control_point_a.base.strength / total_strength - return self._adjust_for_min_dist(strength_pct * self.attack_distance) - - def _adjust_for_min_dist(self, distance: Numeric) -> Numeric: - """ - Ensures the frontline conflict is never located within the minimum distance - constant of either end control point. - """ - if (distance > self.attack_distance / 2) and ( - distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance - ): - distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE - elif (distance < self.attack_distance / 2) and ( - distance < FRONTLINE_MIN_CP_DISTANCE - ): - distance = FRONTLINE_MIN_CP_DISTANCE - return distance - - def _build_segments(self) -> None: - """Create line segments for the frontline""" - control_point_ids = "|".join( - [str(self.control_point_a.id), str(self.control_point_b.id)] - ) # from_cp.id|to_cp.id - reversed_cp_ids = "|".join( - [str(self.control_point_b.id), str(self.control_point_a.id)] - ) - complex_frontlines = self.theater.frontline_data - if (complex_frontlines) and ( - (control_point_ids in complex_frontlines) - or (reversed_cp_ids in complex_frontlines) - ): - # The frontline segments must be stored in the correct order for the distance algorithms to work. - # The points in the frontline are ordered from the id before the | to the id after. - # First, check if control point id pair matches in order, and create segments if a match is found. - if control_point_ids in complex_frontlines: - point_pairs = pairwise(complex_frontlines[control_point_ids].points) - for i in point_pairs: - self.segments.append(FrontLineSegment(i[0], i[1])) - # Check the reverse order and build in reverse if found. - elif reversed_cp_ids in complex_frontlines: - point_pairs = pairwise( - reversed(complex_frontlines[reversed_cp_ids].points) - ) - for i in point_pairs: - self.segments.append(FrontLineSegment(i[0], i[1])) - # If no complex frontline has been configured, fall back to the old straight line method. - else: - self.segments.append( - FrontLineSegment( - self.control_point_a.position, self.control_point_b.position - ) - ) - - - @staticmethod - def load_json_frontlines( - theater: ConflictTheater - ) -> Optional[Dict[str, ComplexFrontLine]]: - """Load complex frontlines from json""" - try: - path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json") - with open(path, "r") as file: - logging.debug(f"Loading frontline from {path}...") - data = json.load(file) - return { - frontline: ComplexFrontLine( - data[frontline]["start_cp"], - [Point(i[0], i[1]) for i in data[frontline]["points"]], - ) - for frontline in data - } - except OSError: - logging.warning( - f"Unable to load preset frontlines for {theater.terrain.name}" - ) - return None +# For save compat. Remove in 2.3. +from game.theater.conflicttheater import * diff --git a/theater/controlpoint.py b/theater/controlpoint.py index b4025663..90a6b164 100644 --- a/theater/controlpoint.py +++ b/theater/controlpoint.py @@ -1,262 +1,2 @@ -from __future__ import annotations - -import itertools -import re -from enum import Enum -from typing import Dict, Iterator, List, TYPE_CHECKING - -from dcs.mapping import Point -from dcs.ships import ( - CVN_74_John_C__Stennis, - CV_1143_5_Admiral_Kuznetsov, - LHA_1_Tarawa, - Type_071_Amphibious_Transport_Dock, -) -from dcs.terrain.terrain import Airport - -from game import db -from gen.ground_forces.combat_stance import CombatStance -from .base import Base -from .missiontarget import MissionTarget -from .theatergroundobject import ( - BaseDefenseGroundObject, - TheaterGroundObject, -) - -if TYPE_CHECKING: - from game import Game - from gen.flights.flight import FlightType - - -class ControlPointType(Enum): - AIRBASE = 0 # An airbase with slots for everything - AIRCRAFT_CARRIER_GROUP = 1 # A group with a Stennis type carrier (F/A-18, F-14 compatible) - LHA_GROUP = 2 # A group with a Tarawa carrier (Helicopters & Harrier) - FARP = 4 # A FARP, with slots for helicopters - FOB = 5 # A FOB (ground units only) - - -class ControlPoint(MissionTarget): - - position = None # type: Point - name = None # type: str - - captured = False - has_frontline = True - frontline_offset = 0.0 - - alt = 0 - - def __init__(self, id: int, name: str, position: Point, - at: db.StartingPosition, radials: List[int], size: int, - importance: float, has_frontline=True, - cptype=ControlPointType.AIRBASE): - super().__init__(" ".join(re.split(r" |-", name)[:2]), position) - self.id = id - self.full_name = name - self.at = at - self.connected_objectives: List[TheaterGroundObject] = [] - self.base_defenses: List[BaseDefenseGroundObject] = [] - - self.size = size - self.importance = importance - self.captured = False - self.captured_invert = False - self.has_frontline = has_frontline - self.radials = radials - self.connected_points: List[ControlPoint] = [] - self.base: Base = Base() - self.cptype = cptype - self.stances: Dict[int, CombatStance] = {} - self.airport = None - - @property - def ground_objects(self) -> List[TheaterGroundObject]: - return list( - itertools.chain(self.connected_objectives, self.base_defenses)) - - @classmethod - def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True): - assert airport - obj = cls(airport.id, airport.name, airport.position, airport, radials, size, importance, has_frontline, cptype=ControlPointType.AIRBASE) - obj.airport = airport() - return obj - - @classmethod - def carrier(cls, name: str, at: Point, id: int): - import theater.conflicttheater - cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, - has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP) - return cp - - @classmethod - def lha(cls, name: str, at: Point, id: int): - import theater.conflicttheater - cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1, - has_frontline=False, cptype=ControlPointType.LHA_GROUP) - return cp - - @property - def heading(self): - if self.cptype == ControlPointType.AIRBASE: - return self.airport.runways[0].heading - elif self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP]: - return 0 # TODO compute heading - else: - return 0 - - def __str__(self): - return self.name - - @property - def is_global(self): - return not self.connected_points - - @property - def is_carrier(self): - """ - :return: Whether this control point is an aircraft carrier - """ - return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] - - @property - def is_fleet(self): - """ - :return: Whether this control point is a boat (mobile) - """ - return self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] - - @property - def is_lha(self): - """ - :return: Whether this control point is an LHA - """ - return self.cptype in [ControlPointType.LHA_GROUP] - - @property - def sea_radials(self) -> List[int]: - # TODO: fix imports - all_radials = [0, 45, 90, 135, 180, 225, 270, 315, ] - result = [] - for r in all_radials: - if r not in self.radials: - result.append(r) - return result - - @property - def available_aircraft_slots(self): - """ - :return: The maximum number of aircraft that can be stored in this control point - """ - if self.cptype == ControlPointType.AIRBASE: - return len(self.airport.parking_slots) - elif self.is_lha: - return 20 - elif self.is_carrier: - return 90 - else: - return 0 - - def connect(self, to): - self.connected_points.append(to) - self.stances[to.id] = CombatStance.DEFENSIVE - - def has_runway(self): - """ - Check whether this control point can have aircraft taking off or landing. - :return: - """ - if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] : - 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 - return False - elif self.cptype in [ControlPointType.AIRBASE, ControlPointType.FARP]: - return True - else: - return True - - def get_carrier_group_name(self): - """ - Get the carrier group name if the airbase is a carrier - :return: Carrier group name - """ - if self.cptype in [ControlPointType.AIRCRAFT_CARRIER_GROUP, ControlPointType.LHA_GROUP] : - for g in self.ground_objects: - if g.dcs_identifier == "CARRIER": - for group in g.groups: - for u in group.units: - if db.unit_type_from_name(u.type) in [CVN_74_John_C__Stennis, CV_1143_5_Admiral_Kuznetsov]: - return group.name - elif g.dcs_identifier == "LHA": - for group in g.groups: - for u in group.units: - if db.unit_type_from_name(u.type) in [LHA_1_Tarawa]: - return group.name - return None - - def is_connected(self, to) -> bool: - return to in self.connected_points - - def find_radial(self, heading: int, ignored_radial: int = None) -> int: - closest_radial = 0 - closest_radial_delta = 360 - for radial in [x for x in self.radials if x != ignored_radial]: - delta = abs(radial - heading) - if delta < closest_radial_delta: - closest_radial = radial - closest_radial_delta = delta - - return closest_radial - - def find_ground_objects_by_obj_name(self, obj_name): - found = [] - for g in self.ground_objects: - if g.obj_name == obj_name: - found.append(g) - return found - - def is_friendly(self, to_player: bool) -> bool: - return self.captured == to_player - - def capture(self, game: Game, for_player: bool) -> None: - if for_player: - self.captured = True - else: - self.captured = False - - self.base.set_strength_to_minimum() - - self.base.aircraft = {} - self.base.armor = {} - - # Handle cyclic dependency. - from .start_generator import BaseDefenseGenerator - self.base_defenses = [] - BaseDefenseGenerator(game, self).generate() - - def mission_types(self, for_player: bool) -> Iterator[FlightType]: - yield from super().mission_types(for_player) - if self.is_friendly(for_player): - if self.is_fleet: - yield from [ - # TODO: FlightType.INTERCEPTION - # TODO: Buddy tanking for the A-4? - # TODO: Rescue chopper? - # TODO: Inter-ship logistics? - ] - else: - yield from [ - # TODO: FlightType.INTERCEPTION - # TODO: FlightType.LOGISTICS - ] - else: - if self.is_fleet: - yield FlightType.ANTISHIP - else: - yield from [ - # TODO: FlightType.STRIKE - ] +# For save compat. Remove in 2.3. +from game.theater.controlpoint import * diff --git a/theater/frontline.py b/theater/frontline.py index 6177b6a6..5ddb5706 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -1,2 +1,3 @@ -"""Only here to keep compatibility for save games generated in version 2.2.0""" -from theater.conflicttheater import * +# For save compat. Remove in 2.3. +from game.theater.frontline import * +from game.theater.conflicttheater import FrontLine \ No newline at end of file diff --git a/theater/theatergroundobject.py b/theater/theatergroundobject.py index 2a829de2..3c77455d 100644 --- a/theater/theatergroundobject.py +++ b/theater/theatergroundobject.py @@ -1,336 +1,2 @@ -from __future__ import annotations - -import itertools -from typing import Iterator, List, TYPE_CHECKING - -from dcs.mapping import Point -from dcs.unit import Unit -from dcs.unitgroup import Group - -if TYPE_CHECKING: - from .controlpoint import ControlPoint - from gen.flights.flight import FlightType - -from .missiontarget import MissionTarget - -NAME_BY_CATEGORY = { - "power": "Power plant", - "ammo": "Ammo depot", - "fuel": "Fuel depot", - "aa": "AA Defense Site", - "ware": "Warehouse", - "farp": "FARP", - "fob": "FOB", - "factory": "Factory", - "comms": "Comms. tower", - "oil": "Oil platform", - "derrick": "Derrick", - "ww2bunker": "Bunker", - "village": "Village", - "allycamp": "Camp", - "EWR":"EWR", -} - -ABBREV_NAME = { - "power": "PLANT", - "ammo": "AMMO", - "fuel": "FUEL", - "aa": "AA", - "ware": "WARE", - "farp": "FARP", - "fob": "FOB", - "factory": "FACTORY", - "comms": "COMMST", - "oil": "OILP", - "derrick": "DERK", - "ww2bunker": "BUNK", - "village": "VLG", - "allycamp": "CMP", -} - -CATEGORY_MAP = { - - # Special cases - "CARRIER": ["CARRIER"], - "LHA": ["LHA"], - "aa": ["AA"], - - # Buildings - "power": ["Workshop A", "Electric power box", "Garage small A", "Farm B", "Repair workshop", "Garage B"], - "ware": ["Warehouse", "Hangar A"], - "fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"], - "ammo": [".Ammunition depot", "Hangar B"], - "farp": ["FARP Tent", "FARP Ammo Dump Coating", "FARP Fuel Depot", "FARP Command Post", "FARP CP Blindage"], - "fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"], - "factory": ["Tech combine", "Tech hangar A"], - "comms": ["TV tower", "Comms tower M"], - "oil": ["Oil platform"], - "derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"], - "ww2bunker": ["Siegfried Line", "Fire Control Bunker", "SK_C_28_naval_gun", "Concertina Wire", "Czech hedgehogs 1"], - "village": ["Small house 1B", "Small House 1A", "Small warehouse 1"], - "allycamp": [], -} - - -class TheaterGroundObject(MissionTarget): - - def __init__(self, name: str, category: str, group_id: int, position: Point, - heading: int, control_point: ControlPoint, dcs_identifier: str, - airbase_group: bool, sea_object: bool) -> None: - super().__init__(name, position) - self.category = category - self.group_id = group_id - self.heading = heading - self.control_point = control_point - self.dcs_identifier = dcs_identifier - self.airbase_group = airbase_group - self.sea_object = sea_object - self.is_dead = False - # TODO: There is never more than one group. - self.groups: List[Group] = [] - - @property - def units(self) -> List[Unit]: - """ - :return: all the units at this location - """ - return list(itertools.chain.from_iterable([g.units for g in self.groups])) - - @property - def group_name(self) -> str: - """The name of the unit group.""" - return f"{self.category}|{self.group_id}" - - def __str__(self) -> str: - return NAME_BY_CATEGORY[self.category] - - def is_same_group(self, identifier: str) -> bool: - return self.group_id == identifier - - @property - def obj_name(self) -> str: - return self.name - - @property - def faction_color(self) -> str: - return "BLUE" if self.control_point.captured else "RED" - - def is_friendly(self, to_player: bool) -> bool: - return self.control_point.is_friendly(to_player) - - def mission_types(self, for_player: bool) -> Iterator[FlightType]: - from gen.flights.flight import FlightType - if self.is_friendly(for_player): - yield from [ - # TODO: FlightType.LOGISTICS - # TODO: FlightType.TROOP_TRANSPORT - ] - else: - yield from [ - FlightType.STRIKE, - FlightType.BAI, - ] - yield from super().mission_types(for_player) - - -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: - super().__init__( - name=name, - category=category, - group_id=group_id, - position=position, - heading=heading, - control_point=control_point, - dcs_identifier=dcs_identifier, - airbase_group=False, - sea_object=False - ) - self.object_id = object_id - - @property - def group_name(self) -> str: - """The name of the unit group.""" - return f"{self.category}|{self.group_id}|{self.object_id}" - - -class NavalGroundObject(TheaterGroundObject): - def mission_types(self, for_player: bool) -> Iterator[FlightType]: - from gen.flights.flight import FlightType - if not self.is_friendly(for_player): - yield FlightType.ANTISHIP - yield from super().mission_types(for_player) - - -class GenericCarrierGroundObject(NavalGroundObject): - pass - - -# TODO: Why is this both a CP and a TGO? -class CarrierGroundObject(GenericCarrierGroundObject): - def __init__(self, name: str, group_id: int, - control_point: ControlPoint) -> None: - super().__init__( - name=name, - category="CARRIER", - group_id=group_id, - position=control_point.position, - heading=0, - control_point=control_point, - dcs_identifier="CARRIER", - airbase_group=True, - sea_object=True - ) - - @property - def group_name(self) -> str: - # Prefix the group names with the side color so Skynet can find them, - # add to EWR. - return f"{self.faction_color}|EWR|{super().group_name}" - - -# TODO: Why is this both a CP and a TGO? -class LhaGroundObject(GenericCarrierGroundObject): - def __init__(self, name: str, group_id: int, - control_point: ControlPoint) -> None: - super().__init__( - name=name, - category="LHA", - group_id=group_id, - position=control_point.position, - heading=0, - control_point=control_point, - dcs_identifier="LHA", - airbase_group=True, - sea_object=True - ) - - @property - def group_name(self) -> str: - # Prefix the group names with the side color so Skynet can find them, - # add to EWR. - return f"{self.faction_color}|EWR|{super().group_name}" - - -class MissileSiteGroundObject(TheaterGroundObject): - def __init__(self, name: str, group_id: int, position: Point, - control_point: ControlPoint) -> None: - super().__init__( - name=name, - category="aa", - group_id=group_id, - position=position, - heading=0, - control_point=control_point, - dcs_identifier="AA", - airbase_group=False, - sea_object=False - ) - - -class BaseDefenseGroundObject(TheaterGroundObject): - """Base type for all base defenses.""" - - -# TODO: Differentiate types. -# This type gets used both for AA sites (SAM, AAA, or SHORAD) but also for the -# armor garrisons at airbases. These should each be split into their own types. -class SamGroundObject(BaseDefenseGroundObject): - def __init__(self, name: str, group_id: int, position: Point, - control_point: ControlPoint, for_airbase: bool) -> None: - super().__init__( - name=name, - category="aa", - group_id=group_id, - position=position, - 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 - # with Skynet. - self.skynet_capable = False - - @property - def group_name(self) -> str: - if self.skynet_capable: - # Prefix the group names of SAM sites with the side color so Skynet - # can find them. - return f"{self.faction_color}|SAM|{self.group_id}" - else: - return super().group_name - - def mission_types(self, for_player: bool) -> Iterator[FlightType]: - from gen.flights.flight import FlightType - if not self.is_friendly(for_player): - yield FlightType.DEAD - yield from super().mission_types(for_player) - - -class VehicleGroupGroundObject(BaseDefenseGroundObject): - def __init__(self, name: str, group_id: int, position: Point, - control_point: ControlPoint, for_airbase: bool) -> None: - super().__init__( - name=name, - category="aa", - group_id=group_id, - position=position, - heading=0, - control_point=control_point, - dcs_identifier="AA", - airbase_group=for_airbase, - sea_object=False - ) - - -class EwrGroundObject(BaseDefenseGroundObject): - def __init__(self, name: str, group_id: int, position: Point, - control_point: ControlPoint) -> None: - super().__init__( - name=name, - category="EWR", - group_id=group_id, - position=position, - heading=0, - control_point=control_point, - dcs_identifier="EWR", - airbase_group=True, - sea_object=False - ) - - @property - def group_name(self) -> str: - # Prefix the group names with the side color so Skynet can find them. - return f"{self.faction_color}|{super().group_name}" - - def mission_types(self, for_player: bool) -> Iterator[FlightType]: - from gen.flights.flight import FlightType - if not self.is_friendly(for_player): - yield FlightType.DEAD - yield from super().mission_types(for_player) - - -class ShipGroundObject(NavalGroundObject): - def __init__(self, name: str, group_id: int, position: Point, - control_point: ControlPoint) -> None: - super().__init__( - name=name, - category="aa", - group_id=group_id, - position=position, - heading=0, - control_point=control_point, - dcs_identifier="AA", - airbase_group=False, - sea_object=True - ) - - @property - def group_name(self) -> str: - # Prefix the group names with the side color so Skynet can find them, - # add to EWR. - return f"{self.faction_color}|EWR|{super().group_name}" +# For save compat. Remove in 2.3. +from game.theater.theatergroundobject import * diff --git a/theater/weatherforecast.py b/theater/weatherforecast.py deleted file mode 100644 index 65ed7351..00000000 --- a/theater/weatherforecast.py +++ /dev/null @@ -1,4 +0,0 @@ - - -class WeatherForecast: - pass