diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index d38e62a6..b7760a04 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -9,6 +9,9 @@ from itertools import tee from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast +from shapely import geometry +from shapely import ops + from dcs import Mission from dcs.countries import ( CombinedJointTaskForcesBlue, @@ -472,6 +475,32 @@ class ConflictTheater: return True + def nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point: + """Returns the nearest point inside a land exclusion zone from point + `extend_dist` determines how far inside the zone the point should be placed""" + if self.is_on_land(point): + return point + point = geometry.Point(point.x, point.y) + nearest_points = [] + for inclusion_zone in self.landmap[0]: + nearest_pair = ops.nearest_points(point, inclusion_zone) + nearest_points.append(nearest_pair[1]) + min_distance = None # type: Optional[geometry.Point] + nearest_point = None # type: Optional[geometry.Point] + for pt in nearest_points: + distance = point.distance(pt) + if not min_distance or distance < min_distance: + min_distance = distance + nearest_point = pt + assert isinstance(nearest_point, geometry.Point) + point = Point(point.x, point.y) + nearest_point = Point(nearest_point.x, nearest_point.y) + new_point = point.point_from_heading( + point.heading_between_point(nearest_point), + point.distance_to_point(nearest_point) + extend_dist + ) + return new_point + def player_points(self) -> List[ControlPoint]: return [point for point in self.controlpoints if point.captured] diff --git a/gen/armor.py b/gen/armor.py index 81931496..f8c92d2c 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -177,7 +177,12 @@ class GroundConflictGenerator: forward_heading: int ) -> None: - infantry_position = group.points[0].position.random_point_within(250, 50) + infantry_position = self.conflict.find_ground_position( + group.points[0].position.random_point_within(250, 50), + 500, + forward_heading, + self.conflict.theater + ) if side == self.conflict.attackers_country: cp = self.conflict.from_cp @@ -228,6 +233,17 @@ class GroundConflictGenerator: heading=forward_heading, move_formation=PointAction.OffRoad) + def _set_reform_waypoint( + self, + dcs_group: VehicleGroup, + forward_heading: int + ) -> None: + """Setting a waypoint close to the spawn position allows the group to reform gracefully + rather than spin + """ + reform_point = dcs_group.position.point_from_heading(forward_heading, 50) + dcs_group.add_waypoint(reform_point) + def _plan_artillery_action( self, stance: CombatStance, @@ -240,6 +256,7 @@ class GroundConflictGenerator: Handles adding the DCS tasks for artillery groups for all combat stances. Returns True if tasking was added, returns False if the stance was not a combat stance. """ + self._set_reform_waypoint(dcs_group, forward_heading) if stance != CombatStance.RETREAT: hold_task = Hold() hold_task.number = 1 @@ -259,10 +276,10 @@ class GroundConflictGenerator: if stance != CombatStance.RETREAT: # Hold position - dcs_group.points[0].tasks.append(Hold()) - retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3)) - dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad) dcs_group.points[1].tasks.append(Hold()) + retreat = self.find_retreat_point(dcs_group, heading_sum(forward_heading, 180), (int)(RETREAT_DISTANCE/3)) + dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad) + dcs_group.points[2].tasks.append(Hold()) dcs_group.add_waypoint(retreat, PointAction.OffRoad) artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id)) @@ -300,6 +317,7 @@ class GroundConflictGenerator: Handles adding the DCS tasks for tank and IFV groups for all combat stances. Returns True if tasking was added, returns False if the stance was not a combat stance. """ + self._set_reform_waypoint(dcs_group, forward_heading) if stance == CombatStance.AGGRESSIVE: # Attack nearest enemy if any # Then move forward OR Attack enemy base if it is not too far away @@ -313,15 +331,20 @@ class GroundConflictGenerator: -RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK ) ) - dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad) - dcs_group.points[1].tasks.append(AttackGroup(target.id)) + target_point = self.conflict.theater.nearest_land_pos( + target.points[0].position + rand_offset + ) + dcs_group.add_waypoint(target_point) + dcs_group.points[2].tasks.append(AttackGroup(target.id)) if ( to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE ): - attack_point = to_cp.position.random_point_within(500, 0) + attack_point = self.conflict.theater.nearest_land_pos( + to_cp.position.random_point_within(500, 0) + ) else: attack_point = self.find_offensive_point( dcs_group, @@ -334,7 +357,9 @@ class GroundConflictGenerator: # If the enemy base is close enough, the units will attack the base if to_cp.position.distance_to_point( dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE: - attack_point = to_cp.position.random_point_within(500, 0) + attack_point = self.conflict.theater.nearest_land_pos( + to_cp.position.random_point_within(500, 0) + ) else: attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE) dcs_group.add_waypoint(attack_point, PointAction.OffRoad) @@ -351,10 +376,15 @@ class GroundConflictGenerator: RANDOM_OFFSET_ATTACK ) ) - dcs_group.add_waypoint(target.points[0].position+rand_offset, PointAction.OffRoad) - dcs_group.points[i].tasks.append(AttackGroup(target.id)) + target_point = self.conflict.theater.nearest_land_pos( + target.points[0].position+rand_offset + ) + dcs_group.add_waypoint(target_point, PointAction.OffRoad) + dcs_group.points[i + 1].tasks.append(AttackGroup(target.id)) if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE: - attack_point = to_cp.position.random_point_within(500, 0) + attack_point = self.conflict.theater.nearest_land_pos( + to_cp.position.random_point_within(500, 0) + ) dcs_group.add_waypoint(attack_point) if stance != CombatStance.RETREAT: @@ -373,10 +403,11 @@ class GroundConflictGenerator: Handles adding the DCS tasks for APC and ATGM groups for all combat stances. Returns True if tasking was added, returns False if the stance was not a combat stance. """ + self._set_reform_waypoint(dcs_group, forward_heading) if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]: # APC & ATGM will never move too much forward, but will follow along any offensive if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE: - attack_point = to_cp.position.random_point_within(500, 0) + attack_point = self.conflict.theater.nearest_land_pos(to_cp.position.random_point_within(500, 0)) else: attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE) dcs_group.add_waypoint(attack_point, PointAction.OffRoad) @@ -464,8 +495,8 @@ class GroundConflictGenerator: self.mission.triggerrules.triggers.append(fallback) - @staticmethod def find_retreat_point( + self, dcs_group: VehicleGroup, frontline_heading: int, distance: int = RETREAT_DISTANCE @@ -476,10 +507,14 @@ class GroundConflictGenerator: :param frontline_heading: Heading of the frontline :return: dcs.mapping.Point object with the desired position """ - return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance) + desired_point = dcs_group.points[0].position.point_from_heading(heading_sum(frontline_heading, +180), distance) + if self.conflict.theater.is_on_land(desired_point): + return desired_point + return self.conflict.theater.nearest_land_pos(desired_point) + - @staticmethod def find_offensive_point( + self, dcs_group: VehicleGroup, frontline_heading: int, distance: int @@ -491,7 +526,10 @@ class GroundConflictGenerator: :param distance: Distance of the offensive (how far unit should move) :return: dcs.mapping.Point object with the desired position """ - return dcs_group.points[0].position.point_from_heading(frontline_heading, distance) + desired_point = dcs_group.points[0].position.point_from_heading(frontline_heading, distance) + if self.conflict.theater.is_on_land(desired_point): + return desired_point + return self.conflict.theater.nearest_land_pos(desired_point) @staticmethod def find_n_nearest_enemy_groups( @@ -562,10 +600,10 @@ class GroundConflictGenerator: For artilery group, decide the distance from frontline with the range of the unit """ rg = group.units[0].threat_range - 7500 - if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]: + if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]: rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY] - if rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK]: - rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100 + if rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]: + rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] return rg def get_valid_position_for_group( @@ -576,16 +614,13 @@ class GroundConflictGenerator: heading: int, spawn_heading: int ): - i = 0 - while i < 1000: - shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width)) - final_position = shifted.point_from_heading(spawn_heading, distance_from_frontline) + shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width)) + desired_point = shifted.point_from_heading( + spawn_heading, + random.randint(distance_from_frontline[0], distance_from_frontline[1]) + ) + return Conflict.find_ground_position(desired_point, combat_width, heading, self.conflict.theater) - if self.conflict.theater.is_on_land(final_position): - return final_position - i += 1 - continue - return None def _generate_groups( self, @@ -618,7 +653,7 @@ class GroundConflictGenerator: group.units[0], len(group.units), final_position, - distance_from_frontline, + random.randint(distance_from_frontline[0], distance_from_frontline[1]), heading=opposite_heading(spawn_heading), ) if is_player: @@ -657,7 +692,7 @@ class GroundConflictGenerator: group = self.mission.vehicle_group( side, namegen.next_unit_name(side, cp.id, unit), unit, - position=self._group_point(at, distance_from_frontline), + position=at, group_size=count, heading=heading, move_formation=move_formation) diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 65ea4058..68946ab2 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -91,8 +91,12 @@ class Conflict: return pos @classmethod - def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: - """Finds the nearest valid ground position along a provided heading and it's inverse""" + def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]: + """ + Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance. + `coerce=True` will return the closest land position to `initial` regardless of heading or distance + `coerce=False` will return None if a point isn't found + """ pos = initial if theater.is_on_land(pos): return pos @@ -101,5 +105,9 @@ class Conflict: if theater.is_on_land(pos): return pos pos = initial.point_from_heading(opposite_heading(heading), distance) + if coerce: + pos = theater.nearest_land_pos(initial) + return pos logging.error("Didn't find ground position ({})!".format(initial)) - return initial + return None + diff --git a/gen/ground_forces/ai_ground_planner.py b/gen/ground_forces/ai_ground_planner.py index b0f14df4..12df519f 100644 --- a/gen/ground_forces/ai_ground_planner.py +++ b/gen/ground_forces/ai_ground_planner.py @@ -187,14 +187,14 @@ class CombatGroupRole(Enum): DISTANCE_FROM_FRONTLINE = { - CombatGroupRole.TANK:3200, - CombatGroupRole.APC:8000, - CombatGroupRole.IFV:3700, - CombatGroupRole.ARTILLERY:18000, - CombatGroupRole.SHORAD:13000, - CombatGroupRole.LOGI:20000, - CombatGroupRole.INFANTRY:3000, - CombatGroupRole.ATGM:6200 + CombatGroupRole.TANK: (2200, 3200), + CombatGroupRole.APC: (7500, 8500), + CombatGroupRole.IFV: (2700, 3700), + CombatGroupRole.ARTILLERY: (16000, 18000), + CombatGroupRole.SHORAD: (12000, 13000), + CombatGroupRole.LOGI: (18000, 20000), + CombatGroupRole.INFANTRY: (2800, 3300), + CombatGroupRole.ATGM: (5200, 6200), } GROUP_SIZES_BY_COMBAT_STANCE = { diff --git a/resources/caulandmap.p b/resources/caulandmap.p index a65459cd..c9f77f43 100644 Binary files a/resources/caulandmap.p and b/resources/caulandmap.p differ diff --git a/resources/tools/cau_terrain.miz b/resources/tools/cau_terrain.miz index 01b84d44..5211c374 100644 Binary files a/resources/tools/cau_terrain.miz and b/resources/tools/cau_terrain.miz differ