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.
This commit is contained in:
Dan Albert 2022-11-20 11:29:34 -08:00
parent bf4728fded
commit 1a255969a7
5 changed files with 50 additions and 24 deletions

View File

@ -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:

View File

@ -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
@ -66,6 +67,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:

View File

@ -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,
)

View File

@ -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:
...

View File

@ -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)