mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge pull request #366 from walterroach/new_frontline
New frontline paths. First step in #287
This commit is contained in:
commit
f3553ced78
@ -369,9 +369,9 @@ 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,
|
||||
front_line.control_point_b)
|
||||
position = Conflict.frontline_position(front_line.control_point_a,
|
||||
front_line.control_point_b,
|
||||
self.theater)
|
||||
points.append(position[0])
|
||||
points.append(front_line.control_point_a.position)
|
||||
points.append(front_line.control_point_b.position)
|
||||
|
||||
17
gen/armor.py
17
gen/armor.py
@ -1,7 +1,8 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.action import AITaskPush
|
||||
@ -36,6 +37,9 @@ from .conflictgen import Conflict
|
||||
from .ground_forces.combat_stance import CombatStance
|
||||
from game.plugins import LuaPluginManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
|
||||
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
|
||||
|
||||
@ -65,7 +69,7 @@ class JtacInfo:
|
||||
|
||||
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.conflict = conflict
|
||||
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
||||
@ -93,7 +97,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, self.game.theater)
|
||||
|
||||
# Create player groups at random position
|
||||
for group in self.player_planned_combat_groups:
|
||||
@ -114,6 +118,8 @@ class GroundConflictGenerator:
|
||||
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
|
||||
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):
|
||||
i = 0
|
||||
while i < 25: # 25 attempt for valid position
|
||||
while i < 1000:
|
||||
heading_diff = -90 if isplayer else 90
|
||||
shifted = conflict_position[0].point_from_heading(self.conflict.heading,
|
||||
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):
|
||||
return final_position
|
||||
else:
|
||||
i = i + 1
|
||||
i += 1
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import os
|
||||
import random
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from theater.frontline import FrontLine
|
||||
from theater import FrontLine
|
||||
from typing import List, Dict, TYPE_CHECKING
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ from typing import Tuple
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
|
||||
from theater import ConflictTheater, ControlPoint
|
||||
from theater import ConflictTheater, ControlPoint, FrontLine
|
||||
|
||||
AIR_DISTANCE = 40000
|
||||
|
||||
@ -134,14 +134,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, theater: ConflictTheater) -> Tuple[Point, int]:
|
||||
frontline = FrontLine(from_cp, to_cp, theater)
|
||||
attack_heading = frontline.attack_heading
|
||||
position = frontline.position
|
||||
return position, _opposite_heading(attack_heading)
|
||||
|
||||
|
||||
@ -162,7 +159,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, theater)
|
||||
center_position, heading = frontline
|
||||
left_position, right_position = None, None
|
||||
|
||||
@ -212,7 +209,7 @@ class Conflict:
|
||||
@classmethod
|
||||
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
pos = initial
|
||||
for _ in range(0, int(max_distance), 500):
|
||||
for _ in range(0, int(max_distance), 100):
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
|
||||
@ -479,7 +476,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, theater)
|
||||
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:
|
||||
|
||||
@ -321,7 +321,7 @@ class ObjectiveFinder:
|
||||
continue
|
||||
|
||||
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]:
|
||||
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
|
||||
|
||||
@ -355,7 +355,7 @@ class GroundObjectsGenerator:
|
||||
"""
|
||||
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):
|
||||
self.m = mission
|
||||
self.conflict = conflict
|
||||
@ -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, self.game.theater)
|
||||
heading -= 90
|
||||
|
||||
initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE)
|
||||
|
||||
@ -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, self.game.theater)
|
||||
if not frontline:
|
||||
continue
|
||||
|
||||
|
||||
@ -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, self.game.theater)[0]
|
||||
wpt = FlightWaypoint(
|
||||
FlightWaypointType.CUSTOM,
|
||||
pos.x,
|
||||
|
||||
@ -14,8 +14,10 @@ from PySide2.QtWidgets import (
|
||||
|
||||
import qt_ui.uiconstants as const
|
||||
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 theater.missiontarget import MissionTarget
|
||||
from theater import FrontLine
|
||||
|
||||
|
||||
class QFrontLine(QGraphicsLineItem):
|
||||
@ -26,9 +28,10 @@ class QFrontLine(QGraphicsLineItem):
|
||||
"""
|
||||
|
||||
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)
|
||||
self.mission_target = mission_target
|
||||
self.game_model = game_model
|
||||
self.new_package_dialog: Optional[QNewPackageDialog] = None
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
@ -55,6 +58,14 @@ class QFrontLine(QGraphicsLineItem):
|
||||
new_package_action.triggered.connect(self.open_new_package_dialog)
|
||||
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())
|
||||
|
||||
@property
|
||||
@ -80,3 +91,16 @@ class QFrontLine(QGraphicsLineItem):
|
||||
def open_new_package_dialog(self) -> None:
|
||||
"""Opens the dialog for planning a new mission package."""
|
||||
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)
|
||||
|
||||
@ -2,7 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
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.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.QMapGroundObject import QMapGroundObject
|
||||
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 (
|
||||
EwrGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
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):
|
||||
WAYPOINT_SIZE = 4
|
||||
@ -189,6 +218,67 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
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):
|
||||
scene = self.scene()
|
||||
scene.clear()
|
||||
@ -205,14 +295,7 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
# Display Culling
|
||||
if DisplayOptions.culling and self.game.settings.perf_culling:
|
||||
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"])
|
||||
self.display_culling()
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
|
||||
@ -230,45 +313,7 @@ class QLiberationMap(QGraphicsView):
|
||||
pen = QPen(brush=CONST.COLORS[enemyColor])
|
||||
brush = CONST.COLORS[enemyColor+"_transparent"]
|
||||
|
||||
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_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)
|
||||
self.draw_ground_objects(scene, cp)
|
||||
|
||||
for cp in self.game.theater.enemy_points():
|
||||
if DisplayOptions.lines:
|
||||
@ -399,36 +444,45 @@ class QLiberationMap(QGraphicsView):
|
||||
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):
|
||||
scene = self.scene()
|
||||
pos = self._transform_point(cp.position)
|
||||
for connected_cp in cp.connected_points:
|
||||
pos2 = self._transform_point(connected_cp.position)
|
||||
if not cp.captured:
|
||||
color = CONST.COLORS["dark_"+enemyColor]
|
||||
elif cp.captured:
|
||||
color = CONST.COLORS["dark_"+playerColor]
|
||||
else:
|
||||
color = CONST.COLORS["dark_"+enemyColor]
|
||||
|
||||
color = CONST.COLORS["dark_"+playerColor]
|
||||
pen = QPen(brush=color)
|
||||
pen.setColor(color)
|
||||
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 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)
|
||||
posx = frontline.position
|
||||
h = frontline.attack_heading
|
||||
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)
|
||||
p2 = point_from_heading(pos2[0], pos2[1], h, 25)
|
||||
scene.addItem(QFrontLine(p1[0], p1[1], p2[0], p2[1],
|
||||
FrontLine(cp, connected_cp)))
|
||||
frontline, self.game_model))
|
||||
|
||||
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):
|
||||
|
||||
@ -451,7 +505,7 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
#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_img = self.game.theater.reference_points[point_a]
|
||||
|
||||
@ -501,18 +555,11 @@ class QLiberationMap(QGraphicsView):
|
||||
return CONST.COLORS[f"{name}_transparent"]
|
||||
|
||||
def threat_pen(self, player: bool) -> QPen:
|
||||
if player:
|
||||
color = "blue"
|
||||
else:
|
||||
color = "red"
|
||||
qpen = QPen(CONST.COLORS[color])
|
||||
return qpen
|
||||
color = "blue" if player else "red"
|
||||
return QPen(CONST.COLORS[color])
|
||||
|
||||
def detection_pen(self, player: bool) -> QPen:
|
||||
if player:
|
||||
color = "purple"
|
||||
else:
|
||||
color = "yellow"
|
||||
color = "purple" if player else "yellow"
|
||||
qpen = QPen(CONST.COLORS[color])
|
||||
qpen.setStyle(Qt.DotLine)
|
||||
return qpen
|
||||
|
||||
1
resources/frontlines/caucasus.json
Normal file
1
resources/frontlines/caucasus.json
Normal file
File diff suppressed because one or more lines are too long
BIN
resources/mizdata/caucasus/caucusus_frontline.miz
Normal file
BIN
resources/mizdata/caucasus/caucusus_frontline.miz
Normal file
Binary file not shown.
74
resources/tools/generate_frontlines.py
Normal file
74
resources/tools/generate_frontlines.py
Normal 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")
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
from .base import *
|
||||
from .conflicttheater import *
|
||||
from .controlpoint import *
|
||||
from .frontline import FrontLine
|
||||
from .missiontarget import MissionTarget
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
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.terrain import (
|
||||
@ -13,11 +18,10 @@ from dcs.terrain import (
|
||||
)
|
||||
from dcs.terrain.terrain import Terrain
|
||||
|
||||
from .controlpoint import ControlPoint
|
||||
from .controlpoint import ControlPoint, MissionTarget
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import FrontLine
|
||||
Numeric = Union[int, float]
|
||||
|
||||
SIZE_TINY = 150
|
||||
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_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:
|
||||
terrain: Terrain
|
||||
@ -67,9 +82,11 @@ class ConflictTheater:
|
||||
land_poly = None # type: Polygon
|
||||
"""
|
||||
daytime_map: Dict[str, Tuple[int, int]]
|
||||
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
||||
|
||||
def __init__(self):
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
self.frontline_data = FrontLine.load_json_frontlines(self)
|
||||
"""
|
||||
self.land_poly = geometry.Polygon(self.landmap[0][0])
|
||||
for x in self.landmap[1]:
|
||||
@ -128,10 +145,9 @@ class ConflictTheater:
|
||||
return [point for point in self.controlpoints if point.captured]
|
||||
|
||||
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 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]:
|
||||
return [point for point in self.controlpoints if not point.captured]
|
||||
@ -183,7 +199,6 @@ class ConflictTheater:
|
||||
theater = theaters[data["theater"]]
|
||||
t = theater()
|
||||
cps = {}
|
||||
|
||||
for p in data["player_points"]:
|
||||
cp = t.add_json_cp(theater, p)
|
||||
cp.captured = True
|
||||
@ -232,7 +247,6 @@ class PersianGulfTheater(ConflictTheater):
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class NevadaTheater(ConflictTheater):
|
||||
terrain = nevada.Nevada()
|
||||
overview_image = "nevada.gif"
|
||||
@ -246,7 +260,6 @@ class NevadaTheater(ConflictTheater):
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class NormandyTheater(ConflictTheater):
|
||||
terrain = normandy.Normandy()
|
||||
overview_image = "normandy.gif"
|
||||
@ -260,7 +273,6 @@ class NormandyTheater(ConflictTheater):
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class TheChannelTheater(ConflictTheater):
|
||||
terrain = thechannel.TheChannel()
|
||||
overview_image = "thechannel.gif"
|
||||
@ -274,7 +286,6 @@ class TheChannelTheater(ConflictTheater):
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
|
||||
class SyriaTheater(ConflictTheater):
|
||||
terrain = syria.Syria()
|
||||
overview_image = "syria.gif"
|
||||
@ -287,3 +298,209 @@ class SyriaTheater(ConflictTheater):
|
||||
"dusk": (16, 18),
|
||||
"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
|
||||
|
||||
@ -1,47 +1,2 @@
|
||||
"""Battlefield front lines."""
|
||||
from typing import Tuple
|
||||
|
||||
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
|
||||
"""Only here to keep compatibility for save games generated in version 2.2.0"""
|
||||
from theater.conflicttheater import *
|
||||
|
||||
@ -73,7 +73,6 @@ class GameGenerator:
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
self.populate_red_airbases()
|
||||
|
||||
game = Game(player_name=self.player,
|
||||
enemy_name=self.enemy,
|
||||
theater=self.theater,
|
||||
@ -89,7 +88,6 @@ class GameGenerator:
|
||||
|
||||
def prepare_theater(self) -> None:
|
||||
to_remove = []
|
||||
|
||||
# Auto-capture half the bases if midgame.
|
||||
if self.midgame:
|
||||
control_points = self.theater.controlpoints
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user