From f5ef509af67166802ad893c99a37509f43e9c116 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 20 Nov 2022 11:29:34 -0800 Subject: [PATCH] Fix drop zone display for air assault. Troops must be dropped inside this zone or they won't attack the target. The zone needs to be drawn in the map so players don't break the flight plan by accidentally moving the drop waypoint outside the DZ. I've move the API for doing this out of `PatrollingFlightPlan` in favor of a mixin so this is no longer presented as `engagement_distance` by the flight plan. I don't love that it's still the `commit-boundary` endpoint, but it's fine for now. I don't know why mypy wasn't able to catch this. pycharm is also struggling to understand this class. --- game/ato/flightplans/airassault.py | 9 ++++++++- game/ato/flightplans/cas.py | 9 ++++++++- game/ato/flightplans/patrolling.py | 9 ++++++++- game/ato/flightplans/uizonedisplay.py | 18 +++++++++++++++++ game/server/flights/routes.py | 29 ++++++++------------------- 5 files changed, 50 insertions(+), 24 deletions(-) create mode 100644 game/ato/flightplans/uizonedisplay.py diff --git a/game/ato/flightplans/airassault.py b/game/ato/flightplans/airassault.py index 8df43e57..015d287d 100644 --- a/game/ato/flightplans/airassault.py +++ b/game/ato/flightplans/airassault.py @@ -10,6 +10,7 @@ from game.theater.missiontarget import MissionTarget from game.utils import Distance, feet, meters from .ibuilder import IBuilder from .planningerror import PlanningError +from .uizonedisplay import UiZone, UiZoneDisplay from .waypointbuilder import WaypointBuilder from ..flightwaypoint import FlightWaypointType @@ -45,7 +46,7 @@ class AirAssaultLayout(StandardLayout): yield self.bullseye -class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]): +class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay): @staticmethod def builder_type() -> Type[Builder]: return Builder @@ -70,6 +71,12 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]): def mission_departure_time(self) -> timedelta: return self.package.time_over_target + def ui_zone(self) -> UiZone: + return UiZone( + [self.layout.target.position], + self.ctld_target_zone_radius, + ) + class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]): def layout(self) -> AirAssaultLayout: diff --git a/game/ato/flightplans/cas.py b/game/ato/flightplans/cas.py index 5e49e401..1d9a5996 100644 --- a/game/ato/flightplans/cas.py +++ b/game/ato/flightplans/cas.py @@ -10,6 +10,7 @@ from game.utils import Distance, Speed, kph, meters from .ibuilder import IBuilder from .invalidobjectivelocation import InvalidObjectiveLocation from .patrolling import PatrollingFlightPlan, PatrollingLayout +from .uizonedisplay import UiZone, UiZoneDisplay from .waypointbuilder import WaypointBuilder from ..flightwaypointtype import FlightWaypointType @@ -34,7 +35,7 @@ class CasLayout(PatrollingLayout): yield self.bullseye -class CasFlightPlan(PatrollingFlightPlan[CasLayout]): +class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay): @staticmethod def builder_type() -> Type[Builder]: return Builder @@ -65,6 +66,12 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout]): def dismiss_escort_at(self) -> FlightWaypoint | None: return self.layout.patrol_end + def ui_zone(self) -> UiZone: + return UiZone( + [self.layout.target.position], + self.engagement_distance, + ) + class Builder(IBuilder[CasFlightPlan, CasLayout]): def layout(self) -> CasLayout: diff --git a/game/ato/flightplans/patrolling.py b/game/ato/flightplans/patrolling.py index c531fdfb..eecc1828 100644 --- a/game/ato/flightplans/patrolling.py +++ b/game/ato/flightplans/patrolling.py @@ -9,6 +9,7 @@ from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout from game.typeguard import self_type_guard from game.utils import Distance, Speed +from .uizonedisplay import UiZone, UiZoneDisplay if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint @@ -37,7 +38,7 @@ class PatrollingLayout(StandardLayout): LayoutT = TypeVar("LayoutT", bound=PatrollingLayout) -class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC): +class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC): @property @abstractmethod def patrol_duration(self) -> timedelta: @@ -97,3 +98,9 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC): self, flight_plan: FlightPlan[Any] ) -> TypeGuard[PatrollingFlightPlan[Any]]: return True + + def ui_zone(self) -> UiZone: + return UiZone( + [self.layout.patrol_start.position, self.layout.patrol_end.position], + self.engagement_distance, + ) diff --git a/game/ato/flightplans/uizonedisplay.py b/game/ato/flightplans/uizonedisplay.py new file mode 100644 index 00000000..8d880c87 --- /dev/null +++ b/game/ato/flightplans/uizonedisplay.py @@ -0,0 +1,18 @@ +import abc +from dataclasses import dataclass + +from dcs import Point + +from game.utils import Distance + + +@dataclass(frozen=True) +class UiZone: + points: list[Point] + radius: Distance + + +class UiZoneDisplay(abc.ABC): + @abc.abstractmethod + def ui_zone(self) -> UiZone: + ... diff --git a/game/server/flights/routes.py b/game/server/flights/routes.py index 6ce69315..de1f99bd 100644 --- a/game/server/flights/routes.py +++ b/game/server/flights/routes.py @@ -4,9 +4,7 @@ from fastapi import APIRouter, Depends from shapely.geometry import LineString, Point as ShapelyPoint from game import Game -from game.ato.flightplans.airassault import AirAssaultFlightPlan -from game.ato.flightplans.cas import CasFlightPlan -from game.ato.flightplans.patrolling import PatrollingFlightPlan +from game.ato.flightplans.uizonedisplay import UiZoneDisplay from game.server import GameContext from game.server.flights.models import FlightJs from game.server.leaflet import LeafletPoly, ShapelyUtil @@ -40,23 +38,12 @@ def commit_boundary( flight_id: UUID, game: Game = Depends(GameContext.require) ) -> LeafletPoly: flight = game.db.flights.get(flight_id) - if isinstance(flight.flight_plan, CasFlightPlan) or isinstance( - flight.flight_plan, AirAssaultFlightPlan - ): - # Special Commit boundary for CAS and AirAssault - center = flight.flight_plan.layout.target.position - commit_center = ShapelyPoint(center.x, center.y) - elif isinstance(flight.flight_plan, PatrollingFlightPlan): - # Commit boundary for standard patrolling flight plan - start = flight.flight_plan.layout.patrol_start - end = flight.flight_plan.layout.patrol_end - commit_center = LineString( - [ - ShapelyPoint(start.x, start.y), - ShapelyPoint(end.x, end.y), - ] - ) - else: + if not isinstance(flight.flight_plan, UiZoneDisplay): return [] - bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters) + zone = flight.flight_plan.ui_zone() + if len(zone.points) == 1: + center = ShapelyPoint(zone.points[0].x, zone.points[0].y) + else: + center = LineString([ShapelyPoint(p.x, p.y) for p in zone.points]) + bubble = center.buffer(zone.radius.meters) return ShapelyUtil.poly_to_leaflet(bubble, game.theater)