diff --git a/game/utils.py b/game/utils.py index 530c559c..772db9a9 100644 --- a/game/utils.py +++ b/game/utils.py @@ -42,4 +42,16 @@ def mps_to_knots(value_in_mps: float) -> int: :arg value_in_mps Meters Per Second """ - return int(value_in_mps * 1.943) \ No newline at end of file + return int(value_in_mps * 1.943) + +def heading_sum(h, a) -> int: + h += a + if h > 360: + return h - 360 + elif h < 0: + return 360 + h + else: + return h + +def opposite_heading(h): + return h+180 \ No newline at end of file diff --git a/gen/armor.py b/gen/armor.py index 085cfdfc..65914798 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging import random from dataclasses import dataclass -from typing import List, TYPE_CHECKING +from typing import List, TYPE_CHECKING, Tuple from dcs import Mission from dcs.action import AITaskPush @@ -25,17 +25,19 @@ from dcs.task import ( from dcs.triggers import Event, TriggerOnce from dcs.unit import Vehicle from dcs.unittype import VehicleType +from dcs.unitgroup import VehicleGroup from game import db from .naming import namegen from gen.ground_forces.ai_ground_planner import ( - CombatGroupRole, + CombatGroup, CombatGroupRole, DISTANCE_FROM_FRONTLINE, ) from .callsigns import callsign_for_support_unit from .conflictgen import Conflict from .ground_forces.combat_stance import CombatStance from game.plugins import LuaPluginManager +from game.utils import heading_sum, opposite_heading if TYPE_CHECKING: from game import Game @@ -69,7 +71,15 @@ class JtacInfo: class GroundConflictGenerator: - def __init__(self, mission: Mission, conflict: Conflict, game: Game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance): + def __init__( + self, + mission: Mission, + conflict: Conflict, + game: Game, + player_planned_combat_groups: List[CombatGroup], + enemy_planned_combat_groups: List[CombatGroup], + player_stance: CombatStance + ): self.mission = mission self.conflict = conflict self.enemy_planned_combat_groups = enemy_planned_combat_groups @@ -102,7 +112,7 @@ class GroundConflictGenerator: ] ) - def _group_point(self, point) -> Point: + def _group_point(self, point: Point) -> Point: distance = random.randint( int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]), int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]), @@ -110,60 +120,19 @@ class GroundConflictGenerator: return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_SIZE_FACTOR) def generate(self): - - player_groups = [] - enemy_groups = [] - - combat_width = self.conflict.distance/2 - if combat_width > 500000: - combat_width = 500000 - if combat_width < 35000: - combat_width = 35000 - position = Conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater) + frontline_vector = Conflict.frontline_vector( + 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: - if group.role == CombatGroupRole.ARTILLERY: - distance_from_frontline = self.get_artilery_group_distance_from_frontline(group) - else: - distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role] - final_position = self.get_valid_position_for_group(position, True, combat_width, distance_from_frontline) - - if final_position is not None: - g = self._generate_group( - side=self.mission.country(self.game.player_country), - unit=group.units[0], - heading=self.conflict.heading+90, - count=len(group.units), - at=final_position) - g.set_skill(self.game.settings.player_skill) - 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}") + player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True) # Create enemy groups at random position - for group in self.enemy_planned_combat_groups: - if group.role == CombatGroupRole.ARTILLERY: - distance_from_frontline = self.get_artilery_group_distance_from_frontline(group) - else: - distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role] - final_position = self.get_valid_position_for_group(position, False, combat_width, distance_from_frontline) - - if final_position is not None: - g = self._generate_group( - side=self.mission.country(self.game.enemy_country), - unit=group.units[0], - heading=self.conflict.heading - 90, - count=len(group.units), - at=final_position) - g.set_skill(self.game.settings.enemy_vehicle_skill) - enemy_groups.append((g, group)) - - self.gen_infantry_group_for_group(g, False, self.mission.country(self.game.enemy_country), self.conflict.heading - 90) - + enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False) + # Plan combat actions for groups self.plan_action_for_groups(self.player_stance, player_groups, enemy_groups, self.conflict.heading + 90, self.conflict.from_cp, self.conflict.to_cp) self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp) @@ -481,21 +450,70 @@ class GroundConflictGenerator: return rg - def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline): + def get_valid_position_for_group( + self, + conflict_position: Point, + combat_width: int, + distance_from_frontline: int, + heading: int, + spawn_heading: int + ): i = 0 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))) - final_position = shifted.point_from_heading(self.conflict.heading + heading_diff, distance_from_frontline) + shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width)) + final_position = shifted.point_from_heading(spawn_heading, distance_from_frontline) if self.conflict.theater.is_on_land(final_position): return final_position i += 1 continue return None + + def _generate_groups(self, groups: List[CombatGroup], frontline_vector: Tuple[Point, int, int], is_player: bool): + """Finds valid positions for planned groups and generates a pydcs group for them""" + positioned_groups = [] + position, heading, combat_width = frontline_vector + spawn_heading = int(heading_sum(heading, -90)) if is_player else int(heading_sum(heading, 90)) + country = self.game.player_country if is_player else self.game.enemy_country + for group in groups: + if group.role == CombatGroupRole.ARTILLERY: + distance_from_frontline = self.get_artilery_group_distance_from_frontline(group) + else: + distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role] - def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading=0): + final_position = self.get_valid_position_for_group( + position, + combat_width, + distance_from_frontline, + heading, + spawn_heading + ) + + if final_position is not None: + g = self._generate_group( + self.mission.country(country), + group.units[0], + len(group.units), + final_position, + heading=opposite_heading(spawn_heading) + ) + g.set_skill(self.game.settings.player_skill) + positioned_groups.append((g,group)) + self.gen_infantry_group_for_group(g, True, self.mission.country(country), opposite_heading(spawn_heading)) + else: + logging.warning(f"Unable to get valid position for {group}") + + return positioned_groups + + def _generate_group( + self, + side: Country, + unit: VehicleType, + count: int, + at: Point, + move_formation: PointAction = PointAction.OffRoad, + heading=0 + ) -> VehicleGroup: if side == self.conflict.attackers_country: cp = self.conflict.from_cp @@ -515,4 +533,4 @@ class GroundConflictGenerator: vehicle: Vehicle = group.units[c] vehicle.player_can_drive = True - return group \ No newline at end of file + return group diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 7f1a5e0a..9bf49f31 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -7,25 +7,11 @@ from dcs.mapping import Point from game.theater.conflicttheater import ConflictTheater, FrontLine from game.theater.controlpoint import ControlPoint +from game.utils import heading_sum, opposite_heading FRONTLINE_LENGTH = 80000 - -def _opposite_heading(h): - return h+180 - - -def _heading_sum(h, a) -> int: - h += a - if h > 360: - return h - 360 - elif h < 0: - return 360 + h - else: - return h - - class Conflict: def __init__(self, theater: ConflictTheater, @@ -37,7 +23,7 @@ class Conflict: defenders_country: Country, position: Point, heading=None, - distance=None, + size=None ): self.attackers_side = attackers_side @@ -50,99 +36,39 @@ class Conflict: self.theater = theater self.position = position self.heading = heading - self.distance = distance - self.size = to_cp.size - - @property - def center(self) -> Point: - return self.position.point_from_heading(self.heading, self.distance / 2) - - @property - def tail(self) -> Point: - return self.position.point_from_heading(self.heading, self.distance) - - @property - def is_vector(self) -> bool: - return self.heading is not None - - @property - def opposite_heading(self) -> int: - return _heading_sum(self.heading, 180) - - def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point: - return Conflict._find_ground_position(at, max_distance, heading, self.theater) + self.size = size @classmethod def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool: return from_cp.has_frontline and to_cp.has_frontline - @staticmethod - def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]: + @classmethod + def frontline_position(cls, 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) - - @classmethod - def flight_frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]: - """Returns the frontline vector without regard for exclusion zones, used in CAS flight plan""" - frontline = cls.frontline_position(from_cp, to_cp, theater) - center_position, heading = frontline - left_position = center_position.point_from_heading(_heading_sum(heading, -90), int(FRONTLINE_LENGTH/2)) - right_position = center_position.point_from_heading(_heading_sum(heading, 90), int(FRONTLINE_LENGTH/2)) - - return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position)) - + position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater) + return position, opposite_heading(attack_heading) @classmethod def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]: """ - probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH) - probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ]) - intersection = probe.intersection(theater.land_poly) - - if isinstance(intersection, geometry.LineString): - intersection = intersection - elif isinstance(intersection, geometry.MultiLineString): - intersection = intersection.geoms[0] - else: - print(intersection) - return None - - return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length + Returns a vector for a valid frontline location avoiding exclusion zones. """ - frontline = cls.frontline_position(from_cp, to_cp, theater) - center_position, heading = frontline - left_position, right_position = None, None - - if not theater.is_on_land(center_position): - pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, -90), theater) - if pos: - right_position = pos - center_position = pos - else: - pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, +90), theater) - if pos: - left_position = pos - center_position = pos - - if left_position is None: - left_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, -90), theater) - - if right_position is None: - right_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, 90), theater) - - return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position)) + center_position, heading = cls.frontline_position(from_cp, to_cp, theater) + left_heading = heading_sum(heading, -90) + right_heading = heading_sum(heading, 90) + left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater) + right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater) + distance = int(left_position.distance_to_point(right_position)) + return left_position, right_heading, distance @classmethod def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): assert cls.has_frontline_between(from_cp, to_cp) position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) - - return cls( + conflict = cls( position=position, heading=heading, - distance=distance, theater=theater, from_cp=from_cp, to_cp=to_cp, @@ -150,50 +76,33 @@ class Conflict: defenders_side=defender_name, attackers_country=attacker, defenders_country=defender, + size=distance ) + return conflict @classmethod - def _extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: + def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: + """Finds a valid ground position in one heading from an initial point""" pos = initial - for offset in range(0, int(max_distance), 500): - new_pos = initial.point_from_heading(heading, offset) - if theater.is_on_land(new_pos): - pos = new_pos - else: + for distance in range(0, int(max_distance), 100): + if not theater.is_on_land(pos): return pos - return pos - - """ - probe_end_point = initial.point_from_heading(heading, max_distance) - probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y)]) - - intersection = probe.intersection(theater.land_poly) - if intersection is geometry.LineString: - return Point(*intersection.xy[1]) - elif intersection is geometry.MultiLineString: - return Point(*intersection.geoms[0].xy[1]) - - return None - """ - - @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), 100): - if theater.is_on_land(pos): - return pos - - pos = pos.point_from_heading(heading, 500) - """ - probe_end_point = initial.point_from_heading(heading, max_distance) - probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ]) - - intersection = probe.intersection(theater.land_poly) - if isinstance(intersection, geometry.LineString): - return Point(*intersection.xy[1]) - elif isinstance(intersection, geometry.MultiLineString): - return Point(*intersection.geoms[0].xy[1]) - """ - + pos = initial.point_from_heading(heading, distance) + if theater.is_on_land(pos): + return pos + logging.error("Didn't find ground position ({})!".format(initial)) + return initial + + @classmethod + def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: + """Finds the nearest ground position along a provided heading and it's inverse""" + pos = initial + for distance in range(0, int(max_distance), 100): + if theater.is_on_land(pos): + return pos + pos = initial.point_from_heading(heading, distance) + if theater.is_on_land(pos): + return pos + pos = initial.point_from_heading(opposite_heading(heading), distance) logging.error("Didn't find ground position ({})!".format(initial)) return initial diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 724bd668..b461d4cd 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -1038,7 +1038,7 @@ class FlightPlanBuilder: if not isinstance(location, FrontLine): raise InvalidObjectiveLocation(flight.flight_type, location) - ingress, heading, distance = Conflict.flight_frontline_vector( + ingress, heading, distance = Conflict.frontline_vector( location.control_points[0], location.control_points[1], self.game.theater ) diff --git a/qt_ui/displayoptions.py b/qt_ui/displayoptions.py index bec194fb..6cbbffc5 100644 --- a/qt_ui/displayoptions.py +++ b/qt_ui/displayoptions.py @@ -58,6 +58,7 @@ class DisplayOptions: waypoint_info = DisplayRule("Waypoint Information", True) culling = DisplayRule("Display Culling Zones", False) flight_paths = FlightPathOptions() + actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False) @classmethod def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]: diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 0937f8c9..d628a0b6 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -476,17 +476,52 @@ class QLiberationMap(QGraphicsView): pen.setWidth(6) frontline = FrontLine(cp, connected_cp, self.game.theater) if cp.captured and not connected_cp.captured and Conflict.has_frontline_between(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)) - + if DisplayOptions.actual_frontline_pos: + self.draw_actual_frontline(frontline, scene, pen) + else: + self.draw_frontline_approximation(frontline, scene, pen) else: self.draw_bezier_frontline(scene, pen, frontline) + + def draw_frontline_approximation(self, frontline: FrontLine, scene: QGraphicsScene, pen: QPen) -> None: + 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 + ) + ) + + def draw_actual_frontline(self, frontline: FrontLine, scene: QGraphicsScene, pen: QPen) -> None: + self.draw_bezier_frontline(scene, pen, frontline) + vector = Conflict.frontline_vector( + frontline.control_point_a, + frontline.control_point_b, + self.game.theater + ) + left_pos = self._transform_point(vector[0]) + right_pos = self._transform_point( + vector[0].point_from_heading(vector[1], vector[2]) + ) + scene.addItem( + QFrontLine( + left_pos[0], + left_pos[1], + right_pos[0], + right_pos[1], + frontline, + self.game_model + ) + ) def wheelEvent(self, event: QWheelEvent):