Merge pull request #366 from walterroach/new_frontline

New frontline paths.  First step in #287
This commit is contained in:
walterroach 2020-11-16 22:13:33 -06:00 committed by GitHub
commit f3553ced78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 490 additions and 173 deletions

View File

@ -369,9 +369,9 @@ 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) self.theater)
points.append(position[0]) points.append(position[0])
points.append(front_line.control_point_a.position) points.append(front_line.control_point_a.position)
points.append(front_line.control_point_b.position) points.append(front_line.control_point_b.position)

View File

@ -1,7 +1,8 @@
from __future__ import annotations
import logging import logging
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import List, TYPE_CHECKING
from dcs import Mission from dcs import Mission
from dcs.action import AITaskPush from dcs.action import AITaskPush
@ -36,6 +37,9 @@ 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
if TYPE_CHECKING:
from game import Game
SPREAD_DISTANCE_FACTOR = 0.1, 0.3 SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1 SPREAD_DISTANCE_SIZE_FACTOR = 0.1
@ -65,7 +69,7 @@ class JtacInfo:
class GroundConflictGenerator: class GroundConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance): def __init__(self, mission: Mission, conflict: Conflict, game: Game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance):
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
@ -93,7 +97,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, self.game.theater)
# 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:
@ -114,6 +118,8 @@ class GroundConflictGenerator:
player_groups.append((g,group)) player_groups.append((g,group))
self.gen_infantry_group_for_group(g, True, self.mission.country(self.game.player_country), self.conflict.heading + 90) 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: for group in self.enemy_planned_combat_groups:
@ -454,7 +460,7 @@ class GroundConflictGenerator:
def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline): def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline):
i = 0 i = 0
while i < 25: # 25 attempt for valid position while i < 1000:
heading_diff = -90 if isplayer else 90 heading_diff = -90 if isplayer else 90
shifted = conflict_position[0].point_from_heading(self.conflict.heading, shifted = conflict_position[0].point_from_heading(self.conflict.heading,
random.randint((int)(-combat_width / 2), (int)(combat_width / 2))) random.randint((int)(-combat_width / 2), (int)(combat_width / 2)))
@ -462,8 +468,7 @@ class GroundConflictGenerator:
if self.conflict.theater.is_on_land(final_position): if self.conflict.theater.is_on_land(final_position):
return final_position return final_position
else: i += 1
i = i + 1
continue continue
return None return None

View File

@ -6,7 +6,7 @@ import os
import random import random
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass
from theater.frontline import FrontLine from theater import FrontLine
from typing import List, Dict, TYPE_CHECKING from typing import List, Dict, TYPE_CHECKING
from jinja2 import Environment, FileSystemLoader, select_autoescape from jinja2 import Environment, FileSystemLoader, select_autoescape

View File

@ -5,7 +5,7 @@ from typing import Tuple
from dcs.country import Country 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, FrontLine
AIR_DISTANCE = 40000 AIR_DISTANCE = 40000
@ -134,14 +134,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, theater: ConflictTheater) -> Tuple[Point, int]:
attack_heading = from_cp.position.heading_between_point(to_cp.position) frontline = FrontLine(from_cp, to_cp, theater)
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 +159,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, theater)
center_position, heading = frontline center_position, heading = frontline
left_position, right_position = None, None left_position, right_position = None, None
@ -212,7 +209,7 @@ class Conflict:
@classmethod @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:
pos = initial pos = initial
for _ in range(0, int(max_distance), 500): for _ in range(0, int(max_distance), 100):
if theater.is_on_land(pos): if theater.is_on_land(pos):
return pos return pos
@ -479,7 +476,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, theater)
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

@ -321,7 +321,7 @@ class ObjectiveFinder:
continue continue
if Conflict.has_frontline_between(cp, connected): if Conflict.has_frontline_between(cp, connected):
yield FrontLine(cp, connected) yield FrontLine(cp, connected, self.game.theater)
def vulnerable_control_points(self) -> Iterator[ControlPoint]: def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs. """Iterates over friendly CPs that are vulnerable to enemy CPs.

View File

@ -355,7 +355,7 @@ class GroundObjectsGenerator:
""" """
FARP_CAPACITY = 4 FARP_CAPACITY = 4
def __init__(self, mission: Mission, conflict: Conflict, game, def __init__(self, mission: Mission, conflict: Conflict, game: Game,
radio_registry: RadioRegistry, tacan_registry: TacanRegistry): radio_registry: RadioRegistry, tacan_registry: TacanRegistry):
self.m = mission self.m = mission
self.conflict = conflict self.conflict = conflict
@ -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, self.game.theater)
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, self.game.theater)
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, self.game.theater)[0]
wpt = FlightWaypoint( wpt = FlightWaypoint(
FlightWaypointType.CUSTOM, FlightWaypointType.CUSTOM,
pos.x, pos.x,

View File

@ -14,8 +14,10 @@ from PySide2.QtWidgets import (
import qt_ui.uiconstants as const import qt_ui.uiconstants as const
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
from theater.missiontarget import MissionTarget from theater import FrontLine
class QFrontLine(QGraphicsLineItem): class QFrontLine(QGraphicsLineItem):
@ -26,9 +28,10 @@ class QFrontLine(QGraphicsLineItem):
""" """
def __init__(self, x1: float, y1: float, x2: float, y2: float, def __init__(self, x1: float, y1: float, x2: float, y2: float,
mission_target: MissionTarget) -> None: mission_target: FrontLine, game_model: GameModel) -> None:
super().__init__(x1, y1, x2, y2) super().__init__(x1, y1, x2, y2)
self.mission_target = mission_target self.mission_target = mission_target
self.game_model = game_model
self.new_package_dialog: Optional[QNewPackageDialog] = None self.new_package_dialog: Optional[QNewPackageDialog] = None
self.setAcceptHoverEvents(True) self.setAcceptHoverEvents(True)
@ -55,6 +58,14 @@ class QFrontLine(QGraphicsLineItem):
new_package_action.triggered.connect(self.open_new_package_dialog) new_package_action.triggered.connect(self.open_new_package_dialog)
menu.addAction(new_package_action) menu.addAction(new_package_action)
cheat_forward = QAction(f"CHEAT: Advance Frontline")
cheat_forward.triggered.connect(self.cheat_forward)
menu.addAction(cheat_forward)
cheat_backward = QAction(f"CHEAT: Retreat Frontline")
cheat_backward.triggered.connect(self.cheat_backward)
menu.addAction(cheat_backward)
menu.exec_(event.screenPos()) menu.exec_(event.screenPos())
@property @property
@ -80,3 +91,16 @@ class QFrontLine(QGraphicsLineItem):
def open_new_package_dialog(self) -> None: def open_new_package_dialog(self) -> None:
"""Opens the dialog for planning a new mission package.""" """Opens the dialog for planning a new mission package."""
Dialog.open_new_package_dialog(self.mission_target) Dialog.open_new_package_dialog(self.mission_target)
def cheat_forward(self) -> None:
self.mission_target.control_point_a.base.affect_strength(0.1)
self.mission_target.control_point_b.base.affect_strength(-0.1)
self.game_model.game.initialize_turn()
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
def cheat_backward(self) -> None:
self.mission_target.control_point_a.base.affect_strength(-0.1)
self.mission_target.control_point_b.base.affect_strength(0.1)
self.game_model.game.initialize_turn()
GameUpdateSignal.get_instance().updateGame(self.game_model.game)

View File

@ -2,7 +2,8 @@ from __future__ import annotations
import datetime import datetime
import logging import logging
from typing import List, Optional, Tuple import math
from typing import Iterable, List, Optional, Tuple, Iterator
from PySide2.QtCore import QPointF, Qt from PySide2.QtCore import QPointF, Qt
from PySide2.QtGui import ( from PySide2.QtGui import (
@ -38,13 +39,41 @@ 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.conflicttheater import FrontLine
from theater.theatergroundobject import ( from theater.theatergroundobject import (
EwrGroundObject, EwrGroundObject,
MissileSiteGroundObject, MissileSiteGroundObject,
TheaterGroundObject, TheaterGroundObject,
) )
def binomial(i: int, n: int) -> float:
"""Binomial coefficient"""
return math.factorial(n) / float(
math.factorial(i) * math.factorial(n - i))
def bernstein(t: float, i: int, n: int) -> float:
"""Bernstein polynom"""
return binomial(i, n) * (t ** i) * ((1 - t) ** (n - i))
def bezier(t: float, points: Iterable[Tuple[float, float]]) -> Tuple[float, float]:
"""Calculate coordinate of a point in the bezier curve"""
n = len(points) - 1
x = y = 0
for i, pos in enumerate(points):
bern = bernstein(t, i, n)
x += pos[0] * bern
y += pos[1] * bern
return x, y
def bezier_curve_range(n: int, points: Iterable[Tuple[float, float]]) -> Iterator[Tuple[float, float]]:
"""Range of points in a curve bezier"""
for i in range(n):
t = i / float(n - 1)
yield bezier(t, points)
class QLiberationMap(QGraphicsView): class QLiberationMap(QGraphicsView):
WAYPOINT_SIZE = 4 WAYPOINT_SIZE = 4
@ -189,6 +218,67 @@ class QLiberationMap(QGraphicsView):
return detection_range, threat_range return detection_range, threat_range
def display_culling(self, scene: QGraphicsScene) -> None:
"""Draws the culling distance rings on the map"""
culling_points = self.game_model.game.get_culling_points()
culling_distance = self.game_model.game.settings.perf_culling_distance
for point in culling_points:
culling_distance_point = Point(point.x + culling_distance*1000, point.y + culling_distance*1000)
distance_point = self._transform_point(culling_distance_point)
transformed = self._transform_point(point)
diameter = distance_point[0] - transformed[0]
scene.addEllipse(transformed[0]-diameter/2, transformed[1]-diameter/2, diameter, diameter, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
@staticmethod
def ground_object_display_options(ground_object: TheaterGroundObject, cp: ControlPoint) -> Tuple[bool, bool]:
is_missile = isinstance(ground_object, MissileSiteGroundObject)
is_aa = ground_object.category == "aa" and not is_missile
is_ewr = isinstance(ground_object, EwrGroundObject)
is_display_type = is_aa or is_ewr
should_display = ((DisplayOptions.sam_ranges and cp.captured)
or
(DisplayOptions.enemy_sam_ranges and not cp.captured))
return is_display_type, should_display
def draw_threat_range(self, scene: QGraphicsScene, ground_object: TheaterGroundObject, cp: ControlPoint) -> None:
go_pos = self._transform_point(ground_object.position)
detection_range, threat_range = self.aa_ranges(
ground_object
)
if threat_range:
threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
ground_object.position.y+threat_range))
threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
# Add threat range circle
scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6,
threat_radius, threat_radius, self.threat_pen(cp.captured))
if detection_range and DisplayOptions.detection_range:
# Add detection range circle
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
ground_object.position.y+detection_range))
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
detection_radius, detection_radius, self.detection_pen(cp.captured))
def draw_ground_objects(self, scene: QGraphicsScene, cp: ControlPoint) -> None:
added_objects = []
for ground_object in cp.ground_objects:
if ground_object.obj_name in added_objects:
continue
go_pos = self._transform_point(ground_object.position)
if not ground_object.airbase_group:
buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
is_display_type, should_display = self.ground_object_display_options(ground_object, cp)
if is_display_type and should_display:
self.draw_threat_range(scene, ground_object, cp)
added_objects.append(ground_object.obj_name)
def reload_scene(self): def reload_scene(self):
scene = self.scene() scene = self.scene()
scene.clear() scene.clear()
@ -199,20 +289,13 @@ class QLiberationMap(QGraphicsView):
self.addBackground() self.addBackground()
# Uncomment below to help set up theater reference points # Uncomment below to help set up theater reference points
#for i, r in enumerate(self.game.theater.reference_points.items()): # for i, r in enumerate(self.game.theater.reference_points.items()):
# text = scene.addText(str(r), font=QFont("Trebuchet MS", 10, weight=5, italic=False)) # text = scene.addText(str(r), font=QFont("Trebuchet MS", 10, weight=5, italic=False))
# text.setPos(0, i * 24) # text.setPos(0, i * 24)
# Display Culling # Display Culling
if DisplayOptions.culling and self.game.settings.perf_culling: if DisplayOptions.culling and self.game.settings.perf_culling:
culling_points = self.game_model.game.get_culling_points() self.display_culling()
culling_distance = self.game_model.game.settings.perf_culling_distance
for point in culling_points:
culling_distance_point = Point(point.x + culling_distance*1000, point.y + culling_distance*1000)
distance_point = self._transform_point(culling_distance_point)
transformed = self._transform_point(point)
diameter = distance_point[0] - transformed[0]
scene.addEllipse(transformed[0]-diameter/2, transformed[1]-diameter/2, diameter, diameter, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
@ -230,45 +313,7 @@ class QLiberationMap(QGraphicsView):
pen = QPen(brush=CONST.COLORS[enemyColor]) pen = QPen(brush=CONST.COLORS[enemyColor])
brush = CONST.COLORS[enemyColor+"_transparent"] brush = CONST.COLORS[enemyColor+"_transparent"]
added_objects = [] self.draw_ground_objects(scene, cp)
for ground_object in cp.ground_objects:
if ground_object.obj_name in added_objects:
continue
go_pos = self._transform_point(ground_object.position)
if not ground_object.airbase_group:
buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
is_missile = isinstance(ground_object, MissileSiteGroundObject)
is_aa = ground_object.category == "aa" and not is_missile
is_ewr = isinstance(ground_object, EwrGroundObject)
is_display_type = is_aa or is_ewr
should_display = ((DisplayOptions.sam_ranges and cp.captured)
or
(DisplayOptions.enemy_sam_ranges and not cp.captured))
if is_display_type and should_display:
detection_range, threat_range = self.aa_ranges(
ground_object
)
if threat_range:
threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
ground_object.position.y+threat_range))
threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
# Add threat range circle
scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6,
threat_radius, threat_radius, self.threat_pen(cp.captured))
if detection_range:
# Add detection range circle
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
ground_object.position.y+detection_range))
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
if DisplayOptions.detection_range:
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
detection_radius, detection_radius, self.detection_pen(cp.captured))
added_objects.append(ground_object.obj_name)
for cp in self.game.theater.enemy_points(): for cp in self.game.theater.enemy_points():
if DisplayOptions.lines: if DisplayOptions.lines:
@ -399,36 +444,45 @@ class QLiberationMap(QGraphicsView):
flight_path_pen flight_path_pen
)) ))
def draw_bezier_frontline(self, scene: QGraphicsScene, pen:QPen, frontline: FrontLine) -> None:
"""
Thanks to Alquimista for sharing a python implementation of the bezier algorithm this is adapted from.
https://gist.github.com/Alquimista/1274149#file-bezdraw-py
"""
bezier_fixed_points = []
for segment in frontline.segments:
bezier_fixed_points.append(self._transform_point(segment.point_a))
bezier_fixed_points.append(self._transform_point(segment.point_b))
old_point = bezier_fixed_points[0]
for point in bezier_curve_range(int(len(bezier_fixed_points) * 2), bezier_fixed_points):
scene.addLine(old_point[0], old_point[1], point[0], point[1], pen=pen)
old_point = point
def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor): def scene_create_lines_for_cp(self, cp: ControlPoint, playerColor, enemyColor):
scene = self.scene() scene = self.scene()
pos = self._transform_point(cp.position)
for connected_cp in cp.connected_points: for connected_cp in cp.connected_points:
pos2 = self._transform_point(connected_cp.position) pos2 = self._transform_point(connected_cp.position)
if not cp.captured: if not cp.captured:
color = CONST.COLORS["dark_"+enemyColor] color = CONST.COLORS["dark_"+enemyColor]
elif cp.captured:
color = CONST.COLORS["dark_"+playerColor]
else: else:
color = CONST.COLORS["dark_"+enemyColor] color = CONST.COLORS["dark_"+playerColor]
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, 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):
if not cp.captured: posx = frontline.position
scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) h = frontline.attack_heading
else:
posx, h = Conflict.frontline_position(self.game.theater, cp, connected_cp)
pos2 = self._transform_point(posx) pos2 = self._transform_point(posx)
scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) self.draw_bezier_frontline(scene, pen, frontline)
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)
scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1], scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1],
FrontLine(cp, connected_cp))) frontline, self.game_model))
else: else:
scene.addLine(pos[0], pos[1], pos2[0], pos2[1], pen=pen) self.draw_bezier_frontline(scene, pen, frontline)
def wheelEvent(self, event: QWheelEvent): def wheelEvent(self, event: QWheelEvent):
@ -451,7 +505,7 @@ class QLiberationMap(QGraphicsView):
#print(self.factorized, factor, self._zoom) #print(self.factorized, factor, self._zoom)
def _transform_point(self, p: Point, treshold=30) -> (int, int): def _transform_point(self, p: Point, treshold=30) -> Tuple[int, int]:
point_a = list(self.game.theater.reference_points.keys())[0] point_a = list(self.game.theater.reference_points.keys())[0]
point_a_img = self.game.theater.reference_points[point_a] point_a_img = self.game.theater.reference_points[point_a]
@ -501,18 +555,11 @@ class QLiberationMap(QGraphicsView):
return CONST.COLORS[f"{name}_transparent"] return CONST.COLORS[f"{name}_transparent"]
def threat_pen(self, player: bool) -> QPen: def threat_pen(self, player: bool) -> QPen:
if player: color = "blue" if player else "red"
color = "blue" return QPen(CONST.COLORS[color])
else:
color = "red"
qpen = QPen(CONST.COLORS[color])
return qpen
def detection_pen(self, player: bool) -> QPen: def detection_pen(self, player: bool) -> QPen:
if player: color = "purple" if player else "yellow"
color = "purple"
else:
color = "yellow"
qpen = QPen(CONST.COLORS[color]) qpen = QPen(CONST.COLORS[color])
qpen.setStyle(Qt.DotLine) qpen.setStyle(Qt.DotLine)
return qpen return qpen

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,74 @@
from __future__ import annotations
import json
import argparse
from pathlib import Path
from typing import List, Tuple, Union, Dict
from dcs.terrain import Caucasus, PersianGulf, Syria, Nevada, Normandy, TheChannel
from dcs import Mission
Terrain = Union[Caucasus, PersianGulf, Syria, Nevada, Normandy, TheChannel]
SAVE_PATH = Path("resources/frontlines")
def validate_miz(file_path: Path) -> bool:
return bool(file_path.suffix == ".miz" and file_path.exists())
def validate_airports(airports: Tuple[int], terrain: Terrain):
for airport in airports:
if terrain.airport_by_id(airport) is None:
print(f"Cannot load airport for invalid id {airport}")
def load_files(files) -> List[Mission]:
missions = []
for file in files:
if validate_miz(file):
mission = Mission()
mission.load_file(file)
missions.append(mission)
else:
print(f"Error: {file} doesn't look like a valid mission file.")
return missions
def create_frontline_dict(mission: Mission) -> Dict[str, Dict]:
frontline_dict = {}
for group in mission.country("USA").vehicle_group:
groupname = str(group.name).replace(group.name.id, "").replace(":","")
control_points = groupname.split("|")
frontline_dict[groupname] = {
"points": [(i.position.x, i.position.y) for i in group.points],
"start_cp": int(control_points[0])
}
return frontline_dict
def process_missions(missions: List[Mission]) -> None:
for mission in missions:
frontline_dict = create_frontline_dict(mission)
write_json(frontline_dict, mission.terrain.name.lower())
def write_json(frontline_dict: Dict[str, Dict], terrain_name: str) -> None:
with open(SAVE_PATH.joinpath(terrain_name + ".json"), "w") as file:
json.dump(frontline_dict, file)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Process a miz file to create json descriptions of multi-segment frontlines"
)
parser.add_argument(
"files",
metavar="N",
type=Path,
nargs="+",
help="A list of space separated .miz files to extract frontlines from",
)
args = parser.parse_args()
missions = load_files(args.files)
process_missions(missions)
# frontline_dict = create_frontline_dict(missions[0])
print("Done")

View File

@ -1,5 +1,4 @@
from .base import * from .base import *
from .conflicttheater import * from .conflicttheater import *
from .controlpoint import * from .controlpoint import *
from .frontline import FrontLine
from .missiontarget import MissionTarget from .missiontarget import MissionTarget

View File

@ -1,6 +1,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING import logging
import json
from dataclasses import dataclass
from itertools import tee
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
from dcs.mapping import Point from dcs.mapping import Point
from dcs.terrain import ( from dcs.terrain import (
@ -13,11 +18,10 @@ from dcs.terrain import (
) )
from dcs.terrain.terrain import Terrain from dcs.terrain.terrain import Terrain
from .controlpoint import ControlPoint from .controlpoint import ControlPoint, MissionTarget
from .landmap import Landmap, load_landmap, poly_contains from .landmap import Landmap, load_landmap, poly_contains
if TYPE_CHECKING: Numeric = Union[int, float]
from . import FrontLine
SIZE_TINY = 150 SIZE_TINY = 150
SIZE_SMALL = 600 SIZE_SMALL = 600
@ -56,6 +60,17 @@ COAST_DL_W = [225, 270, 315, 0, 45]
COAST_DR_E = [315, 0, 45, 90, 135] COAST_DR_E = [315, 0, 45, 90, 135]
COAST_DR_W = [135, 180, 225, 315] COAST_DR_W = [135, 180, 225, 315]
FRONTLINE_MIN_CP_DISTANCE = 5000
def pairwise(iterable):
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
class ConflictTheater: class ConflictTheater:
terrain: Terrain terrain: Terrain
@ -67,9 +82,11 @@ class ConflictTheater:
land_poly = None # type: Polygon land_poly = None # type: Polygon
""" """
daytime_map: Dict[str, Tuple[int, int]] daytime_map: Dict[str, Tuple[int, int]]
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
def __init__(self): def __init__(self):
self.controlpoints: List[ControlPoint] = [] self.controlpoints: List[ControlPoint] = []
self.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]:
@ -128,10 +145,9 @@ class ConflictTheater:
return [point for point in self.controlpoints if point.captured] return [point for point in self.controlpoints if point.captured]
def conflicts(self, from_player=True) -> Iterator[FrontLine]: def conflicts(self, from_player=True) -> Iterator[FrontLine]:
from . import FrontLine # Circular import that needs to be resolved.
for cp in [x for x in self.controlpoints if x.captured == from_player]: for cp in [x for x in self.controlpoints if x.captured == from_player]:
for connected_point in [x for x in cp.connected_points if x.captured != from_player]: for connected_point in [x for x in cp.connected_points if x.captured != from_player]:
yield FrontLine(cp, connected_point) yield FrontLine(cp, connected_point, self)
def enemy_points(self) -> List[ControlPoint]: def enemy_points(self) -> List[ControlPoint]:
return [point for point in self.controlpoints if not point.captured] return [point for point in self.controlpoints if not point.captured]
@ -183,7 +199,6 @@ class ConflictTheater:
theater = theaters[data["theater"]] theater = theaters[data["theater"]]
t = theater() t = theater()
cps = {} cps = {}
for p in data["player_points"]: for p in data["player_points"]:
cp = t.add_json_cp(theater, p) cp = t.add_json_cp(theater, p)
cp.captured = True cp.captured = True
@ -232,7 +247,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"
@ -246,7 +260,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"
@ -260,7 +273,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"
@ -274,7 +286,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"
@ -287,3 +298,209 @@ class SyriaTheater(ConflictTheater):
"dusk": (16, 18), "dusk": (16, 18),
"night": (0, 5), "night": (0, 5),
} }
@dataclass
class ComplexFrontLine:
"""
Stores data necessary for building a multi-segment frontline.
"points" should be ordered from closest to farthest distance originating from start_cp.position
"""
start_cp: ControlPoint
points: List[Point]
@dataclass
class FrontLineSegment:
"""
Describes a line segment of a FrontLine
"""
point_a: Point
point_b: Point
@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)
class FrontLine(MissionTarget):
"""Defines a front line location between two control points.
Front lines are the area where ground combat happens.
Overwrites the entirety of MissionTarget __init__ method to allow for
dynamic position calculation.
"""
def __init__(
self,
control_point_a: ControlPoint,
control_point_b: ControlPoint,
theater: ConflictTheater
) -> None:
self.control_point_a = control_point_a
self.control_point_b = control_point_b
self.segments: List[FrontLineSegment] = []
self.theater = theater
self._build_segments()
self.name = f"Front line {control_point_a}/{control_point_b}"
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
return False
@property
def position(self):
"""
The position where the conflict should occur
according to the current strength of each control point.
"""
return self.point_from_a(self._position_distance)
@property
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b
@property
def middle_point(self):
self.point_from_a(self.attack_distance / 2)
@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
def active_segment(self) -> FrontLineSegment:
"""The FrontLine segment where there can be an active conflict"""
if self._position_distance <= self.segments[0].attack_distance:
return self.segments[0]
remaining_dist = self._position_distance
for segment in self.segments:
if remaining_dist <= segment.attack_distance:
return segment
else:
remaining_dist -= segment.attack_distance
logging.error(
"Frontline attack distance is greater than the sum of its segments"
)
return self.segments[0]
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:
"""
The distance from point "a" where the conflict should occur
according to the current strength of each control point
"""
total_strength = (
self.control_point_a.base.strength + self.control_point_b.base.strength
)
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)
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 _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 = self.theater.frontline_data
if (complex_frontlines) and (
(control_point_ids in complex_frontlines)
or (reversed_cp_ids in complex_frontlines)
):
# The frontline segments must be stored in the correct order for the distance algorithms to work.
# The points in the frontline are ordered from the id before the | to the id after.
# First, check if control point id pair matches in order, and create segments if a match is found.
if control_point_ids in complex_frontlines:
point_pairs = pairwise(complex_frontlines[control_point_ids].points)
for i in point_pairs:
self.segments.append(FrontLineSegment(i[0], i[1]))
# Check the reverse order and build in reverse if found.
elif reversed_cp_ids in complex_frontlines:
point_pairs = pairwise(
reversed(complex_frontlines[reversed_cp_ids].points)
)
for i in point_pairs:
self.segments.append(FrontLineSegment(i[0], i[1]))
# If no complex frontline has been configured, fall back to the old straight line method.
else:
self.segments.append(
FrontLineSegment(
self.control_point_a.position, self.control_point_b.position
)
)
@staticmethod
def load_json_frontlines(
theater: ConflictTheater
) -> Optional[Dict[str, ComplexFrontLine]]:
"""Load complex frontlines from json"""
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"]],
)
for frontline in data
}
except OSError:
logging.warning(
f"Unable to load preset frontlines for {theater.terrain.name}"
)
return None

View File

@ -1,47 +1,2 @@
"""Battlefield front lines.""" """Only here to keep compatibility for save games generated in version 2.2.0"""
from typing import Tuple from theater.conflicttheater import *
from dcs.mapping import Point
from . import ControlPoint, MissionTarget
# TODO: Dedup by moving everything to using this class.
FRONTLINE_MIN_CP_DISTANCE = 5000
def compute_position(control_point_a: ControlPoint,
control_point_b: ControlPoint) -> Point:
a = control_point_a.position
b = control_point_b.position
attack_heading = a.heading_between_point(b)
attack_distance = a.distance_to_point(b)
middle_point = a.point_from_heading(attack_heading, attack_distance / 2)
strength_delta = float(control_point_a.base.strength -
control_point_b.base.strength)
position = middle_point.point_from_heading(attack_heading,
strength_delta *
attack_distance / 2 -
FRONTLINE_MIN_CP_DISTANCE)
return position
class FrontLine(MissionTarget):
"""Defines a front line location between two control points.
Front lines are the area where ground combat happens.
"""
def __init__(self, control_point_a: ControlPoint,
control_point_b: ControlPoint) -> None:
super().__init__(f"Front line {control_point_a}/{control_point_b}",
compute_position(control_point_a, control_point_b))
self.control_point_a = control_point_a
self.control_point_b = control_point_b
@property
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b
def is_friendly(self, to_player: bool) -> bool:
return False

View File

@ -73,7 +73,6 @@ class GameGenerator:
namegen.reset() namegen.reset()
self.prepare_theater() self.prepare_theater()
self.populate_red_airbases() self.populate_red_airbases()
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,
@ -89,7 +88,6 @@ class GameGenerator:
def prepare_theater(self) -> None: def prepare_theater(self) -> None:
to_remove = [] to_remove = []
# Auto-capture half the bases if midgame. # Auto-capture half the bases if midgame.
if self.midgame: if self.midgame:
control_points = self.theater.controlpoints control_points = self.theater.controlpoints