diff --git a/game/game.py b/game/game.py index 4cea586c..734b3ff1 100644 --- a/game/game.py +++ b/game/game.py @@ -273,6 +273,9 @@ class Game: """Initialization for the first turn of the game.""" from .sim import GameUpdateEvents + for control_point in self.theater.controlpoints: + control_point.initialize_turn_0() + self.blue.preinit_turn_0() self.red.preinit_turn_0() # We don't need to actually stream events for turn zero because we haven't given diff --git a/game/missiongenerator/convoygenerator.py b/game/missiongenerator/convoygenerator.py index a9f38df7..086c8b92 100644 --- a/game/missiongenerator/convoygenerator.py +++ b/game/missiongenerator/convoygenerator.py @@ -10,7 +10,6 @@ from dcs.unit import Vehicle from dcs.unitgroup import VehicleGroup from game.dcs.groundunittype import GroundUnitType -from game.theater import FrontLine from game.transfers import Convoy from game.unitmap import UnitMap from game.utils import kph @@ -46,7 +45,7 @@ class ConvoyGenerator: # convoys_travel_full_distance is disabled, so have the convoy only move the first segment on the route. # This option aims to remove long routes for ground vehicles between control points, # since the CPU load for pathfinding long routes on DCS can be pretty heavy. - frontline = FrontLine(convoy.origin, convoy.destination) + frontline = convoy.origin.front_line_with(convoy.destination) # Select the first route segment from the origin towards the destination # so the convoy spawns at the origin CP. This allows the convoy to be diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 240bc38f..775d010b 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -137,11 +137,8 @@ class ConflictTheater: return list(self.control_points_for(player=True)) 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(player_cp, enemy_cp) + for cp in self.player_points(): + yield from cp.front_lines.values() def enemy_points(self) -> List[ControlPoint]: return list(self.control_points_for(player=False)) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index d96b0748..621d0ac0 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -43,6 +43,7 @@ from game.sidc import ( ) from game.utils import Heading from .base import Base +from .frontline import FrontLine from .missiontarget import MissionTarget from .theatergroundobject import ( GenericCarrierGroundObject, @@ -62,7 +63,7 @@ if TYPE_CHECKING: from game.squadrons.squadron import Squadron from ..coalition import Coalition from ..transfers import PendingTransfers - from . import ConflictTheater + from .conflicttheater import ConflictTheater FREE_FRONTLINE_UNIT_SUPPLY: int = 15 AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12 @@ -287,15 +288,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): # the distance of the circle on the map. CAPTURE_DISTANCE = nautical_miles(2) - position = None # type: Point - name = None # type: str - - has_frontline = True - - alt = 0 - # TODO: Only airbases have IDs. - # TODO: has_frontline is only reasonable for airbases. # TODO: cptype is obsolete. def __init__( self, @@ -304,7 +297,6 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): position: Point, at: StartingPosition, starts_blue: bool, - has_frontline: bool = True, cptype: ControlPointType = ControlPointType.AIRBASE, ) -> None: super().__init__(name, position) @@ -319,8 +311,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self._coalition: Optional[Coalition] = None self.captured_invert = False + self.front_lines: dict[ControlPoint, FrontLine] = {} # TODO: Should be Airbase specific. - self.has_frontline = has_frontline self.connected_points: List[ControlPoint] = [] self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {} self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {} @@ -347,6 +339,41 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): assert self._coalition is None self._coalition = game.coalition_for(self.starts_blue) + def initialize_turn_0(self) -> None: + self._recreate_front_lines() + + def _recreate_front_lines(self) -> None: + self._clear_front_lines() + for connection in self.convoy_routes.keys(): + if not connection.front_line_active_with( + self + ) and not connection.is_friendly_to(self): + self._create_front_line_with(connection) + + def _create_front_line_with(self, connection: ControlPoint) -> None: + blue, red = FrontLine.sort_control_points(self, connection) + front = FrontLine(blue, red) + self.front_lines[connection] = front + connection.front_lines[self] = front + + def _remove_front_line_with(self, connection: ControlPoint) -> None: + del self.front_lines[connection] + del connection.front_lines[self] + + def _clear_front_lines(self) -> None: + for opponent in list(self.front_lines.keys()): + self._remove_front_line_with(opponent) + + @property + def has_frontline(self) -> bool: + return bool(self.front_lines) + + def front_line_active_with(self, other: ControlPoint) -> bool: + return other in self.front_lines + + def front_line_with(self, other: ControlPoint) -> FrontLine: + return self.front_lines[other] + @property def captured(self) -> bool: return self.coalition.player @@ -695,6 +722,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC): self._coalition = new_coalition self.base.set_strength_to_minimum() + self._recreate_front_lines() @property def required_aircraft_start_type(self) -> Optional[StartType]: @@ -894,7 +922,6 @@ class Airfield(ControlPoint): airport.position, airport, starts_blue, - has_frontline=True, cptype=ControlPointType.AIRBASE, ) self.airport = airport @@ -1083,7 +1110,6 @@ class Carrier(NavalControlPoint): at, at, starts_blue, - has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP, ) @@ -1128,7 +1154,6 @@ class Lha(NavalControlPoint): at, at, starts_blue, - has_frontline=False, cptype=ControlPointType.LHA_GROUP, ) @@ -1166,7 +1191,6 @@ class OffMapSpawn(ControlPoint): position, position, starts_blue, - has_frontline=False, cptype=ControlPointType.OFF_MAP, ) @@ -1231,7 +1255,6 @@ class Fob(ControlPoint): at, at, starts_blue, - has_frontline=True, cptype=ControlPointType.FOB, ) self.name = name diff --git a/game/theater/frontline.py b/game/theater/frontline.py index 4a3ab51a..a17030c9 100644 --- a/game/theater/frontline.py +++ b/game/theater/frontline.py @@ -2,15 +2,16 @@ from __future__ import annotations import logging from dataclasses import dataclass -from typing import Iterator, List, Tuple, Any, TYPE_CHECKING +from typing import Any, Iterator, List, TYPE_CHECKING, Tuple from dcs.mapping import Point -from .controlpoint import ControlPoint, MissionTarget +from .missiontarget import MissionTarget from ..utils import Heading, pairwise if TYPE_CHECKING: from game.ato import FlightType + from .controlpoint import ControlPoint FRONTLINE_MIN_CP_DISTANCE = 5000 @@ -193,3 +194,15 @@ class FrontLine(MissionTarget): ): distance = FRONTLINE_MIN_CP_DISTANCE return distance + + @staticmethod + def sort_control_points( + a: ControlPoint, b: ControlPoint + ) -> tuple[ControlPoint, ControlPoint]: + if a.is_friendly_to(b): + raise ValueError( + "Cannot sort control points that are friendly to each other" + ) + if a.captured: + return a, b + return b, a diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index bee42a62..9cc55945 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -13,6 +13,8 @@ from PySide2.QtWidgets import ( from game import Game from game.ato.flighttype import FlightType from game.config import RUNWAY_REPAIR_COST +from game.server import EventStream +from game.sim import GameUpdateEvents from game.theater import ( AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION, ControlPoint, @@ -125,7 +127,9 @@ class QBaseMenu2(QDialog): self.cp.capture(self.game_model.game, for_player=not self.cp.captured) # Reinitialized ground planners and the like. The ATO needs to be reset because # missions planned against the flipped base are no longer valid. - self.game_model.game.initialize_turn() + events = GameUpdateEvents() + self.game_model.game.initialize_turn(events) + EventStream.put_nowait(events) GameUpdateSignal.get_instance().updateGame(self.game_model.game) @property