Merge pull request #474 from walterroach/frontline_vector

Frontline vector
This commit is contained in:
walterroach
2020-11-27 18:49:50 -06:00
committed by GitHub
6 changed files with 176 additions and 201 deletions

View File

@@ -42,4 +42,16 @@ def mps_to_knots(value_in_mps: float) -> int:
:arg value_in_mps Meters Per Second :arg value_in_mps Meters Per Second
""" """
return int(value_in_mps * 1.943) 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

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import logging import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, TYPE_CHECKING from typing import List, TYPE_CHECKING, Tuple
from dcs import Mission from dcs import Mission
from dcs.action import AITaskPush from dcs.action import AITaskPush
@@ -25,17 +25,19 @@ from dcs.task import (
from dcs.triggers import Event, TriggerOnce from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle from dcs.unit import Vehicle
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from dcs.unitgroup import VehicleGroup
from game import db from game import db
from .naming import namegen from .naming import namegen
from gen.ground_forces.ai_ground_planner import ( from gen.ground_forces.ai_ground_planner import (
CombatGroupRole, CombatGroup, CombatGroupRole,
DISTANCE_FROM_FRONTLINE, DISTANCE_FROM_FRONTLINE,
) )
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance from .ground_forces.combat_stance import CombatStance
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.utils import heading_sum, opposite_heading
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -69,7 +71,15 @@ class JtacInfo:
class GroundConflictGenerator: 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.mission = mission
self.conflict = conflict self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups 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( distance = random.randint(
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]), int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]), 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) return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_SIZE_FACTOR)
def generate(self): 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) 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 # Create player groups at random position
for group in self.player_planned_combat_groups: player_groups = self._generate_groups(self.player_planned_combat_groups, frontline_vector, True)
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}")
# Create enemy groups at random position # Create enemy groups at random position
for group in self.enemy_planned_combat_groups: enemy_groups = self._generate_groups(self.enemy_planned_combat_groups, frontline_vector, False)
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)
# Plan combat actions for groups # 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.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) 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 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 i = 0
while i < 1000: while i < 1000:
heading_diff = -90 if isplayer else 90 shifted = conflict_position.point_from_heading(heading, random.randint(0, combat_width))
shifted = conflict_position[0].point_from_heading(self.conflict.heading, final_position = shifted.point_from_heading(spawn_heading, distance_from_frontline)
random.randint((int)(-combat_width / 2), (int)(combat_width / 2)))
final_position = shifted.point_from_heading(self.conflict.heading + heading_diff, distance_from_frontline)
if self.conflict.theater.is_on_land(final_position): if self.conflict.theater.is_on_land(final_position):
return final_position return final_position
i += 1 i += 1
continue continue
return None 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: if side == self.conflict.attackers_country:
cp = self.conflict.from_cp cp = self.conflict.from_cp
@@ -515,4 +533,4 @@ class GroundConflictGenerator:
vehicle: Vehicle = group.units[c] vehicle: Vehicle = group.units[c]
vehicle.player_can_drive = True vehicle.player_can_drive = True
return group return group

View File

@@ -7,25 +7,11 @@ from dcs.mapping import Point
from game.theater.conflicttheater import ConflictTheater, FrontLine from game.theater.conflicttheater import ConflictTheater, FrontLine
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
from game.utils import heading_sum, opposite_heading
FRONTLINE_LENGTH = 80000 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: class Conflict:
def __init__(self, def __init__(self,
theater: ConflictTheater, theater: ConflictTheater,
@@ -37,7 +23,7 @@ class Conflict:
defenders_country: Country, defenders_country: Country,
position: Point, position: Point,
heading=None, heading=None,
distance=None, size=None
): ):
self.attackers_side = attackers_side self.attackers_side = attackers_side
@@ -50,99 +36,39 @@ class Conflict:
self.theater = theater self.theater = theater
self.position = position self.position = position
self.heading = heading self.heading = heading
self.distance = distance self.size = size
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)
@classmethod @classmethod
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool: def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
return from_cp.has_frontline and to_cp.has_frontline return from_cp.has_frontline and to_cp.has_frontline
@staticmethod @classmethod
def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]: def frontline_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]:
frontline = FrontLine(from_cp, to_cp, theater) frontline = FrontLine(from_cp, to_cp, theater)
attack_heading = frontline.attack_heading attack_heading = frontline.attack_heading
position = frontline.position position = cls.find_ground_position(frontline.position, FRONTLINE_LENGTH, heading_sum(attack_heading, 90), theater)
return position, _opposite_heading(attack_heading) 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))
@classmethod @classmethod
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]: 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) Returns a vector for a valid frontline location avoiding exclusion zones.
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
""" """
frontline = cls.frontline_position(from_cp, to_cp, theater) center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
center_position, heading = frontline left_heading = heading_sum(heading, -90)
left_position, right_position = None, None right_heading = heading_sum(heading, 90)
left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater)
if not theater.is_on_land(center_position): right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, -90), theater) distance = int(left_position.distance_to_point(right_position))
if pos: return left_position, right_heading, distance
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))
@classmethod @classmethod
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): 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) assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
conflict = cls(
return cls(
position=position, position=position,
heading=heading, heading=heading,
distance=distance,
theater=theater, theater=theater,
from_cp=from_cp, from_cp=from_cp,
to_cp=to_cp, to_cp=to_cp,
@@ -150,50 +76,33 @@ class Conflict:
defenders_side=defender_name, defenders_side=defender_name,
attackers_country=attacker, attackers_country=attacker,
defenders_country=defender, defenders_country=defender,
size=distance
) )
return conflict
@classmethod @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 pos = initial
for offset in range(0, int(max_distance), 500): for distance in range(0, int(max_distance), 100):
new_pos = initial.point_from_heading(heading, offset) if not theater.is_on_land(pos):
if theater.is_on_land(new_pos):
pos = new_pos
else:
return pos return pos
return pos pos = initial.point_from_heading(heading, distance)
if theater.is_on_land(pos):
""" return pos
probe_end_point = initial.point_from_heading(heading, max_distance) logging.error("Didn't find ground position ({})!".format(initial))
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y)]) return initial
intersection = probe.intersection(theater.land_poly) @classmethod
if intersection is geometry.LineString: def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
return Point(*intersection.xy[1]) """Finds the nearest ground position along a provided heading and it's inverse"""
elif intersection is geometry.MultiLineString: pos = initial
return Point(*intersection.geoms[0].xy[1]) for distance in range(0, int(max_distance), 100):
if theater.is_on_land(pos):
return None return pos
""" pos = initial.point_from_heading(heading, distance)
if theater.is_on_land(pos):
@classmethod return pos
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: pos = initial.point_from_heading(opposite_heading(heading), distance)
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])
"""
logging.error("Didn't find ground position ({})!".format(initial)) logging.error("Didn't find ground position ({})!".format(initial))
return initial return initial

View File

@@ -1038,7 +1038,7 @@ class FlightPlanBuilder:
if not isinstance(location, FrontLine): if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location) 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], location.control_points[0], location.control_points[1],
self.game.theater self.game.theater
) )

View File

@@ -58,6 +58,7 @@ class DisplayOptions:
waypoint_info = DisplayRule("Waypoint Information", True) waypoint_info = DisplayRule("Waypoint Information", True)
culling = DisplayRule("Display Culling Zones", False) culling = DisplayRule("Display Culling Zones", False)
flight_paths = FlightPathOptions() flight_paths = FlightPathOptions()
actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False)
@classmethod @classmethod
def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]: def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:

View File

@@ -476,17 +476,52 @@ class QLiberationMap(QGraphicsView):
pen.setWidth(6) pen.setWidth(6)
frontline = FrontLine(cp, connected_cp, self.game.theater) 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 cp.captured and not connected_cp.captured and Conflict.has_frontline_between(cp, connected_cp):
posx = frontline.position if DisplayOptions.actual_frontline_pos:
h = frontline.attack_heading self.draw_actual_frontline(frontline, scene, pen)
pos2 = self._transform_point(posx) else:
self.draw_bezier_frontline(scene, pen, frontline) self.draw_frontline_approximation(frontline, scene, 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, self.game_model))
else: else:
self.draw_bezier_frontline(scene, pen, frontline) 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): def wheelEvent(self, event: QWheelEvent):