initial multi segment frontline implementation

This commit is contained in:
walterroach 2020-11-12 21:47:13 -06:00
parent 5719b136fe
commit 33885e2216
10 changed files with 115 additions and 88 deletions

View File

@ -393,8 +393,7 @@ class Game:
# By default, use the existing frontline conflict position # By default, use the existing frontline conflict position
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts():
position = Conflict.frontline_position(self.theater, position = Conflict.frontline_position(front_line.control_point_a,
front_line.control_point_a,
front_line.control_point_b) front_line.control_point_b)
points.append(position[0]) points.append(position[0])
points.append(front_line.control_point_a.position) points.append(front_line.control_point_a.position)

View File

@ -93,7 +93,7 @@ class GroundConflictGenerator:
if combat_width < 35000: if combat_width < 35000:
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 # Create player groups at random position
for group in self.player_planned_combat_groups: for group in self.player_planned_combat_groups:

View File

@ -6,6 +6,7 @@ from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from theater import ConflictTheater, ControlPoint from theater import ConflictTheater, ControlPoint
from theater.frontline import FrontLine
AIR_DISTANCE = 40000 AIR_DISTANCE = 40000
@ -134,14 +135,11 @@ class Conflict:
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
@classmethod @staticmethod
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]: def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
attack_heading = from_cp.position.heading_between_point(to_cp.position) frontline = FrontLine(from_cp, to_cp)
attack_distance = from_cp.position.distance_to_point(to_cp.position) attack_heading = frontline.attack_heading
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2) position = frontline.position
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)
return position, _opposite_heading(attack_heading) return position, _opposite_heading(attack_heading)
@ -162,7 +160,7 @@ class Conflict:
return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length 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 center_position, heading = frontline
left_position, right_position = None, None left_position, right_position = None, None
@ -479,7 +477,7 @@ class Conflict:
@classmethod @classmethod
def transport_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): 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) 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) dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
if not dest: if not dest:

View File

@ -370,7 +370,7 @@ class GroundObjectsGenerator:
center = self.conflict.center center = self.conflict.center
heading = self.conflict.heading - 90 heading = self.conflict.heading - 90
else: 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 heading -= 90
initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE) initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE)

View File

@ -104,7 +104,7 @@ class VisualGenerator:
if from_cp.is_global or to_cp.is_global: if from_cp.is_global or to_cp.is_global:
continue continue
frontline = Conflict.frontline_position(self.game.theater, from_cp, to_cp) frontline = Conflict.frontline_position(from_cp, to_cp)
if not frontline: if not frontline:
continue continue

View File

@ -55,7 +55,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if cp.captured: if cp.captured:
enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured] enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured]
for ecp in enemy_cp: for ecp in enemy_cp:
pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0] pos = Conflict.frontline_position(cp, ecp)[0]
wpt = FlightWaypoint( wpt = FlightWaypoint(
FlightWaypointType.CUSTOM, FlightWaypointType.CUSTOM,
pos.x, pos.x,

View File

@ -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.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal 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 ( from theater.theatergroundobject import (
EwrGroundObject, EwrGroundObject,
MissileSiteGroundObject, MissileSiteGroundObject,
@ -407,13 +408,21 @@ class QLiberationMap(QGraphicsView):
pen = QPen(brush=color) pen = QPen(brush=color)
pen.setColor(color) pen.setColor(color)
pen.setWidth(6) 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 cp.captured and not connected_cp.captured and Conflict.has_frontline_between(cp, connected_cp):
if not cp.captured: if not cp.captured:
scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen)
else: 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) 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) p1 = point_from_heading(pos2[0], pos2[1], h+180, 25)
p2 = point_from_heading(pos2[0], pos2[1], h, 25) p2 = point_from_heading(pos2[0], pos2[1], h, 25)
@ -421,7 +430,11 @@ class QLiberationMap(QGraphicsView):
FrontLine(cp, connected_cp))) FrontLine(cp, connected_cp)))
else: 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): def wheelEvent(self, event: QWheelEvent):

View File

@ -15,7 +15,7 @@ from dcs.terrain.terrain import Terrain
from .controlpoint import ControlPoint from .controlpoint import ControlPoint
from .landmap import Landmap, load_landmap, poly_contains from .landmap import Landmap, load_landmap, poly_contains
from .frontline import FrontLine from .frontline import FrontLine, ComplexFrontLine
SIZE_TINY = 150 SIZE_TINY = 150
SIZE_SMALL = 600 SIZE_SMALL = 600
@ -61,6 +61,7 @@ class ConflictTheater:
reference_points: Dict[Tuple[float, float], Tuple[float, float]] reference_points: Dict[Tuple[float, float], Tuple[float, float]]
overview_image: str overview_image: str
landmap: Optional[Landmap] landmap: Optional[Landmap]
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
""" """
land_poly = None # type: Polygon land_poly = None # type: Polygon
""" """
@ -68,6 +69,7 @@ class ConflictTheater:
def __init__(self): def __init__(self):
self.controlpoints: List[ControlPoint] = [] self.controlpoints: List[ControlPoint] = []
ConflictTheater.frontline_data = FrontLine.load_json_frontlines(self)
""" """
self.land_poly = geometry.Polygon(self.landmap[0][0]) self.land_poly = geometry.Polygon(self.landmap[0][0])
for x in self.landmap[1]: for x in self.landmap[1]:
@ -228,7 +230,6 @@ class PersianGulfTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
class NevadaTheater(ConflictTheater): class NevadaTheater(ConflictTheater):
terrain = nevada.Nevada() terrain = nevada.Nevada()
overview_image = "nevada.gif" overview_image = "nevada.gif"
@ -242,7 +243,6 @@ class NevadaTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
class NormandyTheater(ConflictTheater): class NormandyTheater(ConflictTheater):
terrain = normandy.Normandy() terrain = normandy.Normandy()
overview_image = "normandy.gif" overview_image = "normandy.gif"
@ -256,7 +256,6 @@ class NormandyTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
class TheChannelTheater(ConflictTheater): class TheChannelTheater(ConflictTheater):
terrain = thechannel.TheChannel() terrain = thechannel.TheChannel()
overview_image = "thechannel.gif" overview_image = "thechannel.gif"
@ -270,7 +269,6 @@ class TheChannelTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
class SyriaTheater(ConflictTheater): class SyriaTheater(ConflictTheater):
terrain = syria.Syria() terrain = syria.Syria()
overview_image = "syria.gif" overview_image = "syria.gif"

View File

@ -1,17 +1,21 @@
"""Battlefield front lines.""" """Battlefield front lines."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
import logging import logging
import json import json
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from itertools import tee 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 dcs.mapping import Point
from .controlpoint import ControlPoint, MissionTarget from .controlpoint import ControlPoint, MissionTarget
if TYPE_CHECKING:
from theater.conflicttheater import ConflictTheater
Numeric = Union[int, float] Numeric = Union[int, float]
# TODO: Dedup by moving everything to using this class. # TODO: Dedup by moving everything to using this class.
@ -19,7 +23,10 @@ FRONTLINE_MIN_CP_DISTANCE = 5000
def pairwise(iterable): def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..." """
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = tee(iterable) a, b = tee(iterable)
next(b, None) next(b, None)
return zip(a, b) return zip(a, b)
@ -48,10 +55,12 @@ class FrontLineSegment:
@property @property
def attack_heading(self) -> Numeric: 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) return self.point_a.heading_between_point(self.point_b)
@property @property
def attack_distance(self) -> Numeric: def attack_distance(self) -> Numeric:
"""Length of the segment"""
return self.point_a.distance_to_point(self.point_b) 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 Overwrites the entirety of MissionTarget __init__ method to allow for
dynamic position calculation. dynamic position calculation.
""" """
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
theater: ConflictTheater
def __init__( def __init__(
self, self,
@ -73,7 +83,6 @@ class FrontLine(MissionTarget):
self.segments: List[FrontLineSegment] = [] self.segments: List[FrontLineSegment] = []
self._build_segments() self._build_segments()
self.name = f"Front line {control_point_a}/{control_point_b}" self.name = f"Front line {control_point_a}/{control_point_b}"
print(f"FRONTLINE SEGMENTS {len(self.segments)}")
@property @property
def position(self): def position(self):
@ -90,10 +99,12 @@ class FrontLine(MissionTarget):
@property @property
def attack_distance(self): def attack_distance(self):
"""The total distance of all segments"""
return sum(i.attack_distance for i in self.segments) return sum(i.attack_distance for i in self.segments)
@property @property
def attack_heading(self): def attack_heading(self):
"""The heading of the active attack segment from player to enemy control point"""
return self.active_segment.attack_heading return self.active_segment.attack_heading
@property @property
@ -113,6 +124,30 @@ class FrontLine(MissionTarget):
) )
return self.segments[0] 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 @property
def _position_distance(self) -> float: def _position_distance(self) -> float:
""" """
@ -122,44 +157,37 @@ class FrontLine(MissionTarget):
total_strength = ( total_strength = (
self.control_point_a.base.strength + self.control_point_b.base.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) 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 strength_pct = self.control_point_a.base.strength / total_strength
return self._adjust_for_min_dist(strength_pct * self.attack_distance) return self._adjust_for_min_dist(strength_pct * self.attack_distance)
@classmethod def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
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 Ensures the frontline conflict is never located within the minimum distance
according to the current strength of each control point. 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: def _build_segments(self) -> None:
"""Create line segments for the frontline"""
control_point_ids = "|".join( control_point_ids = "|".join(
[str(self.control_point_a.id), str(self.control_point_b.id)] [str(self.control_point_a.id), str(self.control_point_b.id)]
) ) # from_cp.id|to_cp.id
reversed_cp_ids = "|".join( reversed_cp_ids = "|".join(
[str(self.control_point_b.id), str(self.control_point_a.id)] [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 ( if (complex_frontlines) and (
(control_point_ids in complex_frontlines) (control_point_ids in complex_frontlines)
or (reversed_cp_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: @classmethod
""" def load_json_frontlines(
Ensures the frontline conflict is never located within the minimum distance cls, theater: ConflictTheater
constant of either end control point. ) -> Optional[Dict[str, ComplexFrontLine]]:
""" """Load complex frontlines from json and set the theater class variable to current theater instance"""
if (distance > self.attack_distance / 2) and ( cls.theater = theater
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance try:
): path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE with open(path, "r") as file:
elif (distance < self.attack_distance / 2) and ( logging.debug(f"Loading frontline from {path}...")
distance < FRONTLINE_MIN_CP_DISTANCE data = json.load(file)
): return {
distance = FRONTLINE_MIN_CP_DISTANCE frontline: ComplexFrontLine(
return distance data[frontline]["start_cp"],
[Point(i[0], i[1]) for i in data[frontline]["points"]],
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: for frontline in data
remaining_dist -= segment.attack_distance }
except OSError:
logging.warning(
f"Unable to load preset frontlines for {theater.terrain.name}"
)
return None

View File

@ -74,7 +74,6 @@ class GameGenerator:
namegen.reset() namegen.reset()
self.prepare_theater() self.prepare_theater()
self.populate_red_airbases() self.populate_red_airbases()
FrontLine.load_json_frontlines(self.theater.terrain.name)
game = Game(player_name=self.player, game = Game(player_name=self.player,
enemy_name=self.enemy, enemy_name=self.enemy,
theater=self.theater, theater=self.theater,