diff --git a/game/ato/flightplans/sweep.py b/game/ato/flightplans/sweep.py index 52507c9d..a81eb0b2 100644 --- a/game/ato/flightplans/sweep.py +++ b/game/ato/flightplans/sweep.py @@ -5,12 +5,15 @@ from datetime import datetime, timedelta from typing import Iterator, TYPE_CHECKING, Type from dcs import Point +from dcs.task import Targets -from game.utils import Heading +from game.flightplan import HoldZoneGeometry +from game.flightplan.waypointactions.engagetargets import EngageTargets +from game.flightplan.waypointoptions.formation import Formation +from game.utils import Heading, nautical_miles from .ibuilder import IBuilder from .loiter import LoiterFlightPlan, LoiterLayout from .waypointbuilder import WaypointBuilder -from ...flightplan import HoldZoneGeometry if TYPE_CHECKING: from ..flightwaypoint import FlightWaypoint @@ -89,6 +92,19 @@ class SweepFlightPlan(LoiterFlightPlan[SweepLayout]): def mission_departure_time(self) -> datetime: return self.sweep_end_time + def add_waypoint_actions(self) -> None: + super().add_waypoint_actions() + self.layout.sweep_start.set_option(Formation.LINE_ABREAST_OPEN) + self.layout.sweep_start.add_action( + EngageTargets( + nautical_miles(50), + [ + Targets.All.Air.Planes.Fighters, + Targets.All.Air.Planes.MultiroleFighters, + ], + ) + ) + class Builder(IBuilder[SweepFlightPlan, SweepLayout]): def layout(self) -> SweepLayout: diff --git a/game/ato/flightwaypoint.py b/game/ato/flightwaypoint.py index 7be69688..b261f579 100644 --- a/game/ato/flightwaypoint.py +++ b/game/ato/flightwaypoint.py @@ -8,6 +8,7 @@ from dcs import Point from game.ato.flightwaypointtype import FlightWaypointType from game.flightplan.waypointactions.waypointaction import WaypointAction +from game.flightplan.waypointoptions.waypointoption import WaypointOption from game.theater.theatergroup import TheaterUnit from game.utils import Distance, meters @@ -41,6 +42,7 @@ class FlightWaypoint: min_fuel: float | None = None actions: list[WaypointAction] = field(default_factory=list) + options: dict[str, WaypointOption] = field(default_factory=dict) # These are set very late by the air conflict generator (part of mission # generation). We do it late so that we don't need to propagate changes @@ -52,6 +54,9 @@ class FlightWaypoint: def add_action(self, action: WaypointAction) -> None: self.actions.append(action) + def set_option(self, option: WaypointOption) -> None: + self.options[option.id()] = option + @property def x(self) -> float: return self.position.x diff --git a/game/flightplan/waypointactions/engagetargets.py b/game/flightplan/waypointactions/engagetargets.py new file mode 100644 index 00000000..0df7bb0a --- /dev/null +++ b/game/flightplan/waypointactions/engagetargets.py @@ -0,0 +1,35 @@ +from collections.abc import Iterator +from datetime import datetime, timedelta + +import dcs.task +from dcs.task import Task + +from game.ato.flightstate.actionstate import ActionState +from game.utils import Distance +from .taskcontext import TaskContext +from .waypointaction import WaypointAction + + +class EngageTargets(WaypointAction): + def __init__( + self, + max_distance_from_flight: Distance, + target_types: list[type[dcs.task.TargetType]], + ) -> None: + self._max_distance_from_flight = max_distance_from_flight + self._target_types = target_types + + def update_state( + self, state: ActionState, time: datetime, duration: timedelta + ) -> timedelta: + state.finish() + return duration + + def describe(self) -> str: + return "Searching for targets" + + def iter_tasks(self, ctx: TaskContext) -> Iterator[Task]: + yield dcs.task.EngageTargets( + max_distance=int(self._max_distance_from_flight.meters), + targets=self._target_types, + ) diff --git a/game/flightplan/waypointoptions/__init__.py b/game/flightplan/waypointoptions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/game/flightplan/waypointoptions/formation.py b/game/flightplan/waypointoptions/formation.py new file mode 100644 index 00000000..dcf48297 --- /dev/null +++ b/game/flightplan/waypointoptions/formation.py @@ -0,0 +1,21 @@ +from collections.abc import Iterator +from enum import Enum + +from dcs.task import OptFormation, Task + +from game.flightplan.waypointactions.taskcontext import TaskContext +from game.flightplan.waypointoptions.waypointoption import WaypointOption + + +class Formation(WaypointOption, Enum): + FINGER_FOUR_CLOSE = OptFormation.finger_four_close() + FINGER_FOUR_OPEN = OptFormation.finger_four_open() + LINE_ABREAST_OPEN = OptFormation.line_abreast_open() + SPREAD_FOUR_OPEN = OptFormation.spread_four_open() + TRAIL_OPEN = OptFormation.trail_open() + + def id(self) -> str: + return "formation" + + def iter_tasks(self, ctx: TaskContext) -> Iterator[Task]: + yield self.value diff --git a/game/flightplan/waypointoptions/waypointoption.py b/game/flightplan/waypointoptions/waypointoption.py new file mode 100644 index 00000000..6cdf945f --- /dev/null +++ b/game/flightplan/waypointoptions/waypointoption.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from collections.abc import Iterator + +from dcs.task import Task + +from game.flightplan.waypointactions.taskcontext import TaskContext + + +# Not explicitly an ABC because that prevents subclasses from deriving Enum. +class WaypointOption: + def id(self) -> str: + raise RuntimeError + + def iter_tasks(self, ctx: TaskContext) -> Iterator[Task]: + raise RuntimeError diff --git a/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py b/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py index eb51a235..1c64e0e4 100644 --- a/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py +++ b/game/missiongenerator/aircraft/waypoints/pydcswaypointbuilder.py @@ -91,6 +91,9 @@ class PydcsWaypointBuilder: for action in self.waypoint.actions: for task in action.iter_tasks(ctx): waypoint.add_task(task) + for option in self.waypoint.options.values(): + for task in option.iter_tasks(ctx): + waypoint.add_task(task) def set_waypoint_tot(self, waypoint: MovingPoint, tot: datetime) -> None: self.waypoint.tot = tot diff --git a/game/missiongenerator/aircraft/waypoints/sweepingress.py b/game/missiongenerator/aircraft/waypoints/sweepingress.py deleted file mode 100644 index 7f3e9851..00000000 --- a/game/missiongenerator/aircraft/waypoints/sweepingress.py +++ /dev/null @@ -1,31 +0,0 @@ -import logging - -from dcs.point import MovingPoint -from dcs.task import EngageTargets, OptFormation, Targets - -from game.ato.flightplans.sweep import SweepFlightPlan -from game.utils import nautical_miles -from .pydcswaypointbuilder import PydcsWaypointBuilder - - -class SweepIngressBuilder(PydcsWaypointBuilder): - def add_tasks(self, waypoint: MovingPoint) -> None: - if not isinstance(self.flight.flight_plan, SweepFlightPlan): - flight_plan_type = self.flight.flight_plan.__class__.__name__ - logging.error( - f"Cannot create sweep for {self.flight} because " - f"{flight_plan_type} is not a sweep flight plan." - ) - return - - waypoint.tasks.append( - EngageTargets( - max_distance=int(nautical_miles(50).meters), - targets=[ - Targets.All.Air.Planes.Fighters, - Targets.All.Air.Planes.MultiroleFighters, - ], - ) - ) - - waypoint.tasks.append(OptFormation.line_abreast_open()) diff --git a/game/missiongenerator/aircraft/waypoints/waypointgenerator.py b/game/missiongenerator/aircraft/waypoints/waypointgenerator.py index 0a688c4f..69ac6ef0 100644 --- a/game/missiongenerator/aircraft/waypoints/waypointgenerator.py +++ b/game/missiongenerator/aircraft/waypoints/waypointgenerator.py @@ -41,7 +41,6 @@ from .refuel import RefuelPointBuilder from .seadingress import SeadIngressBuilder from .splitpoint import SplitPointBuilder from .strikeingress import StrikeIngressBuilder -from .sweepingress import SweepIngressBuilder from .target import TargetBuilder @@ -138,7 +137,6 @@ class WaypointGenerator: FlightWaypointType.INGRESS_OCA_RUNWAY: OcaRunwayIngressBuilder, FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder, FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder, - FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder, FlightWaypointType.JOIN: JoinPointBuilder, FlightWaypointType.LANDING_POINT: LandingPointBuilder, FlightWaypointType.PATROL: RaceTrackEndBuilder, diff --git a/tests/flightplan/waypointactions/test_engagetargets.py b/tests/flightplan/waypointactions/test_engagetargets.py new file mode 100644 index 00000000..d69d8e92 --- /dev/null +++ b/tests/flightplan/waypointactions/test_engagetargets.py @@ -0,0 +1,38 @@ +from datetime import datetime, timedelta + +from dcs.task import Targets + +from game.ato.flightstate.actionstate import ActionState +from game.flightplan.waypointactions.engagetargets import EngageTargets +from game.flightplan.waypointactions.taskcontext import TaskContext +from game.utils import meters + + +def test_engage_targets() -> None: + tasks = list( + EngageTargets( + meters(100), [Targets.All.Air.Planes, Targets.All.Air.Helicopters] + ).iter_tasks(TaskContext(datetime.now())) + ) + assert len(tasks) == 1 + task = tasks[0] + assert task.id == "EngageTargets" + assert task.params["targetTypes"] == { + 1: Targets.All.Air.Planes, + 2: Targets.All.Air.Helicopters, + } + assert task.params["value"] == "Planes;Helicopters" + assert task.params["maxDist"] == 100 + + +def test_engage_targets_update_state() -> None: + task = EngageTargets(meters(100), [Targets.All]) + state = ActionState(task) + assert not task.update_state(state, datetime.now(), timedelta()) + assert state.is_finished() + + +def test_engage_targets_description() -> None: + assert ( + EngageTargets(meters(100), [Targets.All]).describe() == "Searching for targets" + ) diff --git a/tests/flightplan/waypointoptions/__init__.py b/tests/flightplan/waypointoptions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/flightplan/waypointoptions/test_formation.py b/tests/flightplan/waypointoptions/test_formation.py new file mode 100644 index 00000000..7f106309 --- /dev/null +++ b/tests/flightplan/waypointoptions/test_formation.py @@ -0,0 +1,13 @@ +from datetime import datetime + +from dcs.task import OptFormation + +from game.flightplan.waypointactions.taskcontext import TaskContext +from game.flightplan.waypointoptions.formation import Formation + + +def test_formation() -> None: + tasks = list(Formation.LINE_ABREAST_OPEN.iter_tasks(TaskContext(datetime.now()))) + assert len(tasks) == 1 + task = tasks[0] + assert task.dict() == OptFormation.line_abreast_open().dict()