diff --git a/game/game.py b/game/game.py index 49ab15ce..c4ac7250 100644 --- a/game/game.py +++ b/game/game.py @@ -220,11 +220,11 @@ class Game: ) def _generate_events(self): - for front_line in self.theater.conflicts(True): + for front_line in self.theater.conflicts(): self._generate_player_event( FrontlineAttackEvent, - front_line.control_point_a, - front_line.control_point_b, + front_line.blue_cp, + front_line.red_cp, ) def adjust_budget(self, amount: float, player: bool) -> None: @@ -459,12 +459,10 @@ class Game: # By default, use the existing frontline conflict position for front_line in self.theater.conflicts(): - position = Conflict.frontline_position( - front_line.control_point_a, front_line.control_point_b, self.theater - ) + position = Conflict.frontline_position(front_line, self.theater) zones.append(position[0]) - zones.append(front_line.control_point_a.position) - zones.append(front_line.control_point_b.position) + zones.append(front_line.blue_cp.position) + zones.append(front_line.red_cp.position) for cp in self.theater.controlpoints: # Don't cull missile sites - their range is long enough to make them diff --git a/game/operation/operation.py b/game/operation/operation.py index 1304c064..73eb0ca0 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -34,7 +34,7 @@ from gen.radios import RadioFrequency, RadioRegistry from gen.tacan import TacanRegistry from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator from .. import db -from ..theater import Airfield +from ..theater import Airfield, FrontLine from ..unitmap import UnitMap if TYPE_CHECKING: @@ -76,8 +76,7 @@ class Operation: for frontline in cls.game.theater.conflicts(): yield Conflict( cls.game.theater, - frontline.control_point_a, - frontline.control_point_b, + frontline, cls.game.player_name, cls.game.enemy_name, cls.game.player_country, @@ -95,8 +94,7 @@ class Operation: ) return Conflict( cls.game.theater, - player_cp, - enemy_cp, + FrontLine(player_cp, enemy_cp, cls.game.theater), cls.game.player_name, cls.game.enemy_name, cls.game.player_country, @@ -399,16 +397,15 @@ class Operation: @classmethod def _generate_ground_conflicts(cls) -> None: """For each frontline in the Operation, generate the ground conflicts and JTACs""" - for front_line in cls.game.theater.conflicts(True): - player_cp = front_line.control_point_a - enemy_cp = front_line.control_point_b + for front_line in cls.game.theater.conflicts(): + player_cp = front_line.blue_cp + enemy_cp = front_line.red_cp conflict = Conflict.frontline_cas_conflict( cls.game.player_name, cls.game.enemy_name, cls.current_mission.country(cls.game.player_country), cls.current_mission.country(cls.game.enemy_country), - player_cp, - enemy_cp, + front_line, cls.game.theater, ) # Generate frontline ops diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index dd66a5e8..d25bdcff 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -3,6 +3,7 @@ from __future__ import annotations import itertools import json import logging +import math from dataclasses import dataclass from functools import cached_property from pathlib import Path @@ -601,12 +602,12 @@ class ConflictTheater: def player_points(self) -> List[ControlPoint]: return list(self.control_points_for(player=True)) - def conflicts(self, from_player=True) -> Iterator[FrontLine]: - 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 + def conflicts(self) -> Iterator[FrontLine]: + for player_cp in [x for x in self.controlpoints if x.captured]: + for enemy_cp in [ + x for x in player_cp.connected_points if not x.is_friendly_to(player_cp) ]: - yield FrontLine(cp, connected_point, self) + yield FrontLine(player_cp, enemy_cp, self) def enemy_points(self) -> List[ControlPoint]: return list(self.control_points_for(player=False)) @@ -646,36 +647,26 @@ class ConflictTheater: Returns a tuple of the two nearest opposing ControlPoints in theater. (player_cp, enemy_cp) """ - all_cp_min_distances = {} - for idx, control_point in enumerate(self.controlpoints): - distances = {} - closest_distance = None - for i, cp in enumerate(self.controlpoints): - if i != idx and cp.captured is not control_point.captured: - dist = cp.position.distance_to_point(control_point.position) - if not closest_distance: - closest_distance = dist - distances[cp.id] = dist - if dist < closest_distance: - distances[cp.id] = dist - closest_cp_id = min(distances, key=distances.get) # type: ignore + seen = set() + min_distance = math.inf + closest_blue = None + closest_red = None + for blue_cp in self.player_points(): + for red_cp in self.enemy_points(): + if (blue_cp, red_cp) in seen: + continue + seen.add((blue_cp, red_cp)) + seen.add((red_cp, blue_cp)) - all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[ - closest_cp_id - ] - closest_opposing_cps = [ - self.find_control_point_by_id(i) - for i in min( - all_cp_min_distances, key=all_cp_min_distances.get - ) # type: ignore - ] # type: List[ControlPoint] - assert len(closest_opposing_cps) == 2 - if closest_opposing_cps[0].captured: - return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps)) - else: - return cast( - Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps)) - ) + dist = red_cp.position.distance_to_point(blue_cp.position) + if dist < min_distance: + closest_red = red_cp + closest_blue = blue_cp + min_distance = dist + + assert closest_blue is not None + assert closest_red is not None + return closest_blue, closest_red def find_control_point_by_id(self, id: int) -> ControlPoint: for i in self.controlpoints: @@ -923,16 +914,16 @@ class FrontLine(MissionTarget): def __init__( self, - control_point_a: ControlPoint, - control_point_b: ControlPoint, + blue_point: ControlPoint, + red_point: ControlPoint, theater: ConflictTheater, ) -> None: - self.control_point_a = control_point_a - self.control_point_b = control_point_b + self.blue_cp = blue_point + self.red_cp = red_point self.segments: List[FrontLineSegment] = [] self.theater = theater self._build_segments() - self.name = f"Front line {control_point_a}/{control_point_b}" + self.name = f"Front line {blue_point}/{red_point}" def is_friendly(self, to_player: bool) -> bool: """Returns True if the objective is in friendly territory.""" @@ -964,7 +955,7 @@ class FrontLine(MissionTarget): @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 + return self.blue_cp, self.red_cp @property def attack_distance(self): @@ -998,7 +989,7 @@ class FrontLine(MissionTarget): 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( + return self.blue_cp.position.point_from_heading( self.segments[0].attack_heading, distance ) remaining_dist = distance @@ -1016,14 +1007,12 @@ class FrontLine(MissionTarget): 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: + total_strength = self.blue_cp.base.strength + self.red_cp.base.strength + if self.blue_cp.base.strength == 0: return self._adjust_for_min_dist(0) - if self.control_point_b.base.strength == 0: + if self.red_cp.base.strength == 0: return self._adjust_for_min_dist(self.attack_distance) - strength_pct = self.control_point_a.base.strength / total_strength + strength_pct = self.blue_cp.base.strength / total_strength return self._adjust_for_min_dist(strength_pct * self.attack_distance) def _adjust_for_min_dist(self, distance: Numeric) -> Numeric: @@ -1044,11 +1033,9 @@ class FrontLine(MissionTarget): 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)] + [str(self.blue_cp.id), str(self.red_cp.id)] ) # from_cp.id|to_cp.id - reversed_cp_ids = "|".join( - [str(self.control_point_b.id), str(self.control_point_a.id)] - ) + reversed_cp_ids = "|".join([str(self.red_cp.id), str(self.blue_cp.id)]) complex_frontlines = self.theater.frontline_data if (complex_frontlines) and ( (control_point_ids in complex_frontlines) @@ -1071,9 +1058,7 @@ class FrontLine(MissionTarget): # 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 - ) + FrontLineSegment(self.blue_cp.position, self.red_cp.position) ) @staticmethod diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index a0d9f75e..81d900aa 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -92,9 +92,9 @@ class AirSupportConflictGenerator: def generate(self): player_cp = ( - self.conflict.from_cp - if self.conflict.from_cp.captured - else self.conflict.to_cp + self.conflict.blue_cp + if self.conflict.blue_cp.captured + else self.conflict.red_cp ) fallback_tanker_number = 0 @@ -107,8 +107,8 @@ class AirSupportConflictGenerator: freq = self.radio_registry.alloc_uhf() tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) tanker_heading = ( - self.conflict.to_cp.position.heading_between_point( - self.conflict.from_cp.position + self.conflict.red_cp.position.heading_between_point( + self.conflict.blue_cp.position ) + TANKER_HEADING_OFFSET * i ) diff --git a/gen/armor.py b/gen/armor.py index 8f075d38..31447e2c 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -134,10 +134,10 @@ class GroundConflictGenerator: def generate(self): position = Conflict.frontline_position( - self.conflict.from_cp, self.conflict.to_cp, self.game.theater + self.conflict.front_line, self.game.theater ) frontline_vector = Conflict.frontline_vector( - self.conflict.from_cp, self.conflict.to_cp, self.game.theater + self.conflict.front_line, self.game.theater ) # Create player groups at random position @@ -156,21 +156,21 @@ class GroundConflictGenerator: player_groups, enemy_groups, self.conflict.heading + 90, - self.conflict.from_cp, - self.conflict.to_cp, + self.conflict.blue_cp, + self.conflict.red_cp, ) self.plan_action_for_groups( self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, - self.conflict.to_cp, - self.conflict.from_cp, + self.conflict.red_cp, + self.conflict.blue_cp, ) # Add JTAC if self.game.player_faction.has_jtac: - n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id) + n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id) code = 1688 - len(self.jtacs) utype = MQ_9_Reaper @@ -191,7 +191,7 @@ class GroundConflictGenerator: OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle) ) frontline = ( - f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}" + f"Frontline {self.conflict.blue_cp.name}/{self.conflict.red_cp.name}" ) # Note: Will need to change if we ever add ground based JTAC. callsign = callsign_for_support_unit(jtac) @@ -213,9 +213,9 @@ class GroundConflictGenerator: logging.warning("Could not find infantry position") return if side == self.conflict.attackers_country: - cp = self.conflict.from_cp + cp = self.conflict.blue_cp else: - cp = self.conflict.to_cp + cp = self.conflict.red_cp if is_player: faction = self.game.player_name @@ -782,9 +782,9 @@ class GroundConflictGenerator: ) -> VehicleGroup: if side == self.conflict.attackers_country: - cp = self.conflict.from_cp + cp = self.conflict.blue_cp else: - cp = self.conflict.to_cp + cp = self.conflict.red_cp logging.info("armorgen: {} for {}".format(unit, side.id)) group = self.mission.vehicle_group( diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 017c4e4e..87029d7b 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -36,8 +36,8 @@ class CommInfo: class FrontLineInfo: def __init__(self, front_line: FrontLine): self.front_line: FrontLine = front_line - self.player_base: ControlPoint = front_line.control_point_a - self.enemy_base: ControlPoint = front_line.control_point_b + self.player_base: ControlPoint = front_line.blue_cp + self.enemy_base: ControlPoint = front_line.red_cp self.player_zero: bool = self.player_base.base.total_armor == 0 self.enemy_zero: bool = self.enemy_base.base.total_armor == 0 self.advantage: bool = ( @@ -164,7 +164,7 @@ class BriefingGenerator(MissionInfoGenerator): def _generate_frontline_info(self) -> None: """Build FrontLineInfo objects from FrontLine type and append to briefing.""" - for front_line in self.game.theater.conflicts(from_player=True): + for front_line in self.game.theater.conflicts(): self.add_frontline(FrontLineInfo(front_line)) # TODO: This should determine if runway is friendly through a method more robust than the existing string match diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 25e1fed3..d4b145d4 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -17,8 +17,7 @@ class Conflict: def __init__( self, theater: ConflictTheater, - from_cp: ControlPoint, - to_cp: ControlPoint, + front_line: FrontLine, attackers_side: str, defenders_side: str, attackers_country: Country, @@ -33,22 +32,28 @@ class Conflict: self.attackers_country = attackers_country self.defenders_country = defenders_country - self.from_cp = from_cp - self.to_cp = to_cp + self.front_line = front_line self.theater = theater self.position = position self.heading = heading self.size = size + @property + def blue_cp(self) -> ControlPoint: + return self.front_line.blue_cp + + @property + def red_cp(self) -> ControlPoint: + return self.front_line.red_cp + @classmethod 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, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater + cls, frontline: FrontLine, theater: ConflictTheater ) -> Tuple[Point, int]: - frontline = FrontLine(from_cp, to_cp, theater) attack_heading = frontline.attack_heading position = cls.find_ground_position( frontline.position, @@ -60,12 +65,12 @@ class Conflict: @classmethod def frontline_vector( - cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater + cls, front_line: FrontLine, theater: ConflictTheater ) -> Tuple[Point, int, int]: """ Returns a vector for a valid frontline location avoiding exclusion zones. """ - center_position, heading = cls.frontline_position(from_cp, to_cp, theater) + center_position, heading = cls.frontline_position(front_line, theater) left_heading = heading_sum(heading, -90) right_heading = heading_sum(heading, 90) left_position = cls.extend_ground_position( @@ -84,18 +89,16 @@ class Conflict: defender_name: str, attacker: Country, defender: Country, - from_cp: ControlPoint, - to_cp: ControlPoint, + front_line: FrontLine, theater: ConflictTheater, ): - assert cls.has_frontline_between(from_cp, to_cp) - position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) + assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp) + position, heading, distance = cls.frontline_vector(front_line, theater) conflict = cls( position=position, heading=heading, theater=theater, - from_cp=from_cp, - to_cp=to_cp, + front_line=front_line, attackers_side=attacker_name, defenders_side=defender_name, attackers_country=attacker, diff --git a/gen/flights/ai_flight_planner.py b/gen/flights/ai_flight_planner.py index 41bcc4f5..90f23f4a 100644 --- a/gen/flights/ai_flight_planner.py +++ b/gen/flights/ai_flight_planner.py @@ -409,13 +409,7 @@ class ObjectiveFinder: def front_lines(self) -> Iterator[FrontLine]: """Iterates over all active front lines in the theater.""" - for cp in self.friendly_control_points(): - for connected in cp.connected_points: - if connected.is_friendly(self.is_player): - continue - - if Conflict.has_frontline_between(cp, connected): - yield FrontLine(cp, connected, self.game.theater) + yield from self.game.theater.conflicts() def vulnerable_control_points(self) -> Iterator[ControlPoint]: """Iterates over friendly CPs that are vulnerable to enemy CPs. @@ -447,19 +441,19 @@ class ObjectiveFinder: def convoys(self) -> Iterator[Convoy]: for front_line in self.front_lines(): - if front_line.control_point_a.is_friendly(self.is_player): - enemy_cp = front_line.control_point_a + if front_line.blue_cp.is_friendly(self.is_player): + enemy_cp = front_line.blue_cp else: - enemy_cp = front_line.control_point_b + enemy_cp = front_line.red_cp yield from self.game.transfers.convoys.travelling_to(enemy_cp) def cargo_ships(self) -> Iterator[CargoShip]: for front_line in self.front_lines(): - if front_line.control_point_a.is_friendly(self.is_player): - enemy_cp = front_line.control_point_a + if front_line.blue_cp.is_friendly(self.is_player): + enemy_cp = front_line.blue_cp else: - enemy_cp = front_line.control_point_b + enemy_cp = front_line.red_cp yield from self.game.transfers.cargo_ships.travelling_to(enemy_cp) diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index d045e3ea..e648e4bb 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -1320,11 +1320,9 @@ class FlightPlanBuilder: def racetrack_for_frontline( self, origin: Point, front_line: FrontLine ) -> Tuple[Point, Point]: - ally_cp, enemy_cp = front_line.control_points - # Find targets waypoints ingress, heading, distance = Conflict.frontline_vector( - ally_cp, enemy_cp, self.game.theater + front_line, self.game.theater ) center = ingress.point_from_heading(heading, distance / 2) orbit_center = center.point_from_heading( @@ -1533,7 +1531,7 @@ class FlightPlanBuilder: raise InvalidObjectiveLocation(flight.flight_type, location) ingress, heading, distance = Conflict.frontline_vector( - location.control_points[0], location.control_points[1], self.game.theater + location, self.game.theater ) center = ingress.point_from_heading(heading, distance / 2) egress = ingress.point_from_heading(heading, distance) diff --git a/gen/visualgen.py b/gen/visualgen.py index 74d768a5..0fa9c335 100644 --- a/gen/visualgen.py +++ b/gen/visualgen.py @@ -98,13 +98,13 @@ class VisualGenerator: def _generate_frontline_smokes(self): for front_line in self.game.theater.conflicts(): - from_cp = front_line.control_point_a - to_cp = front_line.control_point_b + from_cp = front_line.blue_cp + to_cp = front_line.red_cp if from_cp.is_global or to_cp.is_global: continue plane_start, heading, distance = Conflict.frontline_vector( - from_cp, to_cp, self.game.theater + front_line, self.game.theater ) if not plane_start: continue diff --git a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py index 5de3527e..12e3d55e 100644 --- a/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py +++ b/qt_ui/widgets/combos/QPredefinedWaypointSelectionComboBox.py @@ -63,26 +63,19 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox): return i + 1 if self.include_frontlines: - for cp in self.game.theater.controlpoints: - 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(cp, ecp, self.game.theater)[0] - wpt = FlightWaypoint( - FlightWaypointType.CUSTOM, - pos.x, - pos.y, - Distance.from_meters(800), - ) - wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]" - wpt.alt_type = "RADIO" - wpt.pretty_name = wpt.name - wpt.description = "Frontline" - i = add_model_item(i, model, wpt.pretty_name, wpt) + for front_line in self.game.theater.conflicts(): + pos = Conflict.frontline_position(front_line, self.game.theater)[0] + wpt = FlightWaypoint( + FlightWaypointType.CUSTOM, + pos.x, + pos.y, + Distance.from_meters(800), + ) + wpt.name = f"Frontline {front_line.name} [CAS]" + wpt.alt_type = "RADIO" + wpt.pretty_name = wpt.name + wpt.description = "Frontline" + i = add_model_item(i, model, wpt.pretty_name, wpt) if self.include_targets: for cp in self.game.theater.controlpoints: diff --git a/qt_ui/widgets/map/QFrontLine.py b/qt_ui/widgets/map/QFrontLine.py index 2203bf57..0e886d5d 100644 --- a/qt_ui/widgets/map/QFrontLine.py +++ b/qt_ui/widgets/map/QFrontLine.py @@ -101,16 +101,16 @@ class QFrontLine(QGraphicsLineItem): 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.mission_target.blue_cp.base.affect_strength(0.1) + self.mission_target.red_cp.base.affect_strength(-0.1) # Clear the ATO to replan missions affected by the front line. self.game_model.game.reset_ato() 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.mission_target.blue_cp.base.affect_strength(-0.1) + self.mission_target.red_cp.base.affect_strength(0.1) # Clear the ATO to replan missions affected by the front line. self.game_model.game.reset_ato() self.game_model.game.initialize_turn() diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 32f2934d..50359d19 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -865,8 +865,8 @@ class QLiberationMap(QGraphicsView): a[1], b[0], b[1], - frontline.control_point_a, - frontline.control_point_b, + frontline.blue_cp, + frontline.red_cp, convoys, ) ) @@ -914,7 +914,10 @@ class QLiberationMap(QGraphicsView): if convoy is not None: convoys.append(convoy) - frontline = FrontLine(a, b, self.game.theater) + if a.captured: + frontline = FrontLine(a, b, self.game.theater) + else: + frontline = FrontLine(b, a, self.game.theater) if a.front_is_active(b): if DisplayOptions.actual_frontline_pos: self.draw_actual_frontline(scene, frontline, convoys) @@ -947,7 +950,7 @@ class QLiberationMap(QGraphicsView): ) -> None: self.draw_bezier_frontline(scene, frontline, convoys) vector = Conflict.frontline_vector( - frontline.control_point_a, frontline.control_point_b, self.game.theater + frontline.blue_cp, frontline.red_cp, self.game.theater ) left_pos = self._transform_point(vector[0]) right_pos = self._transform_point(