dcs_liberation/game/commander/tasks/packageplanningtask.py
Dan Albert c65ac5a7cf Move mission range data into the aircraft type.
The doctrine/task limits were capturing a reasonable average for the
era, but it did a bad job for cases like the Harrier vs the Hornet,
which perform similar missions but have drastically different max
ranges. It also forced us into limiting CAS missions (even those flown
by long range aircraft like the A-10) to 50nm since helicopters could
commonly be fragged to them.

This should allow us to design campaigns without needing airfields to be
a max of ~50-100nm apart.
2021-07-17 16:34:13 -07:00

156 lines
5.8 KiB
Python

from __future__ import annotations
import itertools
import operator
from abc import abstractmethod
from dataclasses import dataclass, field
from enum import unique, IntEnum, auto
from typing import TYPE_CHECKING, Optional, Generic, TypeVar, Iterator, Union
from game.commander.missionproposals import ProposedFlight, EscortType, ProposedMission
from game.commander.packagefulfiller import PackageFulfiller
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
from game.commander.theaterstate import TheaterState
from game.data.doctrine import Doctrine
from game.settings import AutoAtoBehavior
from game.theater import MissionTarget
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
from game.utils import Distance, meters
from gen import Package
from gen.flights.flight import FlightType
if TYPE_CHECKING:
from game.coalition import Coalition
MissionTargetT = TypeVar("MissionTargetT", bound=MissionTarget)
@unique
class RangeType(IntEnum):
Detection = auto()
Threat = auto()
# 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)
package: Optional[Package] = field(init=False, default=None)
def __post_init__(self) -> None:
self.flights = []
self.package = Package(self.target)
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player
and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled
):
return False
return self.fulfill_mission(state)
def execute(self, coalition: Coalition) -> None:
if self.package is None:
raise RuntimeError("Attempted to execute failed package planning task")
for flight in self.package.flights:
coalition.aircraft_inventory.claim_for_flight(flight)
coalition.ato.add_package(self.package)
@abstractmethod
def propose_flights(self) -> None:
...
def propose_flight(
self,
task: FlightType,
num_aircraft: int,
escort_type: Optional[EscortType] = None,
) -> None:
self.flights.append(ProposedFlight(task, num_aircraft, escort_type))
@property
def asap(self) -> bool:
return False
def fulfill_mission(self, state: TheaterState) -> bool:
self.propose_flights()
fulfiller = PackageFulfiller(
state.context.coalition,
state.context.theater,
state.available_aircraft,
state.context.settings,
)
self.package = fulfiller.plan_mission(
ProposedMission(self.target, self.flights), state.context.tracer
)
return self.package is not None
def propose_common_escorts(self) -> None:
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
def iter_iads_ranges(
self, state: TheaterState, range_type: RangeType
) -> 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))
if range_type is RangeType.Detection:
target_range = target.max_detection_range()
elif range_type is RangeType.Threat:
target_range = target.max_threat_range()
else:
raise ValueError(f"Unknown RangeType: {range_type}")
if not target_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 - target_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 iter_detecting_iads(
self, state: TheaterState
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
return self.iter_iads_ranges(state, RangeType.Detection)
def iter_iads_threats(
self, state: TheaterState
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
return self.iter_iads_ranges(state, RangeType.Threat)
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
# Non-blocking, but analyzed so we can pick detectors worth eliminating.
for detector in self.iter_detecting_iads(state):
if detector not in state.detecting_air_defenses:
state.detecting_air_defenses.append(detector)
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