From ede5ee60c3df04772e0b0e2a5b159d2b54536b61 Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Thu, 12 Nov 2020 18:21:37 -0600 Subject: [PATCH 01/17] frontline --- theater/frontline.py | 197 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 176 insertions(+), 21 deletions(-) diff --git a/theater/frontline.py b/theater/frontline.py index c70b3417..aa2852af 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -1,44 +1,199 @@ """Battlefield front lines.""" -from typing import Tuple +from __future__ import annotations +from dataclasses import dataclass + +import logging + +from itertools import tee +from typing import Tuple, List, Union, Dict from dcs.mapping import Point -from . import ControlPoint, MissionTarget + +from .controlpoint import ControlPoint, MissionTarget + +Numeric = Union[int, float] # TODO: Dedup by moving everything to using this class. FRONTLINE_MIN_CP_DISTANCE = 5000 -def compute_position(control_point_a: ControlPoint, - control_point_b: ControlPoint) -> Point: - a = control_point_a.position - b = control_point_b.position - attack_heading = a.heading_between_point(b) - attack_distance = a.distance_to_point(b) - middle_point = a.point_from_heading(attack_heading, attack_distance / 2) +def pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2, s3), ..." + a, b = tee(iterable) + next(b, None) + return zip(a, b) - strength_delta = float(control_point_a.base.strength - - control_point_b.base.strength) - position = middle_point.point_from_heading(attack_heading, - strength_delta * - attack_distance / 2 - - FRONTLINE_MIN_CP_DISTANCE) - return position + +@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] + # control_points: List[ControlPoint] + + +@dataclass +class FrontLineSegment: + """ + Describes a line segment of a FrontLine + """ + + point_a: Point + point_b: Point + + @property + def attack_heading(self) -> Numeric: + return self.point_a.heading_between_point(self.point_b) + + @property + def attack_distance(self) -> Numeric: + 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) -> None: - super().__init__(f"Front line {control_point_a}/{control_point_b}", - compute_position(control_point_a, control_point_b)) + def __init__( + self, + control_point_a: ControlPoint, + control_point_b: ControlPoint, + frontline_data: Dict[str, ComplexFrontLine], + ) -> None: self.control_point_a = control_point_a self.control_point_b = control_point_b + self.segments: List[FrontLineSegment] = [] + self._build_segments(frontline_data) + self.name = f"Front line {control_point_a}/{control_point_b}" + + @property + def position(self): + return self._calculate_position() @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): + return sum(i.attack_distance for i in self.segments) + + @property + def attack_heading(self): + 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] + + @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 total_strength == 0: + return self._adjust_for_min_dist(0) + strength_pct = self.control_point_a.base.strength / total_strength + return self._adjust_for_min_dist(strength_pct * self.attack_distance) + + 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 _build_segments(self, frontline_data: Dict[str, ComplexFrontLine]) -> None: + control_point_ids = "|".join( + [str(self.control_point_a.id), str(self.control_point_b.id)] + ) + reversed_cp_ids = "|".join( + [str(self.control_point_b.id), str(self.control_point_a.id)] + ) + complex_frontlines = 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 + ) + ) + + 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 + ) + else: + remaining_dist -= segment.attack_distance From 5719b136fe7522198abf2a3fc75f9fd94e474561 Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Thu, 12 Nov 2020 19:16:01 -0600 Subject: [PATCH 02/17] sanity check --- theater/conflicttheater.py | 8 ++------ theater/frontline.py | 32 ++++++++++++++++++++++++++------ theater/start_generator.py | 4 ++-- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 373eb959..cf611b10 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -15,9 +15,7 @@ from dcs.terrain.terrain import Terrain from .controlpoint import ControlPoint from .landmap import Landmap, load_landmap, poly_contains - -if TYPE_CHECKING: - from . import FrontLine +from .frontline import FrontLine SIZE_TINY = 150 SIZE_SMALL = 600 @@ -128,7 +126,6 @@ class ConflictTheater: return [point for point in self.controlpoints if point.captured] def conflicts(self, from_player=True) -> Iterator[FrontLine]: - from . import FrontLine # Circular import that needs to be resolved. 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) @@ -169,7 +166,7 @@ class ConflictTheater: cp.captured_invert = False return cp - + @staticmethod def from_json(data: Dict[str, Any]) -> ConflictTheater: theaters = { @@ -183,7 +180,6 @@ class ConflictTheater: theater = theaters[data["theater"]] t = theater() cps = {} - for p in data["player_points"]: cp = t.add_json_cp(theater, p) cp.captured = True diff --git a/theater/frontline.py b/theater/frontline.py index aa2852af..f8c5c4d3 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -3,9 +3,10 @@ from __future__ import annotations from dataclasses import dataclass import logging - +import json +from pathlib import Path from itertools import tee -from typing import Tuple, List, Union, Dict +from typing import Tuple, List, Union, Dict, Optional from dcs.mapping import Point @@ -60,18 +61,19 @@ class FrontLine(MissionTarget): Overwrites the entirety of MissionTarget __init__ method to allow for dynamic position calculation. """ + frontline_data: Optional[Dict[str, ComplexFrontLine]] = None def __init__( self, control_point_a: ControlPoint, control_point_b: ControlPoint, - frontline_data: Dict[str, ComplexFrontLine], ) -> None: self.control_point_a = control_point_a self.control_point_b = control_point_b self.segments: List[FrontLineSegment] = [] - self._build_segments(frontline_data) + 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): @@ -125,6 +127,24 @@ class FrontLine(MissionTarget): 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: """ The position where the conflict should occur @@ -132,14 +152,14 @@ class FrontLine(MissionTarget): """ return self.point_from_a(self._position_distance) - def _build_segments(self, frontline_data: Dict[str, ComplexFrontLine]) -> None: + def _build_segments(self) -> None: control_point_ids = "|".join( [str(self.control_point_a.id), str(self.control_point_b.id)] ) reversed_cp_ids = "|".join( [str(self.control_point_b.id), str(self.control_point_a.id)] ) - complex_frontlines = frontline_data + complex_frontlines = FrontLine.frontline_data if (complex_frontlines) and ( (control_point_ids in complex_frontlines) or (reversed_cp_ids in complex_frontlines) diff --git a/theater/start_generator.py b/theater/start_generator.py index eb0252c4..968dc9af 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -40,6 +40,7 @@ from theater.theatergroundobject import ( LhaGroundObject, MissileSiteGroundObject, ShipGroundObject, ) +from theater.frontline import FrontLine GroundObjectTemplates = Dict[str, Dict[str, Any]] @@ -73,7 +74,7 @@ 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, @@ -89,7 +90,6 @@ class GameGenerator: def prepare_theater(self) -> None: to_remove = [] - # Auto-capture half the bases if midgame. if self.midgame: control_points = self.theater.controlpoints 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 03/17] 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, From 61400ba72608b3cc8e4bb32f8be82813603d9609 Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Thu, 12 Nov 2020 21:47:54 -0600 Subject: [PATCH 04/17] caucasus test data --- resources/frontlines/caucasus.json | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 resources/frontlines/caucasus.json diff --git a/resources/frontlines/caucasus.json b/resources/frontlines/caucasus.json new file mode 100644 index 00000000..5c230fe3 --- /dev/null +++ b/resources/frontlines/caucasus.json @@ -0,0 +1,60 @@ +{ + "28|26": { + "points": [ + [ + -83519.432788948, + 834083.89603137 + ], + [ + -86613.505278114, + 739016.54498214 + ], + [ + -69364.629004892, + 719860.24185348 + ], + [ + -66223.861015868, + 712472.15322055 + ], + [ + -55063.838612323, + 701790.55845189 + ], + [ + -51581.840958501, + 704161.97484394 + ] + ], + "start_cp": 28 + }, + "32|27": { + "points": [ + [ + -148553.72718928, + 843792.03708555 + ], + [ + -117826.94574919, + 848703.6547547 + ], + [ + -110332.26741882, + 812577.62100492 + ], + [ + -111700.49804512, + 785894.84332503 + ], + [ + -122845.00736393, + 774040.49264833 + ], + [ + -124832.56316602, + 760695.47512006 + ] + ], + "start_cp": 32 + } +} \ No newline at end of file From 398630d51ed4ca8f0a5fd7daec74259d5885a86c Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Thu, 12 Nov 2020 21:51:54 -0600 Subject: [PATCH 05/17] initial tooling --- .../mizdata/caucasus/caucusus_frontline.miz | Bin 0 -> 7464 bytes resources/tools/generate_frontlines.py | 74 ++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 resources/mizdata/caucasus/caucusus_frontline.miz create mode 100644 resources/tools/generate_frontlines.py diff --git a/resources/mizdata/caucasus/caucusus_frontline.miz b/resources/mizdata/caucasus/caucusus_frontline.miz new file mode 100644 index 0000000000000000000000000000000000000000..06fa3d506365b3130aeabb35c4b5b8050e9f9123 GIT binary patch literal 7464 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 literal 0 HcmV?d00001 diff --git a/resources/tools/generate_frontlines.py b/resources/tools/generate_frontlines.py new file mode 100644 index 00000000..c1346bc9 --- /dev/null +++ b/resources/tools/generate_frontlines.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import json +import argparse +from pathlib import Path +from typing import List, Tuple, Union, Dict + +from dcs.terrain import Caucasus, PersianGulf, Syria, Nevada, Normandy, TheChannel +from dcs import Mission + +Terrain = Union[Caucasus, PersianGulf, Syria, Nevada, Normandy, TheChannel] + +SAVE_PATH = Path("resources/frontlines") + +def validate_miz(file_path: Path) -> bool: + return bool(file_path.suffix == ".miz" and file_path.exists()) + +def validate_airports(airports: Tuple[int], terrain: Terrain): + for airport in airports: + if terrain.airport_by_id(airport) is None: + print(f"Cannot load airport for invalid id {airport}") + +def load_files(files) -> List[Mission]: + missions = [] + for file in files: + if validate_miz(file): + mission = Mission() + mission.load_file(file) + missions.append(mission) + else: + print(f"Error: {file} doesn't look like a valid mission file.") + return missions + +def create_frontline_dict(mission: Mission) -> Dict[str, Dict]: + frontline_dict = {} + for group in mission.country("USA").vehicle_group: + groupname = str(group.name).replace(group.name.id, "").replace(":","") + control_points = groupname.split("|") + frontline_dict[groupname] = { + "points": [(i.position.x, i.position.y) for i in group.points], + "start_cp": int(control_points[0]) + } + return frontline_dict + +def process_missions(missions: List[Mission]) -> None: + for mission in missions: + frontline_dict = create_frontline_dict(mission) + write_json(frontline_dict, mission.terrain.name.lower()) + +def write_json(frontline_dict: Dict[str, Dict], terrain_name: str) -> None: + with open(SAVE_PATH.joinpath(terrain_name + ".json"), "w") as file: + json.dump(frontline_dict, file) + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Process a miz file to create json descriptions of multi-segment frontlines" + ) + parser.add_argument( + "files", + metavar="N", + type=Path, + nargs="+", + help="A list of space separated .miz files to extract frontlines from", + ) + + args = parser.parse_args() + missions = load_files(args.files) + process_missions(missions) + # frontline_dict = create_frontline_dict(missions[0]) + + print("Done") + + + From 6237fffa5ac7ce9763df204010132341ced07f8b Mon Sep 17 00:00:00 2001 From: walterroach Date: Fri, 13 Nov 2020 09:03:02 -0600 Subject: [PATCH 06/17] cleanup comments --- qt_ui/widgets/map/QLiberationMap.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 13538803..4d61507e 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -420,8 +420,6 @@ class QLiberationMap(QGraphicsView): if not cp.captured: scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) else: - # pass - # frontline = FrontLine(cp, connected_cp, self.game.theater.terrain) posx = frontline.position h = frontline.attack_heading pos2 = self._transform_point(posx) @@ -429,7 +427,6 @@ class QLiberationMap(QGraphicsView): 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) @@ -441,7 +438,6 @@ class QLiberationMap(QGraphicsView): 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): From 33b92423d885008bc224c7f834df6f8a59dda8fa Mon Sep 17 00:00:00 2001 From: walterroach Date: Fri, 13 Nov 2020 09:03:02 -0600 Subject: [PATCH 07/17] cleanup comments remove unnecessary method call --- qt_ui/widgets/map/QLiberationMap.py | 4 ---- theater/frontline.py | 16 ++++++---------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 13538803..4d61507e 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -420,8 +420,6 @@ class QLiberationMap(QGraphicsView): if not cp.captured: scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) else: - # pass - # frontline = FrontLine(cp, connected_cp, self.game.theater.terrain) posx = frontline.position h = frontline.attack_heading pos2 = self._transform_point(posx) @@ -429,7 +427,6 @@ class QLiberationMap(QGraphicsView): 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) @@ -441,7 +438,6 @@ class QLiberationMap(QGraphicsView): 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/frontline.py b/theater/frontline.py index 51f5c423..9cbf7608 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -41,7 +41,6 @@ class ComplexFrontLine: start_cp: ControlPoint points: List[Point] - # control_points: List[ControlPoint] @dataclass @@ -86,7 +85,11 @@ class FrontLine(MissionTarget): @property def position(self): - return self._calculate_position() + """ + 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]: @@ -124,13 +127,6 @@ 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. @@ -151,7 +147,7 @@ class FrontLine(MissionTarget): @property def _position_distance(self) -> float: """ - The distance from point a where the conflict should occur + The distance from point "a" where the conflict should occur according to the current strength of each control point """ total_strength = ( From 3838b3ca4f7ae6010ee53a2f833dbff97cd87f17 Mon Sep 17 00:00:00 2001 From: walterroach Date: Fri, 13 Nov 2020 17:03:46 -0600 Subject: [PATCH 08/17] More frontlines --- resources/frontlines/caucasus.json | 61 +----------------- .../mizdata/caucasus/caucusus_frontline.miz | Bin 7464 -> 12931 bytes 2 files changed, 1 insertion(+), 60 deletions(-) diff --git a/resources/frontlines/caucasus.json b/resources/frontlines/caucasus.json index 5c230fe3..33a3e29e 100644 --- a/resources/frontlines/caucasus.json +++ b/resources/frontlines/caucasus.json @@ -1,60 +1 @@ -{ - "28|26": { - "points": [ - [ - -83519.432788948, - 834083.89603137 - ], - [ - -86613.505278114, - 739016.54498214 - ], - [ - -69364.629004892, - 719860.24185348 - ], - [ - -66223.861015868, - 712472.15322055 - ], - [ - -55063.838612323, - 701790.55845189 - ], - [ - -51581.840958501, - 704161.97484394 - ] - ], - "start_cp": 28 - }, - "32|27": { - "points": [ - [ - -148553.72718928, - 843792.03708555 - ], - [ - -117826.94574919, - 848703.6547547 - ], - [ - -110332.26741882, - 812577.62100492 - ], - [ - -111700.49804512, - 785894.84332503 - ], - [ - -122845.00736393, - 774040.49264833 - ], - [ - -124832.56316602, - 760695.47512006 - ] - ], - "start_cp": 32 - } -} \ No newline at end of file +{"28|26": {"points": [[-83519.432788948, 834083.89603137], [-76119.776928765, 743854.15425227], [-69364.629004892, 719860.24185348], [-66223.861015868, 712472.15322055], [-55063.838612323, 701790.55845189], [-51581.840958501, 704161.97484394]], "start_cp": 28}, "32|27": {"points": [[-148553.72718928, 843792.03708555], [-114733.18523211, 848507.87882073], [-93593.960629467, 841634.68667249], [-83721.151431284, 834200.8738046]], "start_cp": 32}, "32|28": {"points": [[-148554.87082704, 843689.04452871], [-114733.18523211, 845562.62834108], [-110339.43457942, 812629.20979521], [-111707.66520572, 785946.43211532], [-119336.17607842, 776106.17954928], [-124839.73032662, 760747.06391035]], "start_cp": 32}, "27|26": {"points": [[-124831.33530722, 760646.27221205], [-100762.17095534, 753347.75031825], [-86615.899059596, 729586.43747856], [-70460.285574716, 694513.4327179], [-50443.346691892, 703139.51687735]], "start_cp": 27}, "26|16": {"points": [[-51128.273849964, 705442.60983569], [-17096.829383992, 583922.76901062], [-9413.0964883513, 489204.29454645], [-26751.202695677, 458085.37842522]], "start_cp": 26}, "16|17": {"points": [[-26290.077082534, 457532.52869133], [-12087.691792587, 444620.30900361], [-9183.8957732278, 411618.86444668], [-19095.485460604, 393640.78914313], [-12512.173613316, 386130.07819866], [-6723.0801746587, 294506.97247323], [8289.1802310393, 267690.0671157], [-1709.4806012567, 249581.13338705], [-20329.725545816, 256364.19845868], [-32338.671748957, 281488.17801525], [-44457.480224214, 298277.60074695], [-50348.762426692, 298378.38229135]], "start_cp": 16}, "17|18": {"points": [[-50428.449268252, 298385.98215495], [-47925.882995864, 299522.84586356], [-47742.307667018, 302423.33605932], [-51670.819704316, 306425.27822816], [-52890.978467907, 309683.60757879], [-52158.285335864, 315248.49431547], [-54476.310454094, 321830.02991902], [-61058.499192715, 325379.38150096], [-61379.264542506, 327677.41312797], [-59644.880081364, 329300.50019054], [-59830.375745657, 332806.36824568], [-63794.637882896, 337172.7521818], [-67470.778941327, 340636.53352785], [-70179.929699705, 341613.85524925], [-69660.171385319, 350494.94296899], [-69388.993134334, 354630.4112965], [-75106.334592587, 354856.39317232], [-76281.440346853, 358652.8886861], [-81388.630740391, 366494.45977706], [-87241.561324136, 371036.69548105], [-89840.352896069, 379895.1850132], [-95874.068980471, 384753.79534334], [-98992.618866791, 390742.31505257], [-105500.89689041, 395510.53263238], [-109975.33803166, 401747.63240502], [-112664.52235392, 406199.47535868], [-118042.89099844, 411623.04037836], [-119433.38420159, 414545.36512974], [-121777.77888263, 415031.75406772], [-125094.95143961, 417580.4321027], [-127809.00171351, 419876.18788994], [-131233.17983685, 423387.91602212], [-134219.60791602, 427162.2941808], [-142354.54855856, 436882.58763511], [-142151.02610079, 439008.26663852], [-144141.02346569, 440738.2075296], [-154337.7160827, 451791.06647059], [-156349.9178749, 457412.78487971], [-161121.11800072, 459238.28753654], [-164180.10148029, 461855.90812217]], "start_cp": 17}} \ No newline at end of file diff --git a/resources/mizdata/caucasus/caucusus_frontline.miz b/resources/mizdata/caucasus/caucusus_frontline.miz index 06fa3d506365b3130aeabb35c4b5b8050e9f9123..22795f76de348adc02977320d285bebad2d4a90f 100644 GIT binary patch delta 11977 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 AW&i*H From c20e9e19cb3f0bcc6c1e7ec9e26ecfe579785827 Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Sun, 15 Nov 2020 20:56:09 -0600 Subject: [PATCH 09/17] Add cheat for more easily debugging frontline --- qt_ui/widgets/map/QFrontLine.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py index f1425893..1849f5ff 100644 --- a/qt_ui/widgets/map/QFrontLine.py +++ b/qt_ui/widgets/map/QFrontLine.py @@ -14,8 +14,10 @@ from PySide2.QtWidgets import ( import qt_ui.uiconstants as const from qt_ui.dialogs import Dialog +from qt_ui.models import GameModel +from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog -from theater.missiontarget import MissionTarget +from theater import FrontLine class QFrontLine(QGraphicsLineItem): @@ -26,9 +28,10 @@ class QFrontLine(QGraphicsLineItem): """ def __init__(self, x1: float, y1: float, x2: float, y2: float, - mission_target: MissionTarget) -> None: + mission_target: FrontLine, game_model: GameModel) -> None: super().__init__(x1, y1, x2, y2) self.mission_target = mission_target + self.game_model = game_model self.new_package_dialog: Optional[QNewPackageDialog] = None self.setAcceptHoverEvents(True) @@ -55,6 +58,14 @@ class QFrontLine(QGraphicsLineItem): new_package_action.triggered.connect(self.open_new_package_dialog) menu.addAction(new_package_action) + cheat_forward = QAction(f"CHEAT: Advance Frontline") + cheat_forward.triggered.connect(self.cheat_forward) + menu.addAction(cheat_forward) + + cheat_backward = QAction(f"CHEAT: Retreat Frontline") + cheat_backward.triggered.connect(self.cheat_backward) + menu.addAction(cheat_backward) + menu.exec_(event.screenPos()) @property @@ -80,3 +91,16 @@ class QFrontLine(QGraphicsLineItem): def open_new_package_dialog(self) -> None: """Opens the dialog for planning a new mission package.""" Dialog.open_new_package_dialog(self.mission_target) + + def cheat_forward(self) -> None: + self.mission_target.control_point_a.base.affect_strength(0.1) + self.mission_target.control_point_b.base.affect_strength(-0.1) + self.game_model.game.initialize_turn() + GameUpdateSignal.get_instance().updateGame(self.game_model.game) + + def cheat_backward(self) -> None: + self.mission_target.control_point_a.base.affect_strength(-0.1) + self.mission_target.control_point_b.base.affect_strength(0.1) + self.game_model.game.initialize_turn() + GameUpdateSignal.get_instance().updateGame(self.game_model.game) + \ No newline at end of file From c1f88b4a5fc8de6c371c5174a068624aaeee42f2 Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Sun, 15 Nov 2020 21:22:13 -0600 Subject: [PATCH 10/17] frontline refactoring `FrontLine` is tightly coupled with `ConflictTheater`. Moved into the same module to prevent circular imports. Moved `ConflictTheater.frontline_data` from class var to instance var to allow save games to have different versions of frontlines. --- game/game.py | 3 +- gen/armor.py | 14 +- gen/briefinggen.py | 2 +- gen/conflictgen.py | 13 +- gen/flights/ai_flight_planner.py | 2 +- gen/groundobjectsgen.py | 4 +- gen/visualgen.py | 2 +- .../QPredefinedWaypointSelectionComboBox.py | 2 +- theater/__init__.py | 1 - theater/conflicttheater.py | 233 ++++++++++++++++- theater/frontline.py | 237 +----------------- theater/start_generator.py | 1 - 12 files changed, 252 insertions(+), 262 deletions(-) diff --git a/game/game.py b/game/game.py index 9e09621f..a78ec5e4 100644 --- a/game/game.py +++ b/game/game.py @@ -370,7 +370,8 @@ class Game: # By default, use the existing frontline conflict position for front_line in self.theater.conflicts(): position = Conflict.frontline_position(front_line.control_point_a, - front_line.control_point_b) + front_line.control_point_b, + self.theater) points.append(position[0]) points.append(front_line.control_point_a.position) points.append(front_line.control_point_b.position) diff --git a/gen/armor.py b/gen/armor.py index f7875c72..8c6c6d6b 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -1,7 +1,8 @@ +from __future__ import annotations import logging import random from dataclasses import dataclass -from typing import List +from typing import List, TYPE_CHECKING from dcs import Mission from dcs.action import AITaskPush @@ -36,6 +37,9 @@ from .conflictgen import Conflict from .ground_forces.combat_stance import CombatStance from game.plugins import LuaPluginManager +if TYPE_CHECKING: + from game import Game + SPREAD_DISTANCE_FACTOR = 0.1, 0.3 SPREAD_DISTANCE_SIZE_FACTOR = 0.1 @@ -65,7 +69,7 @@ class JtacInfo: class GroundConflictGenerator: - def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance): + def __init__(self, mission: Mission, conflict: Conflict, game: Game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance): self.mission = mission self.conflict = conflict self.enemy_planned_combat_groups = enemy_planned_combat_groups @@ -93,7 +97,7 @@ class GroundConflictGenerator: if combat_width < 35000: combat_width = 35000 - position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp) + position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater) # Create player groups at random position for group in self.player_planned_combat_groups: @@ -114,6 +118,8 @@ class GroundConflictGenerator: player_groups.append((g,group)) self.gen_infantry_group_for_group(g, True, self.mission.country(self.game.player_country), self.conflict.heading + 90) + else: + logging.warning(f"Unable to get valid position for {group}") # Create enemy groups at random position for group in self.enemy_planned_combat_groups: @@ -454,7 +460,7 @@ class GroundConflictGenerator: def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline): i = 0 - while i < 25: # 25 attempt for valid position + while i < 1000: # 25 attempt for valid position heading_diff = -90 if isplayer else 90 shifted = conflict_position[0].point_from_heading(self.conflict.heading, random.randint((int)(-combat_width / 2), (int)(combat_width / 2))) diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 062ee8b1..b35a587d 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -6,7 +6,7 @@ import os import random import logging from dataclasses import dataclass -from theater.frontline import FrontLine +from theater import FrontLine from typing import List, Dict, TYPE_CHECKING from jinja2 import Environment, FileSystemLoader, select_autoescape diff --git a/gen/conflictgen.py b/gen/conflictgen.py index d7d90ea9..6a5a8e07 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -5,8 +5,7 @@ from typing import Tuple from dcs.country import Country from dcs.mapping import Point -from theater import ConflictTheater, ControlPoint -from theater.frontline import FrontLine +from theater import ConflictTheater, ControlPoint, FrontLine AIR_DISTANCE = 40000 @@ -136,8 +135,8 @@ class Conflict: return from_cp.has_frontline and to_cp.has_frontline @staticmethod - def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]: - frontline = FrontLine(from_cp, to_cp) + def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]: + frontline = FrontLine(from_cp, to_cp, theater) attack_heading = frontline.attack_heading position = frontline.position return position, _opposite_heading(attack_heading) @@ -160,7 +159,7 @@ class Conflict: return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length """ - frontline = cls.frontline_position(from_cp, to_cp) + frontline = cls.frontline_position(from_cp, to_cp, theater) center_position, heading = frontline left_position, right_position = None, None @@ -210,7 +209,7 @@ class Conflict: @classmethod def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: pos = initial - for _ in range(0, int(max_distance), 500): + for _ in range(0, int(max_distance), 100): if theater.is_on_land(pos): return pos @@ -477,7 +476,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(from_cp, to_cp) + frontline_position, heading = cls.frontline_position(from_cp, to_cp, theater) 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/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index ce68be2d..41a4957f 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -321,7 +321,7 @@ class ObjectiveFinder: continue if Conflict.has_frontline_between(cp, connected): - yield FrontLine(cp, connected) + yield FrontLine(cp, connected, self.game.theater) def vulnerable_control_points(self) -> Iterator[ControlPoint]: """Iterates over friendly CPs that are vulnerable to enemy CPs. diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index e47acce6..78cf29d7 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -355,7 +355,7 @@ class GroundObjectsGenerator: """ FARP_CAPACITY = 4 - def __init__(self, mission: Mission, conflict: Conflict, game, + def __init__(self, mission: Mission, conflict: Conflict, game: Game, radio_registry: RadioRegistry, tacan_registry: TacanRegistry): self.m = mission self.conflict = conflict @@ -370,7 +370,7 @@ class GroundObjectsGenerator: center = self.conflict.center heading = self.conflict.heading - 90 else: - center, heading = self.conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp) + center, heading = self.conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater) heading -= 90 initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE) diff --git a/gen/visualgen.py b/gen/visualgen.py index c187ec90..c2636ea6 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(from_cp, to_cp) + frontline = Conflict.frontline_position(from_cp, to_cp, self.game.theater) if not frontline: continue diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index a3f8f029..c339ffb1 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(cp, ecp)[0] + pos = Conflict.frontline_position(cp, ecp, self.game.theater)[0] wpt = FlightWaypoint( FlightWaypointType.CUSTOM, pos.x, diff --git a/theater/__init__.py b/theater/__init__.py index 209a6646..8fb31434 100644 --- a/theater/__init__.py +++ b/theater/__init__.py @@ -1,5 +1,4 @@ from .base import * from .conflicttheater import * from .controlpoint import * -from .frontline import FrontLine from .missiontarget import MissionTarget diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 10ce735d..863e2add 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -1,6 +1,11 @@ from __future__ import annotations -from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING +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 ( @@ -13,9 +18,10 @@ from dcs.terrain import ( ) from dcs.terrain.terrain import Terrain -from .controlpoint import ControlPoint +from .controlpoint import ControlPoint, MissionTarget from .landmap import Landmap, load_landmap, poly_contains -from .frontline import FrontLine, ComplexFrontLine + +Numeric = Union[int, float] SIZE_TINY = 150 SIZE_SMALL = 600 @@ -54,6 +60,17 @@ 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 @@ -61,15 +78,15 @@ 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 """ daytime_map: Dict[str, Tuple[int, int]] + frontline_data: Optional[Dict[str, ComplexFrontLine]] = None def __init__(self): self.controlpoints: List[ControlPoint] = [] - ConflictTheater.frontline_data = FrontLine.load_json_frontlines(self) + self.frontline_data = FrontLine.load_json_frontlines(self) """ self.land_poly = geometry.Polygon(self.landmap[0][0]) for x in self.landmap[1]: @@ -130,7 +147,7 @@ class ConflictTheater: 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) + yield FrontLine(cp, connected_point, self) def enemy_points(self) -> List[ControlPoint]: return [point for point in self.controlpoints if not point.captured] @@ -280,4 +297,206 @@ class SyriaTheater(ConflictTheater): "day": (8, 16), "dusk": (16, 18), "night": (0, 5), - } \ No newline at end of file + } + +@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}" + + @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/theater/frontline.py b/theater/frontline.py index 9cbf7608..76e7aac8 100644 --- a/theater/frontline.py +++ b/theater/frontline.py @@ -1,235 +1,2 @@ -"""Battlefield front lines.""" -from __future__ import annotations - - -import logging -import json -from dataclasses import dataclass -from pathlib import Path -from itertools import tee -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. -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) - - -@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. - """ - - theater: ConflictTheater - - def __init__( - self, - control_point_a: ControlPoint, - control_point_b: ControlPoint, - ) -> None: - self.control_point_a = control_point_a - self.control_point_b = control_point_b - self.segments: List[FrontLineSegment] = [] - self._build_segments() - self.name = f"Front line {control_point_a}/{control_point_b}" - - @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 - ) - ) - - @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"]], - ) - for frontline in data - } - except OSError: - logging.warning( - f"Unable to load preset frontlines for {theater.terrain.name}" - ) - return None +"""Only here to keep compatibility for save games generated in version 2.2.0""" +from theater.conflicttheater import * \ No newline at end of file diff --git a/theater/start_generator.py b/theater/start_generator.py index b5ff0a89..f074218d 100644 --- a/theater/start_generator.py +++ b/theater/start_generator.py @@ -40,7 +40,6 @@ from theater.theatergroundobject import ( LhaGroundObject, MissileSiteGroundObject, ShipGroundObject, ) -from theater.frontline import FrontLine GroundObjectTemplates = Dict[str, Dict[str, Any]] From bd1457c435e0549624b340d79dbd94996a960f2c Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Sun, 15 Nov 2020 21:50:41 -0600 Subject: [PATCH 11/17] Complete Caucasus Full Map frontline data. --- resources/frontlines/caucasus.json | 2 +- .../mizdata/caucasus/caucusus_frontline.miz | Bin 12931 -> 30742 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/frontlines/caucasus.json b/resources/frontlines/caucasus.json index 33a3e29e..2bf1b5e3 100644 --- a/resources/frontlines/caucasus.json +++ b/resources/frontlines/caucasus.json @@ -1 +1 @@ -{"28|26": {"points": [[-83519.432788948, 834083.89603137], [-76119.776928765, 743854.15425227], [-69364.629004892, 719860.24185348], [-66223.861015868, 712472.15322055], [-55063.838612323, 701790.55845189], [-51581.840958501, 704161.97484394]], "start_cp": 28}, "32|27": {"points": [[-148553.72718928, 843792.03708555], [-114733.18523211, 848507.87882073], [-93593.960629467, 841634.68667249], [-83721.151431284, 834200.8738046]], "start_cp": 32}, "32|28": {"points": [[-148554.87082704, 843689.04452871], [-114733.18523211, 845562.62834108], [-110339.43457942, 812629.20979521], [-111707.66520572, 785946.43211532], [-119336.17607842, 776106.17954928], [-124839.73032662, 760747.06391035]], "start_cp": 32}, "27|26": {"points": [[-124831.33530722, 760646.27221205], [-100762.17095534, 753347.75031825], [-86615.899059596, 729586.43747856], [-70460.285574716, 694513.4327179], [-50443.346691892, 703139.51687735]], "start_cp": 27}, "26|16": {"points": [[-51128.273849964, 705442.60983569], [-17096.829383992, 583922.76901062], [-9413.0964883513, 489204.29454645], [-26751.202695677, 458085.37842522]], "start_cp": 26}, "16|17": {"points": [[-26290.077082534, 457532.52869133], [-12087.691792587, 444620.30900361], [-9183.8957732278, 411618.86444668], [-19095.485460604, 393640.78914313], [-12512.173613316, 386130.07819866], [-6723.0801746587, 294506.97247323], [8289.1802310393, 267690.0671157], [-1709.4806012567, 249581.13338705], [-20329.725545816, 256364.19845868], [-32338.671748957, 281488.17801525], [-44457.480224214, 298277.60074695], [-50348.762426692, 298378.38229135]], "start_cp": 16}, "17|18": {"points": [[-50428.449268252, 298385.98215495], [-47925.882995864, 299522.84586356], [-47742.307667018, 302423.33605932], [-51670.819704316, 306425.27822816], [-52890.978467907, 309683.60757879], [-52158.285335864, 315248.49431547], [-54476.310454094, 321830.02991902], [-61058.499192715, 325379.38150096], [-61379.264542506, 327677.41312797], [-59644.880081364, 329300.50019054], [-59830.375745657, 332806.36824568], [-63794.637882896, 337172.7521818], [-67470.778941327, 340636.53352785], [-70179.929699705, 341613.85524925], [-69660.171385319, 350494.94296899], [-69388.993134334, 354630.4112965], [-75106.334592587, 354856.39317232], [-76281.440346853, 358652.8886861], [-81388.630740391, 366494.45977706], [-87241.561324136, 371036.69548105], [-89840.352896069, 379895.1850132], [-95874.068980471, 384753.79534334], [-98992.618866791, 390742.31505257], [-105500.89689041, 395510.53263238], [-109975.33803166, 401747.63240502], [-112664.52235392, 406199.47535868], [-118042.89099844, 411623.04037836], [-119433.38420159, 414545.36512974], [-121777.77888263, 415031.75406772], [-125094.95143961, 417580.4321027], [-127809.00171351, 419876.18788994], [-131233.17983685, 423387.91602212], [-134219.60791602, 427162.2941808], [-142354.54855856, 436882.58763511], [-142151.02610079, 439008.26663852], [-144141.02346569, 440738.2075296], [-154337.7160827, 451791.06647059], [-156349.9178749, 457412.78487971], [-161121.11800072, 459238.28753654], [-164180.10148029, 461855.90812217]], "start_cp": 17}} \ No newline at end of file +{"28|26": {"points": [[-83519.432788948, 834083.89603137], [-76119.776928765, 743854.15425227], [-69364.629004892, 719860.24185348], [-66223.861015868, 712472.15322055], [-55063.838612323, 701790.55845189], [-51581.840958501, 704161.97484394]], "start_cp": 28}, "32|27": {"points": [[-148553.72718928, 843792.03708555], [-114733.18523211, 848507.87882073], [-93593.960629467, 841634.68667249], [-83721.151431284, 834200.8738046]], "start_cp": 32}, "32|28": {"points": [[-148554.87082704, 843689.04452871], [-114733.18523211, 845562.62834108], [-110339.43457942, 812629.20979521], [-111707.66520572, 785946.43211532], [-119336.17607842, 776106.17954928], [-124839.73032662, 760747.06391035]], "start_cp": 32}, "27|26": {"points": [[-124831.33530722, 760646.27221205], [-100762.17095534, 753347.75031825], [-86615.899059596, 729586.43747856], [-70460.285574716, 694513.4327179], [-50443.346691892, 703139.51687735]], "start_cp": 27}, "26|16": {"points": [[-51128.273849964, 705442.60983569], [-17096.829383992, 583922.76901062], [-9413.0964883513, 489204.29454645], [-26751.202695677, 458085.37842522]], "start_cp": 26}, "16|17": {"points": [[-26290.077082534, 457532.52869133], [-12087.691792587, 444620.30900361], [-9183.8957732278, 411618.86444668], [-19095.485460604, 393640.78914313], [-12512.173613316, 386130.07819866], [-6723.0801746587, 294506.97247323], [8289.1802310393, 267690.0671157], [-1709.4806012567, 249581.13338705], [-20329.725545816, 256364.19845868], [-32338.671748957, 281488.17801525], [-44457.480224214, 298277.60074695], [-50348.762426692, 298378.38229135]], "start_cp": 16}, "17|18": {"points": [[-50428.449268252, 298385.98215495], [-47925.882995864, 299522.84586356], [-47742.307667018, 302423.33605932], [-51670.819704316, 306425.27822816], [-52890.978467907, 309683.60757879], [-52158.285335864, 315248.49431547], [-54476.310454094, 321830.02991902], [-61058.499192715, 325379.38150096], [-61379.264542506, 327677.41312797], [-59644.880081364, 329300.50019054], [-59830.375745657, 332806.36824568], [-63794.637882896, 337172.7521818], [-67470.778941327, 340636.53352785], [-70179.929699705, 341613.85524925], [-69660.171385319, 350494.94296899], [-69388.993134334, 354630.4112965], [-75106.334592587, 354856.39317232], [-76281.440346853, 358652.8886861], [-81388.630740391, 366494.45977706], [-87241.561324136, 371036.69548105], [-89840.352896069, 379895.1850132], [-95874.068980471, 384753.79534334], [-98992.618866791, 390742.31505257], [-105500.89689041, 395510.53263238], [-109975.33803166, 401747.63240502], [-112664.52235392, 406199.47535868], [-118042.89099844, 411623.04037836], [-119433.38420159, 414545.36512974], [-121777.77888263, 415031.75406772], [-125094.95143961, 417580.4321027], [-127809.00171351, 419876.18788994], [-131233.17983685, 423387.91602212], [-134219.60791602, 427162.2941808], [-142354.54855856, 436882.58763511], [-142151.02610079, 439008.26663852], [-144141.02346569, 440738.2075296], [-154337.7160827, 451791.06647059], [-156349.9178749, 457412.78487971], [-161121.11800072, 459238.28753654], [-164180.10148029, 461855.90812217]], "start_cp": 17}, "18|21": {"points": [[-164264.74921603, 462240.8713078], [-170274.83407271, 474599.20385019], [-172409.88129215, 477254.97986699], [-173104.00129217, 479130.75653371], [-174533.55795887, 482196.45320045], [-175268.99462556, 485708.3698672], [-176186.22462558, 487716.35986724], [-177285.24795894, 488666.6432006], [-178400.79795897, 489162.44320061], [-180541.00129235, 490542.41986731], [-183995.07462577, 493831.22653406], [-185517.42530201, 501467.08245066], [-185474.55938496, 507404.01196323], [-187746.45298904, 511540.57295936], [-190489.87168078, 513490.97218551], [-196791.16148834, 516405.85454547]], "start_cp": 18}, "21|20": {"points": [[-196714.42589455, 516411.96814195], [-197450.59049978, 523168.80433533], [-197732.15105425, 528120.95761683], [-197699.02628313, 533768.73109171], [-198311.83454874, 540807.74495336], [-198842.52672667, 546607.07868341], [-199402.89801799, 549442.89703645], [-200896.04977609, 551418.00206698], [-202121.5817902, 551281.83184319], [-202802.43290916, 551962.68296214], [-202604.3671291, 553881.44520647], [-204919.26093354, 558771.19415168], [-207135.12184795, 559043.53459927], [-209821.38899001, 559390.14971437], [-212321.96946326, 561717.42263007], [-214993.578381, 562726.87596614], [-220482.29198081, 564029.17214026]], "start_cp": 21}, "20|23": {"points": [[-220672.07816602, 564666.16009935], [-225466.53293841, 577226.74074072], [-225867.80020827, 582560.25153588], [-226971.28520037, 588278.31013131], [-229011.0604888, 592424.73858648], [-232087.44289102, 595718.47409321], [-233993.46242283, 598192.95559065], [-234745.0795704, 605985.79430519], [-238229.0345155, 610496.39618843], [-245013.37774879, 611843.03758538], [-252127.56713626, 613492.22847069], [-260588.37132113, 620015.44606012], [-270266.03318718, 624574.33114385], [-274285.71028992, 632588.96571051], [-278888.32106515, 640507.86809234], [-281561.45308744, 647019.85888349]], "start_cp": 20}, "23|24": {"points": [[-281730.7296916, 647253.59774005], [-290982.81972459, 649906.82826413], [-294663.23013322, 650003.68116962], [-298488.96273308, 642238.09370951], [-301465.17238261, 635293.60452728], [-305217.78454941, 628521.64923777], [-309876.19965302, 626882.57725687], [-316044.28631799, 627745.2467205], [-318472.62171129, 628904.60667428], [-317887.74401676, 635471.94764428]], "start_cp": 23}, "24|25": {"points": [[-317873.22860422, 635639.0650959], [-312899.88159494, 646184.05618911], [-306709.82545376, 651773.13600592], [-302953.72342634, 651502.69665994], [-299948.8418044, 651262.30613019], [-297935.57111771, 652704.64930872], [-296162.69096076, 656130.21435772], [-295856.9172118, 663129.98092517], [-295423.18658872, 668374.17845869], [-293700.92883347, 671849.09769727], [-290615.92415898, 672244.90961777], [-288066.42972986, 674165.76158491], [-286820.78633299, 677600.01207161], [-286893.79300875, 679835.68894319], [-286618.90964092, 682436.5085003], [-285244.4928018, 683705.20096718]], "start_cp": 24}, "25|23": {"points": [[-284669.68592925, 683920.85836338], [-284033.62440161, 671710.30738432], [-284533.77033624, 669857.91503381], [-285774.87321108, 669765.29541629], [-288423.79427231, 670358.06096845], [-288982.2616923, 669926.15330961], [-288459.09137937, 667542.82188402], [-285718.97087515, 657267.36992099], [-281630.5138127, 647419.52117057]], "start_cp": 25}, "25|31": {"points": [[-284263.99293597, 684700.54580073], [-272604.87474782, 700374.86510124], [-273331.93020798, 704467.91806214], [-277532.6950889, 709395.73840322], [-281410.32420975, 715562.24582457], [-282763.16813743, 718965.99542922], [-284038.34156377, 723212.87736217], [-285647.59520339, 726629.3333984], [-286742.41427361, 730713.85069882], [-290637.44365803, 734503.60901881], [-294658.79831979, 742062.07144589], [-297037.92437623, 747430.89573254], [-299143.34566511, 751157.49141386], [-299648.64677444, 755747.30982361], [-297543.22548556, 760400.29087204], [-293500.81661091, 766927.09686757], [-290805.87736114, 772043.27059955], [-294728.00259664, 779192.46248204], [-289131.68727782, 794537.19803365], [-284302.60873658, 803382.98676341], [-286153.0033178, 818998.51176593], [-283670.76668445, 834568.9051933], [-290756.42398328, 855871.00866495], [-293497.37680705, 872691.40288022], [-298728.59569045, 876959.26028386], [-302426.32111763, 886345.79406053], [-308456.45796809, 893001.69982944], [-313415.88310948, 898749.18653378], [-318952.43355462, 903032.81497309]], "start_cp": 25}, "31|32": {"points": [[-319413.50770939, 903161.13927527], [-299973.76567703, 879091.99809442], [-288926.34291968, 873766.98136965], [-272951.29274537, 873210.63633871], [-262767.79330839, 883418.21980792], [-255300.09049795, 886097.47196129], [-240022.65268728, 889745.81531906], [-225543.28998612, 892596.08356732], [-216365.42622672, 890999.9333483], [-213743.17943832, 883418.21980792], [-215007.91769269, 872553.78042396], [-217795.79179069, 862875.12317806], [-216059.9456542, 857983.19315703], [-212483.05058506, 853985.48690329], [-205939.33685399, 854213.80611265], [-200950.59208156, 852508.09558384], [-194072.72704603, 852086.25319499], [-191404.28495595, 852694.64158494], [-188982.449823, 852694.64158494], [-186034.22200645, 852522.66532349], [-183530.30158366, 851166.88402139], [-180342.38338683, 851032.52731578], [-178180.46185106, 851948.59576314], [-175212.40008161, 852779.16448874], [-171877.91093321, 852962.37817822], [-167460.79156926, 854118.55241154], [-163919.58243455, 854146.2181079], [-159410.07392707, 852181.95366599], [-155370.88225779, 848557.74744219], [-151193.36210669, 845265.52957477], [-148897.1093084, 843799.24766743]], "start_cp": 31}} \ No newline at end of file diff --git a/resources/mizdata/caucasus/caucusus_frontline.miz b/resources/mizdata/caucasus/caucusus_frontline.miz index 22795f76de348adc02977320d285bebad2d4a90f..d392b5cea94a7b4c2b2fdb4f126ffefb95c788d8 100644 GIT binary patch literal 30742 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¨b;hWvD*m&MUCWLD-VX6FdS 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 Date: Sun, 15 Nov 2020 21:53:44 -0600 Subject: [PATCH 12/17] bezier frontline display Start on refactoring and cleanup of QLiberationMap.py --- qt_ui/widgets/map/QLiberationMap.py | 212 ++++++++++++++++------------ 1 file changed, 125 insertions(+), 87 deletions(-) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 4d61507e..b113eb17 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -2,6 +2,7 @@ from __future__ import annotations import datetime import logging +import math from typing import List, Optional, Tuple from PySide2.QtCore import QPointF, Qt @@ -39,13 +40,40 @@ 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.frontline import FrontLine +from theater.conflicttheater import FrontLine from theater.theatergroundobject import ( EwrGroundObject, MissileSiteGroundObject, TheaterGroundObject, ) +def binomial(i, n): + """Binomial coefficient""" + return math.factorial(n) / float( + math.factorial(i) * math.factorial(n - i)) + + +def bernstein(t, i, n): + """Bernstein polynom""" + return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i)) + + +def bezier(t, points): + """Calculate coordinate of a point in the bezier curve""" + n = len(points) - 1 + x = y = 0 + for i, pos in enumerate(points): + bern = bernstein(t, i, n) + x += pos[0] * bern + y += pos[1] * bern + return x, y + + +def bezier_curve_range(n, points): + """Range of points in a curve bezier""" + for i in range(n): + t = i / float(n - 1) + yield bezier(t, points) class QLiberationMap(QGraphicsView): WAYPOINT_SIZE = 4 @@ -190,6 +218,67 @@ class QLiberationMap(QGraphicsView): return detection_range, threat_range + def display_culling(self, scene: QGraphicsScene) -> None: + """Draws the culling distance rings on the map""" + culling_points = self.game_model.game.get_culling_points() + culling_distance = self.game_model.game.settings.perf_culling_distance + for point in culling_points: + culling_distance_point = Point(point.x + culling_distance*1000, point.y + culling_distance*1000) + distance_point = self._transform_point(culling_distance_point) + transformed = self._transform_point(point) + diameter = distance_point[0] - transformed[0] + scene.addEllipse(transformed[0]-diameter/2, transformed[1]-diameter/2, diameter, diameter, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"]) + + @staticmethod + def ground_object_display_options(ground_object: TheaterGroundObject, cp: ControlPoint) -> Tuple[bool, bool]: + is_missile = isinstance(ground_object, MissileSiteGroundObject) + is_aa = ground_object.category == "aa" and not is_missile + is_ewr = isinstance(ground_object, EwrGroundObject) + is_display_type = is_aa or is_ewr + should_display = ((DisplayOptions.sam_ranges and cp.captured) + or + (DisplayOptions.enemy_sam_ranges and not cp.captured)) + return is_display_type, should_display + + def draw_threat_range(self, scene: QGraphicsScene, ground_object: TheaterGroundObject, cp: ControlPoint) -> None: + go_pos = self._transform_point(ground_object.position) + detection_range, threat_range = self.aa_ranges( + ground_object + ) + if threat_range: + threat_pos = self._transform_point(Point(ground_object.position.x+threat_range, + ground_object.position.y+threat_range)) + threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos)) + + # Add threat range circle + scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6, + threat_radius, threat_radius, self.threat_pen(cp.captured)) + + if detection_range and DisplayOptions.detection_range: + # Add detection range circle + detection_pos = self._transform_point(Point(ground_object.position.x+detection_range, + ground_object.position.y+detection_range)) + detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos)) + scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6, + detection_radius, detection_radius, self.detection_pen(cp.captured)) + + def draw_ground_objects(self, scene: QGraphicsScene, cp: ControlPoint) -> None: + added_objects = [] + for ground_object in cp.ground_objects: + if ground_object.obj_name in added_objects: + continue + + go_pos = self._transform_point(ground_object.position) + if not ground_object.airbase_group: + buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name) + scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings)) + + is_display_type, should_display = self.ground_object_display_options(ground_object, cp) + + if is_display_type and should_display: + self.draw_threat_range(scene, ground_object, cp) + added_objects.append(ground_object.obj_name) + def reload_scene(self): scene = self.scene() scene.clear() @@ -200,20 +289,13 @@ class QLiberationMap(QGraphicsView): self.addBackground() # Uncomment below to help set up theater reference points - #for i, r in enumerate(self.game.theater.reference_points.items()): - # text = scene.addText(str(r), font=QFont("Trebuchet MS", 10, weight=5, italic=False)) - # text.setPos(0, i * 24) + # for i, r in enumerate(self.game.theater.reference_points.items()): + # text = scene.addText(str(r), font=QFont("Trebuchet MS", 10, weight=5, italic=False)) + # text.setPos(0, i * 24) # Display Culling if DisplayOptions.culling and self.game.settings.perf_culling: - culling_points = self.game_model.game.get_culling_points() - culling_distance = self.game_model.game.settings.perf_culling_distance - for point in culling_points: - culling_distance_point = Point(point.x + culling_distance*1000, point.y + culling_distance*1000) - distance_point = self._transform_point(culling_distance_point) - transformed = self._transform_point(point) - diameter = distance_point[0] - transformed[0] - scene.addEllipse(transformed[0]-diameter/2, transformed[1]-diameter/2, diameter, diameter, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"]) + self.display_culling() for cp in self.game.theater.controlpoints: @@ -231,45 +313,7 @@ class QLiberationMap(QGraphicsView): pen = QPen(brush=CONST.COLORS[enemyColor]) brush = CONST.COLORS[enemyColor+"_transparent"] - added_objects = [] - for ground_object in cp.ground_objects: - if ground_object.obj_name in added_objects: - continue - - go_pos = self._transform_point(ground_object.position) - if not ground_object.airbase_group: - buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name) - scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings)) - - is_missile = isinstance(ground_object, MissileSiteGroundObject) - is_aa = ground_object.category == "aa" and not is_missile - is_ewr = isinstance(ground_object, EwrGroundObject) - is_display_type = is_aa or is_ewr - should_display = ((DisplayOptions.sam_ranges and cp.captured) - or - (DisplayOptions.enemy_sam_ranges and not cp.captured)) - - if is_display_type and should_display: - detection_range, threat_range = self.aa_ranges( - ground_object - ) - if threat_range: - threat_pos = self._transform_point(Point(ground_object.position.x+threat_range, - ground_object.position.y+threat_range)) - threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos)) - - # Add threat range circle - scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6, - threat_radius, threat_radius, self.threat_pen(cp.captured)) - if detection_range: - # Add detection range circle - detection_pos = self._transform_point(Point(ground_object.position.x+detection_range, - ground_object.position.y+detection_range)) - detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos)) - if DisplayOptions.detection_range: - scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6, - detection_radius, detection_radius, self.detection_pen(cp.captured)) - added_objects.append(ground_object.obj_name) + self.draw_ground_objects(scene, cp) for cp in self.game.theater.enemy_points(): if DisplayOptions.lines: @@ -400,44 +444,45 @@ class QLiberationMap(QGraphicsView): flight_path_pen )) + def draw_bezier_frontline(self, scene: QGraphicsScene, pen:QPen, frontline: FrontLine) -> None: + """ + Thanks to Alquimista for sharing a python implementation of the bezier algorithm this is adapted from. + https://gist.github.com/Alquimista/1274149#file-bezdraw-py + """ + bezier_fixed_points = [] + for segment in frontline.segments: + bezier_fixed_points.append(self._transform_point(segment.point_a)) + bezier_fixed_points.append(self._transform_point(segment.point_b)) + + old_point = bezier_fixed_points[0] + for point in bezier_curve_range(int(len(bezier_fixed_points) * 2), bezier_fixed_points): + scene.addLine(old_point[0], old_point[1], point[0], point[1], pen=pen) + old_point = point + def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor): scene = self.scene() - pos = self._transform_point(cp.position) for connected_cp in cp.connected_points: pos2 = self._transform_point(connected_cp.position) if not cp.captured: color = CONST.COLORS["dark_"+enemyColor] - elif cp.captured: - color = CONST.COLORS["dark_"+playerColor] else: - color = CONST.COLORS["dark_"+enemyColor] - + color = CONST.COLORS["dark_"+playerColor] pen = QPen(brush=color) pen.setColor(color) pen.setWidth(6) - frontline = FrontLine(cp, connected_cp) + frontline = FrontLine(cp, connected_cp, self.game.theater) 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 = frontline.position - h = frontline.attack_heading - pos2 = self._transform_point(posx) - 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) - - p1 = point_from_heading(pos2[0], pos2[1], h+180, 25) - p2 = point_from_heading(pos2[0], pos2[1], h, 25) - scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1], - FrontLine(cp, connected_cp))) + posx = frontline.position + h = frontline.attack_heading + pos2 = self._transform_point(posx) + self.draw_bezier_frontline(scene, pen, frontline) + p1 = point_from_heading(pos2[0], pos2[1], h+180, 25) + p2 = point_from_heading(pos2[0], pos2[1], h, 25) + scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1], + frontline, self.game_model)) else: - 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) + self.draw_bezier_frontline(scene, pen, frontline) def wheelEvent(self, event: QWheelEvent): @@ -460,7 +505,7 @@ class QLiberationMap(QGraphicsView): #print(self.factorized, factor, self._zoom) - def _transform_point(self, p: Point, treshold=30) -> (int, int): + def _transform_point(self, p: Point, treshold=30) -> Tuple[int, int]: point_a = list(self.game.theater.reference_points.keys())[0] point_a_img = self.game.theater.reference_points[point_a] @@ -510,18 +555,11 @@ class QLiberationMap(QGraphicsView): return CONST.COLORS[f"{name}_transparent"] def threat_pen(self, player: bool) -> QPen: - if player: - color = "blue" - else: - color = "red" - qpen = QPen(CONST.COLORS[color]) - return qpen + color = "blue" if player else "red" + return QPen(CONST.COLORS[color]) def detection_pen(self, player: bool) -> QPen: - if player: - color = "purple" - else: - color = "yellow" + color = "purple" if player else "yellow" qpen = QPen(CONST.COLORS[color]) qpen.setStyle(Qt.DotLine) return qpen From 253e8a209cc78d62abd1dd77af7389225b8b1587 Mon Sep 17 00:00:00 2001 From: walterroach Date: Mon, 16 Nov 2020 17:02:04 -0600 Subject: [PATCH 13/17] fix return statement --- theater/conflicttheater.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/theater/conflicttheater.py b/theater/conflicttheater.py index 3be055e9..78f3c052 100644 --- a/theater/conflicttheater.py +++ b/theater/conflicttheater.py @@ -352,8 +352,8 @@ class FrontLine(MissionTarget): def is_friendly(self, to_player: bool) -> bool: """Returns True if the objective is in friendly territory.""" - raise False - + return False + @property def position(self): """ From ecd073e31def6a6373dd124aa924aa5dba9aa46e Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Mon, 16 Nov 2020 22:01:49 -0600 Subject: [PATCH 14/17] typing and comment cleanup --- gen/armor.py | 7 +++---- qt_ui/widgets/map/QLiberationMap.py | 10 +++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/gen/armor.py b/gen/armor.py index 8c6c6d6b..0e504021 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -460,7 +460,7 @@ class GroundConflictGenerator: def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline): i = 0 - while i < 1000: # 25 attempt for valid position + while i < 1000: heading_diff = -90 if isplayer else 90 shifted = conflict_position[0].point_from_heading(self.conflict.heading, random.randint((int)(-combat_width / 2), (int)(combat_width / 2))) @@ -468,9 +468,8 @@ class GroundConflictGenerator: if self.conflict.theater.is_on_land(final_position): return final_position - else: - i = i + 1 - continue + i += 1 + continue return None def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading=0): diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index b113eb17..04e2d5d6 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -3,7 +3,7 @@ from __future__ import annotations import datetime import logging import math -from typing import List, Optional, Tuple +from typing import Iterable, List, Optional, Tuple, Iterator from PySide2.QtCore import QPointF, Qt from PySide2.QtGui import ( @@ -47,18 +47,18 @@ from theater.theatergroundobject import ( TheaterGroundObject, ) -def binomial(i, n): +def binomial(i: int, n: int) -> float: """Binomial coefficient""" return math.factorial(n) / float( math.factorial(i) * math.factorial(n - i)) -def bernstein(t, i, n): +def bernstein(t: float, i: int, n: int) -> float: """Bernstein polynom""" return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i)) -def bezier(t, points): +def bezier(t: float, points: Iterable[Tuple[float, float]]) -> Tuple[float, float]: """Calculate coordinate of a point in the bezier curve""" n = len(points) - 1 x = y = 0 @@ -69,7 +69,7 @@ def bezier(t, points): return x, y -def bezier_curve_range(n, points): +def bezier_curve_range(n: int, points: Iterable[Tuple[float, float]]) -> Iterator[Tuple[float, float]]: """Range of points in a curve bezier""" for i in range(n): t = i / float(n - 1) From f6371d2ef18a9e69720de4dbeb081c4b0854ba75 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Mon, 16 Nov 2020 19:15:11 -0800 Subject: [PATCH 15/17] Further improve split/join positioning. --- gen/flights/flightplan.py | 62 +++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index b16732d0..7181cd9f 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -655,8 +655,8 @@ class FlightPlanBuilder: def regenerate_package_waypoints(self) -> None: ingress_point = self._ingress_point() egress_point = self._egress_point() - join_point = self._join_point(ingress_point) - split_point = self._split_point(egress_point) + join_point = self._rendezvous_point(ingress_point) + split_point = self._rendezvous_point(egress_point) from gen.ato import PackageWaypoints self.package.waypoints = PackageWaypoints( @@ -1106,31 +1106,41 @@ class FlightPlanBuilder: land=land ) - def _join_point(self, ingress_point: Point) -> Point: - ingress_distance = self._distance_to_package_airfield(ingress_point) - if ingress_distance < self.doctrine.join_distance: - # If the ingress point is close to the origin, plan the join point - # farther back. - return ingress_point.point_from_heading( - self.package.target.position.heading_between_point( - self.package_airfield().position), - self.doctrine.join_distance) - heading = self._heading_to_package_airfield(ingress_point) - return ingress_point.point_from_heading(heading, - -self.doctrine.join_distance) + def _retreating_rendezvous_point(self, attack_transition: Point) -> Point: + """Creates a rendezvous point that retreats from the origin airfield.""" + return attack_transition.point_from_heading( + self.package.target.position.heading_between_point( + self.package_airfield().position), + self.doctrine.join_distance) - def _split_point(self, egress_point: Point) -> Point: - egress_distance = self._distance_to_package_airfield(egress_point) - if egress_distance < self.doctrine.split_distance: - # If the ingress point is close to the origin, plan the split point - # farther back. - return egress_point.point_from_heading( - self.package.target.position.heading_between_point( - self.package_airfield().position), - self.doctrine.split_distance) - heading = self._heading_to_package_airfield(egress_point) - return egress_point.point_from_heading(heading, - -self.doctrine.split_distance) + def _advancing_rendezvous_point(self, attack_transition: Point) -> Point: + """Creates a rendezvous point that advances toward the target.""" + heading = self._heading_to_package_airfield(attack_transition) + return attack_transition.point_from_heading(heading, + -self.doctrine.join_distance) + + def _rendezvous_should_retreat(self, attack_transition: Point) -> bool: + transition_target_distance = attack_transition.distance_to_point( + self.package.target.position + ) + origin_target_distance = self._distance_to_package_airfield( + self.package.target.position + ) + + # If the origin point is closer to the target than the ingress point, + # the rendezvous point should be positioned in a position that retreats + # from the origin airfield. + return origin_target_distance < transition_target_distance + + def _rendezvous_point(self, attack_transition: Point) -> Point: + """Returns the position of the rendezvous point. + + Args: + attack_transition: The ingress or egress point for this rendezvous. + """ + if self._rendezvous_should_retreat(attack_transition): + return self._retreating_rendezvous_point(attack_transition) + return self._advancing_rendezvous_point(attack_transition) def _ingress_point(self) -> Point: heading = self._target_heading_to_package_airfield() From 0b6b40a3582b53f888aeda87f0a208377b0a39bd Mon Sep 17 00:00:00 2001 From: Khopa Date: Tue, 17 Nov 2020 00:06:10 +0100 Subject: [PATCH 16/17] Pydcs repository update : Fixed issue with clipped wing variant of the spitfire not being seen as a flyable module --- changelog.md | 1 + pydcs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 4f71438e..4909c7d3 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ ## Fixes : * **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned. +* **[Flight Planner]** Spitfire clipped wing variant was not seen as a flyable module # 2.2.0 diff --git a/pydcs b/pydcs index fa9195fb..ef40dbfc 160000 --- a/pydcs +++ b/pydcs @@ -1 +1 @@ -Subproject commit fa9195fbccbf96775d108a22c13c3ee2375e4c0b +Subproject commit ef40dbfc98ddec13329d8c10ee14a0626997d68a From a52dc43c9e2dd404b5d14fa0f991ccfe43261b06 Mon Sep 17 00:00:00 2001 From: Khopa Date: Mon, 16 Nov 2020 23:57:12 +0100 Subject: [PATCH 17/17] MAde it possible to setup liveries in faction files. --- changelog.md | 1 + game/factions/faction.py | 11 +++++++++++ gen/aircraft.py | 11 +++++++++++ resources/factions/us_aggressors.json | 21 ++++++++++++++++++++- resources/factions/usa_2005.json | 22 +++++++++++++++++++++- resources/factions/usn_1985.json | 16 +++++++++++++++- 6 files changed, 79 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 4909c7d3..03821d5a 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ # Features/Improvements * **[Flight Planner]** Added fighter sweep missions. * **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package. +* **[Modding]** Possible to setup liveries overrides for factions # 2.2.1 diff --git a/game/factions/faction.py b/game/factions/faction.py index 5a056bf1..b0caf4bb 100644 --- a/game/factions/faction.py +++ b/game/factions/faction.py @@ -105,6 +105,9 @@ class Faction: # List of available buildings for this faction building_set: List[str] = field(default_factory=list) + # List of default livery overrides + liveries_overrides: Dict[UnitType, List[str]] = field(default_factory=dict) + @classmethod def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: @@ -183,6 +186,14 @@ class Faction: else: faction.building_set = DEFAULT_AVAILABLE_BUILDINGS + # Load liveries override + faction.liveries_overrides = {} + liveries_overrides = json.get("liveries_overrides", {}) + for k, v in liveries_overrides.items(): + k = load_aircraft(k) + if k is not None: + faction.liveries_overrides[k] = [s.lower() for s in v] + return faction @property diff --git a/gen/aircraft.py b/gen/aircraft.py index 5aa6ba55..03a9e619 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -714,6 +714,17 @@ class AircraftConflictGenerator: for unit_instance in group.units: unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type] + # Override livery by faction file data + if flight.from_cp.captured: + faction = self.game.player_faction + else: + faction = self.game.enemy_faction + + if unit_type in faction.liveries_overrides: + livery = random.choice(faction.liveries_overrides[unit_type]) + for unit_instance in group.units: + unit_instance.livery_id = livery + for idx in range(0, min(len(group.units), flight.client_count)): unit = group.units[idx] if self.use_client: diff --git a/resources/factions/us_aggressors.json b/resources/factions/us_aggressors.json index 9e2b41a2..e3bf8108 100644 --- a/resources/factions/us_aggressors.json +++ b/resources/factions/us_aggressors.json @@ -61,5 +61,24 @@ "OliverHazardPerryGroupGenerator" ], "has_jtac": true, - "jtac_unit": "MQ_9_Reaper" + "jtac_unit": "MQ_9_Reaper", + "liveries_overrides": { + "FA_18C_hornet": [ + "NSAWC brown splinter", + "NAWDC black", + "VFC-12" + ], + "F_15C": [ + "65th Aggressor SQN (WA) MiG", + "65th Aggressor SQN (WA) MiG", + "65th Aggressor SQN (WA) SUPER_Flanker" + ], + "F_16C_50": [ + "usaf 64th aggressor sqn - shark", + "usaf 64th aggressor sqn-splinter", + "64th_aggressor_squadron_ghost" + ], "F_14B": [ + "vf-74 adversary" + ] + } } \ No newline at end of file diff --git a/resources/factions/usa_2005.json b/resources/factions/usa_2005.json index 59c3a4f2..f10e36bb 100644 --- a/resources/factions/usa_2005.json +++ b/resources/factions/usa_2005.json @@ -86,5 +86,25 @@ "OliverHazardPerryGroupGenerator" ], "has_jtac": true, - "jtac_unit": "MQ_9_Reaper" + "jtac_unit": "MQ_9_Reaper", + "liveries_overrides": { + "FA_18C_hornet": [ + "VFA-37", + "VFA-106", + "VFA-113", + "VFA-122", + "VFA-131", + "VFA-192", + "VFA-34", + "VFA-83", + "VFA-87", + "VFA-97", + "VMFA-122", + "VMFA-132", + "VMFA-251", + "VMFA-312", + "VMFA-314", + "VMFA-323" + ] + } } \ No newline at end of file diff --git a/resources/factions/usn_1985.json b/resources/factions/usn_1985.json index 6ca2f2f1..24d0e2ea 100644 --- a/resources/factions/usn_1985.json +++ b/resources/factions/usn_1985.json @@ -69,5 +69,19 @@ "OliverHazardPerryGroupGenerator" ], "requirements": {}, - "doctrine": "coldwar" + "doctrine": "coldwar", + "liveries_overrides": { + "FA_18C_hornet": [ + "VFA-37", + "VFA-106", + "VFA-113", + "VFA-122", + "VFA-131", + "VFA-192", + "VFA-34", + "VFA-83", + "VFA-87", + "VFA-97" + ] + } } \ No newline at end of file