From 33885e2216eca1427bff43a174f654f9d2c20d52 Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Thu, 12 Nov 2020 21:47:13 -0600 Subject: [PATCH] initial multi segment frontline implementation --- game/game.py | 3 +- gen/armor.py | 2 +- gen/conflictgen.py | 18 +-- gen/groundobjectsgen.py | 2 +- gen/visualgen.py | 2 +- .../QPredefinedWaypointSelectionComboBox.py | 2 +- qt_ui/widgets/map/QLiberationMap.py | 21 ++- theater/conflicttheater.py | 12 +- theater/frontline.py | 140 ++++++++++-------- theater/start_generator.py | 1 - 10 files changed, 115 insertions(+), 88 deletions(-) diff --git a/game/game.py b/game/game.py index 775f36f3..49933e43 100644 --- a/game/game.py +++ b/game/game.py @@ -393,8 +393,7 @@ class Game: # By default, use the existing frontline conflict position for front_line in self.theater.conflicts(): - position = Conflict.frontline_position(self.theater, - front_line.control_point_a, + position = Conflict.frontline_position(front_line.control_point_a, front_line.control_point_b) points.append(position[0]) points.append(front_line.control_point_a.position) diff --git a/gen/armor.py b/gen/armor.py index 5685a120..f7875c72 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -93,7 +93,7 @@ class GroundConflictGenerator: if combat_width < 35000: combat_width = 35000 - position = Conflict.frontline_position(self.game.theater, self.conflict.from_cp, self.conflict.to_cp) + position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp) # Create player groups at random position for group in self.player_planned_combat_groups: diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 3c9eecfe..d7d90ea9 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -6,6 +6,7 @@ from dcs.country import Country from dcs.mapping import Point from theater import ConflictTheater, ControlPoint +from theater.frontline import FrontLine AIR_DISTANCE = 40000 @@ -134,14 +135,11 @@ class Conflict: def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool: return from_cp.has_frontline and to_cp.has_frontline - @classmethod - def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]: - attack_heading = from_cp.position.heading_between_point(to_cp.position) - attack_distance = from_cp.position.distance_to_point(to_cp.position) - middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2) - - strength_delta = (from_cp.base.strength - to_cp.base.strength) / 1.0 - position = middle_point.point_from_heading(attack_heading, strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE) + @staticmethod + def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]: + frontline = FrontLine(from_cp, to_cp) + attack_heading = frontline.attack_heading + position = frontline.position return position, _opposite_heading(attack_heading) @@ -162,7 +160,7 @@ class Conflict: return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length """ - frontline = cls.frontline_position(theater, from_cp, to_cp) + frontline = cls.frontline_position(from_cp, to_cp) center_position, heading = frontline left_position, right_position = None, None @@ -479,7 +477,7 @@ class Conflict: @classmethod def transport_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - frontline_position, heading = cls.frontline_position(theater, from_cp, to_cp) + frontline_position, heading = cls.frontline_position(from_cp, to_cp) initial_dest = frontline_position.point_from_heading(heading, TRANSPORT_FRONTLINE_DIST) dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater) if not dest: diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 1989452e..e47acce6 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -370,7 +370,7 @@ class GroundObjectsGenerator: center = self.conflict.center heading = self.conflict.heading - 90 else: - center, heading = self.conflict.frontline_position(self.conflict.theater, self.conflict.from_cp, self.conflict.to_cp) + center, heading = self.conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp) heading -= 90 initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE) diff --git a/gen/visualgen.py b/gen/visualgen.py index efd0c1f9..c187ec90 100644 --- a/gen/visualgen.py +++ b/gen/visualgen.py @@ -104,7 +104,7 @@ class VisualGenerator: if from_cp.is_global or to_cp.is_global: continue - frontline = Conflict.frontline_position(self.game.theater, from_cp, to_cp) + frontline = Conflict.frontline_position(from_cp, to_cp) if not frontline: continue diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index 72ece41e..a3f8f029 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -55,7 +55,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): if cp.captured: enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured] for ecp in enemy_cp: - pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0] + pos = Conflict.frontline_position(cp, ecp)[0] wpt = FlightWaypoint( FlightWaypointType.CUSTOM, pos.x, diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 5834ba6f..25d5c83d 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -38,7 +38,8 @@ from qt_ui.widgets.map.QLiberationScene import QLiberationScene 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, FrontLine +from theater import ControlPoint +from theater.frontline import FrontLine from theater.theatergroundobject import ( EwrGroundObject, MissileSiteGroundObject, @@ -407,13 +408,21 @@ class QLiberationMap(QGraphicsView): pen = QPen(brush=color) pen.setColor(color) pen.setWidth(6) + frontline = FrontLine(cp, connected_cp) if cp.captured and not connected_cp.captured and Conflict.has_frontline_between(cp, connected_cp): if not cp.captured: scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) else: - posx, h = Conflict.frontline_position(self.game.theater, cp, connected_cp) + # pass + # frontline = FrontLine(cp, connected_cp, self.game.theater.terrain) + posx = frontline.position + h = frontline.attack_heading pos2 = self._transform_point(posx) - scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) + for segment in frontline.segments: + seg_a = self._transform_point(segment.point_a) + seg_b = self._transform_point(segment.point_b) + scene.addLine(seg_a[0], seg_a[1], seg_b[0], seg_b[1], pen=pen) + # scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) p1 = point_from_heading(pos2[0], pos2[1], h+180, 25) p2 = point_from_heading(pos2[0], pos2[1], h, 25) @@ -421,7 +430,11 @@ class QLiberationMap(QGraphicsView): FrontLine(cp, connected_cp))) else: - scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) + for segment in frontline.segments: + seg_a = self._transform_point(segment.point_a) + seg_b = self._transform_point(segment.point_b) + scene.addLine(seg_a[0], seg_a[1], seg_b[0], seg_b[1], pen=pen) + # scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) def wheelEvent(self, event: QWheelEvent): diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index cf611b10..10ce735d 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -15,7 +15,7 @@ from dcs.terrain.terrain import Terrain from .controlpoint import ControlPoint from .landmap import Landmap, load_landmap, poly_contains -from .frontline import FrontLine +from .frontline import FrontLine, ComplexFrontLine SIZE_TINY = 150 SIZE_SMALL = 600 @@ -61,6 +61,7 @@ class ConflictTheater: reference_points: Dict[Tuple[float, float], Tuple[float, float]] overview_image: str landmap: Optional[Landmap] + frontline_data: Optional[Dict[str, ComplexFrontLine]] = None """ land_poly = None # type: Polygon """ @@ -68,6 +69,7 @@ class ConflictTheater: def __init__(self): self.controlpoints: List[ControlPoint] = [] + ConflictTheater.frontline_data = FrontLine.load_json_frontlines(self) """ self.land_poly = geometry.Polygon(self.landmap[0][0]) for x in self.landmap[1]: @@ -196,7 +198,7 @@ class ConflictTheater: cps[l[1]].connect(cps[l[0]]) return t - + class CaucasusTheater(ConflictTheater): terrain = caucasus.Caucasus() @@ -228,7 +230,6 @@ class PersianGulfTheater(ConflictTheater): "night": (0, 5), } - class NevadaTheater(ConflictTheater): terrain = nevada.Nevada() overview_image = "nevada.gif" @@ -242,7 +243,6 @@ class NevadaTheater(ConflictTheater): "night": (0, 5), } - class NormandyTheater(ConflictTheater): terrain = normandy.Normandy() overview_image = "normandy.gif" @@ -256,7 +256,6 @@ class NormandyTheater(ConflictTheater): "night": (0, 5), } - class TheChannelTheater(ConflictTheater): terrain = thechannel.TheChannel() overview_image = "thechannel.gif" @@ -270,7 +269,6 @@ class TheChannelTheater(ConflictTheater): "night": (0, 5), } - class SyriaTheater(ConflictTheater): terrain = syria.Syria() overview_image = "syria.gif" @@ -282,4 +280,4 @@ class SyriaTheater(ConflictTheater): "day": (8, 16), "dusk": (16, 18), "night": (0, 5), - } + } \ No newline at end of file diff --git a/theater/frontline.py b/theater/frontline.py index f8c5c4d3..51f5c423 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -1,17 +1,21 @@ """Battlefield front lines.""" from __future__ import annotations -from dataclasses import dataclass + import logging import json +from dataclasses import dataclass from pathlib import Path from itertools import tee -from typing import Tuple, List, Union, Dict, Optional +from typing import Tuple, List, Union, Dict, Optional, TYPE_CHECKING from dcs.mapping import Point from .controlpoint import ControlPoint, MissionTarget +if TYPE_CHECKING: + from theater.conflicttheater import ConflictTheater + Numeric = Union[int, float] # TODO: Dedup by moving everything to using this class. @@ -19,7 +23,10 @@ FRONTLINE_MIN_CP_DISTANCE = 5000 def pairwise(iterable): - "s -> (s0,s1), (s1,s2), (s2, s3), ..." + """ + itertools recipe + s -> (s0,s1), (s1,s2), (s2, s3), ... + """ a, b = tee(iterable) next(b, None) return zip(a, b) @@ -48,10 +55,12 @@ class FrontLineSegment: @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) @@ -61,7 +70,8 @@ class FrontLine(MissionTarget): Overwrites the entirety of MissionTarget __init__ method to allow for dynamic position calculation. """ - frontline_data: Optional[Dict[str, ComplexFrontLine]] = None + + theater: ConflictTheater def __init__( self, @@ -73,7 +83,6 @@ class FrontLine(MissionTarget): self.segments: List[FrontLineSegment] = [] self._build_segments() self.name = f"Front line {control_point_a}/{control_point_b}" - print(f"FRONTLINE SEGMENTS {len(self.segments)}") @property def position(self): @@ -90,10 +99,12 @@ class FrontLine(MissionTarget): @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 @@ -113,6 +124,30 @@ class FrontLine(MissionTarget): ) return self.segments[0] + def _calculate_position(self) -> Point: + """ + The position where the conflict should occur + according to the current strength of each control point. + """ + return self.point_from_a(self._position_distance) + + 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: """ @@ -122,44 +157,37 @@ class FrontLine(MissionTarget): total_strength = ( self.control_point_a.base.strength + self.control_point_b.base.strength ) - if total_strength == 0: + 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) - @classmethod - def load_json_frontlines(cls, terrain_name: str) -> None: - try: - path = Path(f"resources/frontlines/{terrain_name.lower()}.json") - with open(path, "r") as file: - logging.debug(f"Loading frontline from {path}...") - data = json.load(file) - cls.frontline_data = { - frontline: ComplexFrontLine( - data[frontline]["start_cp"], - [Point(i[0], i[1]) for i in data[frontline]["points"]], - ) - for frontline in data - } - print(cls.frontline_data) - except OSError: - logging.warning(f"Unable to load preset frontlines for {terrain_name}") - - def _calculate_position(self) -> Point: + def _adjust_for_min_dist(self, distance: Numeric) -> Numeric: """ - The position where the conflict should occur - according to the current strength of each control point. + Ensures the frontline conflict is never located within the minimum distance + constant of either end control point. """ - return self.point_from_a(self._position_distance) + 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 = FrontLine.frontline_data + complex_frontlines = self.theater.frontline_data if (complex_frontlines) and ( (control_point_ids in complex_frontlines) or (reversed_cp_ids in complex_frontlines) @@ -186,34 +214,26 @@ class FrontLine(MissionTarget): ) ) - 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 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 self.control_point_a.position.point_from_heading( - segment.attack_heading, remaining_dist + @classmethod + def load_json_frontlines( + cls, theater: ConflictTheater + ) -> Optional[Dict[str, ComplexFrontLine]]: + """Load complex frontlines from json and set the theater class variable to current theater instance""" + cls.theater = theater + 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"]], ) - else: - remaining_dist -= segment.attack_distance + for frontline in data + } + except OSError: + logging.warning( + f"Unable to load preset frontlines for {theater.terrain.name}" + ) + return None diff --git a/theater/start_generator.py b/theater/start_generator.py index 968dc9af..b5ff0a89 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -74,7 +74,6 @@ class GameGenerator: namegen.reset() self.prepare_theater() self.populate_red_airbases() - FrontLine.load_json_frontlines(self.theater.terrain.name) game = Game(player_name=self.player, enemy_name=self.enemy, theater=self.theater,