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
for front_line in self.theater.conflicts():
position = Conflict.frontline_position(self.theater,
front_line.control_point_a,
position = Conflict.frontline_position(front_line.control_point_a,
front_line.control_point_b)
points.append(position[0])
points.append(front_line.control_point_a.position)

View File

@ -93,7 +93,7 @@ class GroundConflictGenerator:
if 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
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 theater import ConflictTheater, ControlPoint
from theater.frontline import FrontLine
AIR_DISTANCE = 40000
@ -134,14 +135,11 @@ class Conflict:
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
return from_cp.has_frontline and to_cp.has_frontline
@classmethod
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
attack_heading = from_cp.position.heading_between_point(to_cp.position)
attack_distance = from_cp.position.distance_to_point(to_cp.position)
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2)
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)
@staticmethod
def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
frontline = FrontLine(from_cp, to_cp)
attack_heading = frontline.attack_heading
position = frontline.position
return position, _opposite_heading(attack_heading)
@ -162,7 +160,7 @@ class Conflict:
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
left_position, right_position = None, None
@ -479,7 +477,7 @@ class Conflict:
@classmethod
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)
dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
if not dest:

View File

@ -370,7 +370,7 @@ class GroundObjectsGenerator:
center = self.conflict.center
heading = self.conflict.heading - 90
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
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:
continue
frontline = Conflict.frontline_position(self.game.theater, from_cp, to_cp)
frontline = Conflict.frontline_position(from_cp, to_cp)
if not frontline:
continue

View File

@ -55,7 +55,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if cp.captured:
enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured]
for ecp in enemy_cp:
pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0]
pos = Conflict.frontline_position(cp, ecp)[0]
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
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.QMapGroundObject import QMapGroundObject
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 (
EwrGroundObject,
MissileSiteGroundObject,
@ -407,13 +408,21 @@ class QLiberationMap(QGraphicsView):
pen = QPen(brush=color)
pen.setColor(color)
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 not cp.captured:
scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen)
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)
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)
p2 = point_from_heading(pos2[0], pos2[1], h, 25)
@ -421,7 +430,11 @@ class QLiberationMap(QGraphicsView):
FrontLine(cp, connected_cp)))
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):

View File

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

View File

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