diff --git a/changelog.md b/changelog.md index 6ab94362..6a873d60 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,7 @@ Saves from 2.5 are not compatible with 3.0. * **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions. * **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's). * **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups. +* **[Flight Planner]** Flight plans now include bullseye waypoints. * **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows. * **[UI]** Added new web based map UI. This is mostly functional but many of the old display options are a WIP. Revert to the old map with --old-map. * **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present. diff --git a/game/game.py b/game/game.py index 637c15b3..770c849a 100644 --- a/game/game.py +++ b/game/game.py @@ -34,6 +34,7 @@ from .procurement import AircraftProcurementRequest, ProcurementAi from .profiling import logged_duration from .settings import Settings from .theater import ConflictTheater +from .theater.bullseye import Bullseye from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from .threatzones import ThreatZones from .transfers import PendingTransfers @@ -130,6 +131,9 @@ class Game: self.blue_ato = AirTaskingOrder() self.red_ato = AirTaskingOrder() + self.blue_bullseye = Bullseye(Point(0, 0)) + self.red_bullseye = Bullseye(Point(0, 0)) + self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints) self.transfers = PendingTransfers(self) @@ -201,6 +205,11 @@ class Game: return self.player_faction return self.enemy_faction + def bullseye_for(self, player: bool) -> Bullseye: + if player: + return self.blue_bullseye + return self.red_bullseye + def _roll(self, prob, mult): if self.settings.version == "dev": # always generate all events for dev @@ -337,10 +346,17 @@ class Game: return TurnState.CONTINUE + def set_bullseye(self) -> None: + player_cp, enemy_cp = self.theater.closest_opposing_control_points() + self.blue_bullseye = Bullseye(enemy_cp.position) + self.red_bullseye = Bullseye(player_cp.position) + def initialize_turn(self) -> None: self.events = [] self._generate_events() + self.set_bullseye() + # Update statistics self.game_stats.update(self) diff --git a/game/operation/operation.py b/game/operation/operation.py index 8297a599..3899155a 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -108,8 +108,12 @@ class Operation: @classmethod def _setup_mission_coalitions(cls): - cls.current_mission.coalition["blue"] = Coalition("blue") - cls.current_mission.coalition["red"] = Coalition("red") + cls.current_mission.coalition["blue"] = Coalition( + "blue", bullseye=cls.game.blue_bullseye.to_pydcs() + ) + cls.current_mission.coalition["red"] = Coalition( + "red", bullseye=cls.game.red_bullseye.to_pydcs() + ) p_country = cls.game.player_country e_country = cls.game.enemy_country diff --git a/game/theater/bullseye.py b/game/theater/bullseye.py new file mode 100644 index 00000000..6d39821c --- /dev/null +++ b/game/theater/bullseye.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, TYPE_CHECKING + +from dcs import Point + +from game.theater import LatLon + +if TYPE_CHECKING: + from game.theater import ConflictTheater + + +@dataclass +class Bullseye: + position: Point + + @classmethod + def from_pydcs(cls, bulls: Dict[str, float]) -> Bullseye: + return cls(Point(bulls["x"], bulls["y"])) + + def to_pydcs(self) -> Dict[str, float]: + return {"x": self.position.x, "y": self.position.y} + + def to_lat_lon(self, theater: ConflictTheater) -> LatLon: + return theater.point_to_ll(self.position) diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index 7e9fa0fe..26192cb7 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -41,6 +41,7 @@ from dcs.unitgroup import ( ) from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed +from .latlon import LatLon from ..scenery_group import SceneryGroup from pyproj import CRS, Transformer from shapely import geometry, ops @@ -582,15 +583,6 @@ class ReferencePoint: image_coordinates: Point -@dataclass(frozen=True) -class LatLon: - latitude: float - longitude: float - - def as_list(self) -> List[float]: - return [self.latitude, self.longitude] - - class ConflictTheater: terrain: Terrain diff --git a/game/theater/latlon.py b/game/theater/latlon.py new file mode 100644 index 00000000..dde2e442 --- /dev/null +++ b/game/theater/latlon.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass(frozen=True) +class LatLon: + latitude: float + longitude: float + + def as_list(self) -> List[float]: + return [self.latitude, self.longitude] diff --git a/gen/flights/flight.py b/gen/flights/flight.py index 397399ed..d5f95862 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -79,6 +79,7 @@ class FlightWaypointType(Enum): INGRESS_OCA_AIRCRAFT = 25 PICKUP = 26 DROP_OFF = 27 + BULLSEYE = 28 class FlightWaypoint: diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index e648e4bb..b18d8a91 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -426,6 +426,7 @@ class BarCapFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint land: FlightWaypoint divert: Optional[FlightWaypoint] + bullseye: FlightWaypoint def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.takeoff @@ -438,6 +439,7 @@ class BarCapFlightPlan(PatrollingFlightPlan): yield self.land if self.divert is not None: yield self.divert + yield self.bullseye @dataclass(frozen=True) @@ -446,6 +448,7 @@ class CasFlightPlan(PatrollingFlightPlan): target: FlightWaypoint land: FlightWaypoint divert: Optional[FlightWaypoint] + bullseye: FlightWaypoint def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.takeoff @@ -459,6 +462,7 @@ class CasFlightPlan(PatrollingFlightPlan): yield self.land if self.divert is not None: yield self.divert + yield self.bullseye def request_escort_at(self) -> Optional[FlightWaypoint]: return self.patrol_start @@ -472,6 +476,7 @@ class TarCapFlightPlan(PatrollingFlightPlan): takeoff: FlightWaypoint land: FlightWaypoint divert: Optional[FlightWaypoint] + bullseye: FlightWaypoint lead_time: timedelta def iter_waypoints(self) -> Iterator[FlightWaypoint]: @@ -485,6 +490,7 @@ class TarCapFlightPlan(PatrollingFlightPlan): yield self.land if self.divert is not None: yield self.divert + yield self.bullseye @property def tot_offset(self) -> timedelta: @@ -523,6 +529,7 @@ class StrikeFlightPlan(FormationFlightPlan): nav_from: List[FlightWaypoint] land: FlightWaypoint divert: Optional[FlightWaypoint] + bullseye: FlightWaypoint def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.takeoff @@ -537,6 +544,7 @@ class StrikeFlightPlan(FormationFlightPlan): yield self.land if self.divert is not None: yield self.divert + yield self.bullseye @property def package_speed_waypoints(self) -> Set[FlightWaypoint]: @@ -641,6 +649,7 @@ class SweepFlightPlan(LoiterFlightPlan): nav_from: List[FlightWaypoint] land: FlightWaypoint divert: Optional[FlightWaypoint] + bullseye: FlightWaypoint lead_time: timedelta def iter_waypoints(self) -> Iterator[FlightWaypoint]: @@ -653,6 +662,7 @@ class SweepFlightPlan(LoiterFlightPlan): yield self.land if self.divert is not None: yield self.divert + yield self.bullseye @property def tot_waypoint(self) -> Optional[FlightWaypoint]: @@ -704,6 +714,7 @@ class AwacsFlightPlan(LoiterFlightPlan): nav_from: List[FlightWaypoint] land: FlightWaypoint divert: Optional[FlightWaypoint] + bullseye: FlightWaypoint def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.takeoff @@ -713,6 +724,7 @@ class AwacsFlightPlan(LoiterFlightPlan): yield self.land if self.divert is not None: yield self.divert + yield self.bullseye @property def mission_start_time(self) -> Optional[timedelta]: @@ -746,6 +758,7 @@ class AirliftFlightPlan(FlightPlan): nav_to_home: List[FlightWaypoint] land: FlightWaypoint divert: Optional[FlightWaypoint] + bullseye: FlightWaypoint def iter_waypoints(self) -> Iterator[FlightWaypoint]: yield self.takeoff @@ -758,6 +771,7 @@ class AirliftFlightPlan(FlightPlan): yield self.land if self.divert is not None: yield self.divert + yield self.bullseye @property def tot_waypoint(self) -> Optional[FlightWaypoint]: @@ -1053,6 +1067,7 @@ class FlightPlanBuilder: ), land=builder.land(flight.arrival), divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), hold=start, hold_duration=timedelta(hours=4), ) @@ -1151,6 +1166,7 @@ class FlightPlanBuilder: patrol_end=end, land=builder.land(flight.arrival), divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), ) def generate_sweep(self, flight: Flight) -> SweepFlightPlan: @@ -1187,6 +1203,7 @@ class FlightPlanBuilder: sweep_end=end, land=builder.land(flight.arrival), divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), ) def generate_transport(self, flight: Flight) -> AirliftFlightPlan: @@ -1238,6 +1255,7 @@ class FlightPlanBuilder: ), land=builder.land(flight.arrival), divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), ) def racetrack_for_objective( @@ -1389,6 +1407,7 @@ class FlightPlanBuilder: patrol_end=end, land=builder.land(flight.arrival), divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), ) def generate_dead( @@ -1517,6 +1536,7 @@ class FlightPlanBuilder: ), land=builder.land(flight.arrival), divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), ) def generate_cas(self, flight: Flight) -> CasFlightPlan: @@ -1562,6 +1582,7 @@ class FlightPlanBuilder: patrol_end=builder.egress(egress, location), land=builder.land(flight.arrival), divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), ) @staticmethod @@ -1696,6 +1717,7 @@ class FlightPlanBuilder: ), land=builder.land(flight.arrival), divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), ) def _retreating_rendezvous_point(self, attack_transition: Point) -> Point: diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index ffb1e6ab..670cdc01 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -50,6 +50,7 @@ class WaypointBuilder: self.threat_zones = game.threat_zone_for(not player) self.navmesh = game.navmesh_for(player) self.targets = targets + self._bullseye = game.bullseye_for(player) @property def is_helo(self) -> bool: @@ -145,6 +146,19 @@ class WaypointBuilder: waypoint.only_for_player = True return waypoint + def bullseye(self) -> FlightWaypoint: + waypoint = FlightWaypoint( + FlightWaypointType.BULLSEYE, + self._bullseye.position.x, + self._bullseye.position.y, + meters(0), + ) + waypoint.pretty_name = "Bullseye" + waypoint.description = "Bullseye" + waypoint.name = "BULLSEYE" + waypoint.only_for_player = True + return waypoint + def hold(self, position: Point) -> FlightWaypoint: waypoint = FlightWaypoint( FlightWaypointType.LOITER, diff --git a/gen/triggergen.py b/gen/triggergen.py index 7df4838f..fc68f96e 100644 --- a/gen/triggergen.py +++ b/gen/triggergen.py @@ -209,16 +209,6 @@ class TriggersGenerator: player_coalition = "blue" enemy_coalition = "red" - player_cp, enemy_cp = self.game.theater.closest_opposing_control_points() - self.mission.coalition["blue"].bullseye = { - "x": enemy_cp.position.x, - "y": enemy_cp.position.y, - } - self.mission.coalition["red"].bullseye = { - "x": player_cp.position.x, - "y": player_cp.position.y, - } - self._set_skill(player_coalition, enemy_coalition) self._set_allegiances(player_coalition, enemy_coalition) self._gen_markers() diff --git a/qt_ui/widgets/map/mapmodel.py b/qt_ui/widgets/map/mapmodel.py index 0a77094f..bdb379d3 100644 --- a/qt_ui/widgets/map/mapmodel.py +++ b/qt_ui/widgets/map/mapmodel.py @@ -375,6 +375,7 @@ class WaypointJs(QObject): timingChanged = Signal() isTakeoffChanged = Signal() isDivertChanged = Signal() + isBullseyeChanged = Signal() def __init__( self, @@ -439,6 +440,10 @@ class WaypointJs(QObject): def isDivert(self) -> bool: return self.waypoint.waypoint_type is FlightWaypointType.DIVERT + @Property(bool, notify=isBullseyeChanged) + def isBullseye(self) -> bool: + return self.waypoint.waypoint_type is FlightWaypointType.BULLSEYE + @Slot(list, result=str) def setPosition(self, position: LeafletLatLon) -> str: point = self.theater.ll_to_point(LatLon(*position)) diff --git a/resources/ui/map/map.js b/resources/ui/map/map.js index 3aade142..1b421520 100644 --- a/resources/ui/map/map.js +++ b/resources/ui/map/map.js @@ -570,7 +570,7 @@ class Waypoint { } includeInPath() { - return !this.waypoint.isDivert; + return !this.waypoint.isDivert && !this.waypoint.isBullseye; } }