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..653a9345 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -1,8 +1,9 @@ from __future__ import annotations import logging +from pydcs.dcs.unitgroup import VehicleGroup 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 @@ -29,13 +30,14 @@ from dcs.unittype import VehicleType 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 42daa4c2..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,49 +36,18 @@ 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]: @@ -100,11 +55,10 @@ class Conflict: Returns a vector for a valid frontline location avoiding exclusion zones. """ center_position, heading = cls.frontline_position(from_cp, to_cp, theater) - center_position = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, 90), 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) + 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 @@ -112,11 +66,9 @@ class Conflict: 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, @@ -124,10 +76,12 @@ 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 distance in range(0, int(max_distance), 100): @@ -140,7 +94,7 @@ class Conflict: return initial @classmethod - def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: + 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): @@ -149,6 +103,6 @@ class Conflict: pos = initial.point_from_heading(heading, distance) if theater.is_on_land(pos): return pos - pos = initial.point_from_heading(_opposite_heading(heading), distance) + 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 )