mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
frontline refactoring
`FrontLine` is tightly coupled with `ConflictTheater`. Moved into the same module to prevent circular imports. Moved `ConflictTheater.frontline_data` from class var to instance var to allow save games to have different versions of frontlines.
This commit is contained in:
parent
c20e9e19cb
commit
c1f88b4a5f
@ -370,7 +370,8 @@ 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(front_line.control_point_a,
|
position = Conflict.frontline_position(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)
|
||||||
|
|||||||
14
gen/armor.py
14
gen/armor.py
@ -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.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: # 25 attempt for valid position
|
||||||
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)))
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +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
|
||||||
from theater.frontline import FrontLine
|
|
||||||
|
|
||||||
AIR_DISTANCE = 40000
|
AIR_DISTANCE = 40000
|
||||||
|
|
||||||
@ -136,8 +135,8 @@ class Conflict:
|
|||||||
return from_cp.has_frontline and to_cp.has_frontline
|
return from_cp.has_frontline and to_cp.has_frontline
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
|
def frontline_position(from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int]:
|
||||||
frontline = FrontLine(from_cp, to_cp)
|
frontline = FrontLine(from_cp, to_cp, theater)
|
||||||
attack_heading = frontline.attack_heading
|
attack_heading = frontline.attack_heading
|
||||||
position = frontline.position
|
position = frontline.position
|
||||||
return position, _opposite_heading(attack_heading)
|
return position, _opposite_heading(attack_heading)
|
||||||
@ -160,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(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
|
||||||
|
|
||||||
@ -210,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
|
||||||
|
|
||||||
@ -477,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(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:
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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.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)
|
||||||
|
|||||||
@ -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(from_cp, to_cp)
|
frontline = Conflict.frontline_position(from_cp, to_cp, self.game.theater)
|
||||||
if not frontline:
|
if not frontline:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@ -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(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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,9 +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
|
||||||
from .frontline import FrontLine, ComplexFrontLine
|
|
||||||
|
Numeric = Union[int, float]
|
||||||
|
|
||||||
SIZE_TINY = 150
|
SIZE_TINY = 150
|
||||||
SIZE_SMALL = 600
|
SIZE_SMALL = 600
|
||||||
@ -54,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
|
||||||
@ -61,15 +78,15 @@ class ConflictTheater:
|
|||||||
reference_points: Dict[Tuple[float, float], Tuple[float, float]]
|
reference_points: Dict[Tuple[float, float], Tuple[float, float]]
|
||||||
overview_image: str
|
overview_image: str
|
||||||
landmap: Optional[Landmap]
|
landmap: Optional[Landmap]
|
||||||
frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
|
|
||||||
"""
|
"""
|
||||||
land_poly = None # type: Polygon
|
land_poly = None # type: Polygon
|
||||||
"""
|
"""
|
||||||
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] = []
|
||||||
ConflictTheater.frontline_data = FrontLine.load_json_frontlines(self)
|
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]:
|
||||||
@ -130,7 +147,7 @@ class ConflictTheater:
|
|||||||
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
def conflicts(self, from_player=True) -> Iterator[FrontLine]:
|
||||||
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]
|
||||||
@ -280,4 +297,206 @@ class SyriaTheater(ConflictTheater):
|
|||||||
"day": (8, 16),
|
"day": (8, 16),
|
||||||
"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}"
|
||||||
|
|
||||||
|
@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,235 +1,2 @@
|
|||||||
"""Battlefield front lines."""
|
"""Only here to keep compatibility for save games generated in version 2.2.0"""
|
||||||
from __future__ import annotations
|
from theater.conflicttheater import *
|
||||||
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from itertools import tee
|
|
||||||
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.
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
theater: ConflictTheater
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
control_point_a: ControlPoint,
|
|
||||||
control_point_b: ControlPoint,
|
|
||||||
) -> None:
|
|
||||||
self.control_point_a = control_point_a
|
|
||||||
self.control_point_b = control_point_b
|
|
||||||
self.segments: List[FrontLineSegment] = []
|
|
||||||
self._build_segments()
|
|
||||||
self.name = f"Front line {control_point_a}/{control_point_b}"
|
|
||||||
|
|
||||||
@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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@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"]],
|
|
||||||
)
|
|
||||||
for frontline in data
|
|
||||||
}
|
|
||||||
except OSError:
|
|
||||||
logging.warning(
|
|
||||||
f"Unable to load preset frontlines for {theater.terrain.name}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
@ -40,7 +40,6 @@ from theater.theatergroundobject import (
|
|||||||
LhaGroundObject,
|
LhaGroundObject,
|
||||||
MissileSiteGroundObject, ShipGroundObject,
|
MissileSiteGroundObject, ShipGroundObject,
|
||||||
)
|
)
|
||||||
from theater.frontline import FrontLine
|
|
||||||
|
|
||||||
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user