mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
This alters the DEAD task planning to be the *least* preferred task, but prevents other tasks from being planned unless they are excepted to be clear of air defenses first. Even so, missions are a guaranteed success so those other missions will still get SEAD escorts if there's potential for a SAM in the area. This means that air defenses that are not protecting a more useful target (like a convoy, armor column, building, etc) will no longer be considered by the mission planner. This isn't *quite* right since we currently only check the target area for air defenses rather than the entire flight plan, so there's a chance that we ignore IADS that have threatened ingress points (though that's mostly solved by the flight plan layout). This also is still slightly limited because it's not checking for aircraft availability at this stage yet, so we may aggressively plan missions that we should be skipping unless we can guarantee that the DEAD mission was planned. However, that's not new behavior.
118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import itertools
|
|
import operator
|
|
from abc import abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING, Optional, Generic, TypeVar, Iterator, Union
|
|
|
|
from game.commander.missionproposals import ProposedFlight, EscortType, ProposedMission
|
|
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
|
from game.commander.theaterstate import TheaterState
|
|
from game.data.doctrine import Doctrine
|
|
from game.profiling import MultiEventTracer
|
|
from game.theater import MissionTarget
|
|
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
|
from game.utils import Distance, meters
|
|
from gen.flights.flight import FlightType
|
|
|
|
if TYPE_CHECKING:
|
|
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
|
|
|
|
|
MissionTargetT = TypeVar("MissionTargetT", bound=MissionTarget)
|
|
|
|
|
|
# TODO: Refactor so that we don't need to call up to the mission planner.
|
|
# Bypass type checker due to https://github.com/python/mypy/issues/5374
|
|
@dataclass # type: ignore
|
|
class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|
target: MissionTargetT
|
|
flights: list[ProposedFlight] = field(init=False)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.flights = []
|
|
|
|
def execute(
|
|
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
|
|
) -> None:
|
|
self.propose_flights(mission_planner.doctrine)
|
|
mission_planner.plan_mission(ProposedMission(self.target, self.flights), tracer)
|
|
|
|
@abstractmethod
|
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
|
...
|
|
|
|
def propose_flight(
|
|
self,
|
|
task: FlightType,
|
|
num_aircraft: int,
|
|
max_distance: Optional[Distance],
|
|
escort_type: Optional[EscortType] = None,
|
|
) -> None:
|
|
if max_distance is None:
|
|
max_distance = Distance.inf()
|
|
self.flights.append(
|
|
ProposedFlight(task, num_aircraft, max_distance, escort_type)
|
|
)
|
|
|
|
@property
|
|
def asap(self) -> bool:
|
|
return False
|
|
|
|
def propose_common_escorts(self, doctrine: Doctrine) -> None:
|
|
self.propose_flight(
|
|
FlightType.SEAD_ESCORT,
|
|
2,
|
|
doctrine.mission_ranges.offensive,
|
|
EscortType.Sead,
|
|
)
|
|
|
|
self.propose_flight(
|
|
FlightType.ESCORT,
|
|
2,
|
|
doctrine.mission_ranges.offensive,
|
|
EscortType.AirToAir,
|
|
)
|
|
|
|
def iter_iads_threats(
|
|
self, state: TheaterState
|
|
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
|
|
target_ranges: list[
|
|
tuple[Union[IadsGroundObject, NavalGroundObject], Distance]
|
|
] = []
|
|
all_iads: Iterator[
|
|
Union[IadsGroundObject, NavalGroundObject]
|
|
] = itertools.chain(state.enemy_air_defenses, state.enemy_ships)
|
|
for target in all_iads:
|
|
distance = meters(target.distance_to(self.target))
|
|
threat_range = target.max_threat_range()
|
|
if not threat_range:
|
|
continue
|
|
# IADS out of range of our target area will have a positive
|
|
# distance_to_threat and should be pruned. The rest have a decreasing
|
|
# distance_to_threat as overlap increases. The most negative distance has
|
|
# the greatest coverage of the target and should be treated as the highest
|
|
# priority threat.
|
|
distance_to_threat = distance - threat_range
|
|
if distance_to_threat > meters(0):
|
|
continue
|
|
target_ranges.append((target, distance_to_threat))
|
|
|
|
# TODO: Prioritize IADS by vulnerability?
|
|
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
|
|
for target, _range in target_ranges:
|
|
yield target
|
|
|
|
def target_area_preconditions_met(
|
|
self, state: TheaterState, ignore_iads: bool = False
|
|
) -> bool:
|
|
"""Checks if the target area has been cleared of threats."""
|
|
threatened = False
|
|
if not ignore_iads:
|
|
for iads_threat in self.iter_iads_threats(state):
|
|
threatened = True
|
|
if iads_threat not in state.threatening_air_defenses:
|
|
state.threatening_air_defenses.append(iads_threat)
|
|
return not threatened
|