mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Reduce mission planning dependence on Game.
This commit is contained in:
parent
17c19d453b
commit
24f6aff8c8
@ -11,7 +11,7 @@ from game.navmesh import NavMesh
|
||||
from game.profiling import logged_duration, MultiEventTracer
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import PendingTransfers
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner, MissionScheduler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -181,13 +181,20 @@ class Coalition:
|
||||
def plan_missions(self) -> None:
|
||||
color = "Blue" if self.player else "Red"
|
||||
with MultiEventTracer() as tracer:
|
||||
mission_planner = CoalitionMissionPlanner(self.game, self.player)
|
||||
mission_planner = CoalitionMissionPlanner(
|
||||
self,
|
||||
self.game.theater,
|
||||
self.game.aircraft_inventory,
|
||||
self.game.settings,
|
||||
)
|
||||
with tracer.trace(f"{color} mission planning"):
|
||||
with tracer.trace(f"{color} mission identification"):
|
||||
commander = TheaterCommander(self.game, self.player)
|
||||
commander.plan_missions(mission_planner, tracer)
|
||||
with tracer.trace(f"{color} mission fulfillment"):
|
||||
mission_planner.fulfill_missions()
|
||||
with tracer.trace(f"{color} mission scheduling"):
|
||||
MissionScheduler(
|
||||
self, self.game.settings.desired_player_mission_duration
|
||||
).schedule_missions()
|
||||
|
||||
def plan_procurement(self) -> None:
|
||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much
|
||||
|
||||
@ -18,12 +18,12 @@ from game.theater.theatergroundobject import (
|
||||
IadsGroundObject,
|
||||
NavalGroundObject,
|
||||
)
|
||||
from game.transfers import CargoShip, Convoy
|
||||
from game.utils import meters, nautical_miles
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache, ClosestAirfields
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.transfers import CargoShip, Convoy
|
||||
|
||||
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
||||
|
||||
|
||||
@ -8,4 +8,4 @@ from game.htn import CompoundTask, Method
|
||||
class ProtectAirSpace(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for cp in state.vulnerable_control_points:
|
||||
yield [PlanBarcap(cp)]
|
||||
yield [PlanBarcap(cp, state.barcap_rounds)]
|
||||
|
||||
@ -17,6 +17,7 @@ if TYPE_CHECKING:
|
||||
@dataclass
|
||||
class PlanBarcap(TheaterCommanderTask):
|
||||
target: ControlPoint
|
||||
rounds: int
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if state.player and not state.ato_automation_enabled:
|
||||
@ -29,19 +30,7 @@ class PlanBarcap(TheaterCommanderTask):
|
||||
def execute(
|
||||
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
|
||||
) -> None:
|
||||
# Plan enough rounds of CAP that the target has coverage over the expected
|
||||
# mission duration.
|
||||
mission_duration = int(
|
||||
mission_planner.game.settings.desired_player_mission_duration.total_seconds()
|
||||
)
|
||||
barcap_duration = int(
|
||||
mission_planner.faction.doctrine.cap_duration.total_seconds()
|
||||
)
|
||||
for _ in range(
|
||||
0,
|
||||
mission_duration,
|
||||
barcap_duration,
|
||||
):
|
||||
for _ in range(self.rounds):
|
||||
mission_planner.plan_mission(
|
||||
ProposedMission(
|
||||
self.target,
|
||||
|
||||
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Union, Optional
|
||||
|
||||
@ -18,11 +19,11 @@ from game.theater.theatergroundobject import (
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import Convoy, CargoShip
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.transfers import Convoy, CargoShip
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -30,6 +31,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
player: bool
|
||||
stance_automation_enabled: bool
|
||||
ato_automation_enabled: bool
|
||||
barcap_rounds: int
|
||||
vulnerable_control_points: list[ControlPoint]
|
||||
active_front_lines: list[FrontLine]
|
||||
front_line_stances: dict[FrontLine, Optional[CombatStance]]
|
||||
@ -86,6 +88,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
player=self.player,
|
||||
stance_automation_enabled=self.stance_automation_enabled,
|
||||
ato_automation_enabled=self.ato_automation_enabled,
|
||||
barcap_rounds=self.barcap_rounds,
|
||||
vulnerable_control_points=list(self.vulnerable_control_points),
|
||||
active_front_lines=list(self.active_front_lines),
|
||||
front_line_stances=dict(self.front_line_stances),
|
||||
@ -120,10 +123,20 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
auto_stance = game.settings.automate_front_line_stance
|
||||
auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
|
||||
ordered_capturable_points = finder.prioritized_unisolated_points()
|
||||
|
||||
# Plan enough rounds of CAP that the target has coverage over the expected
|
||||
# mission duration.
|
||||
mission_duration = game.settings.desired_player_mission_duration.total_seconds()
|
||||
barcap_duration = game.coalition_for(
|
||||
player
|
||||
).doctrine.cap_duration.total_seconds()
|
||||
barcap_rounds = math.ceil(mission_duration / barcap_duration)
|
||||
|
||||
return TheaterState(
|
||||
player=player,
|
||||
stance_automation_enabled=auto_stance,
|
||||
ato_automation_enabled=auto_ato,
|
||||
barcap_rounds=barcap_rounds,
|
||||
vulnerable_control_points=list(finder.vulnerable_control_points()),
|
||||
active_front_lines=list(finder.front_lines()),
|
||||
front_line_stances={f: None for f in finder.front_lines()},
|
||||
|
||||
@ -4,7 +4,7 @@ import itertools
|
||||
import logging
|
||||
from abc import ABC
|
||||
from collections import Sequence
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union, Generic, TypeVar, Any
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union, Generic, TypeVar
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.triggers import TriggerZone
|
||||
@ -257,13 +257,17 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
def kill(self) -> None:
|
||||
self._dead = True
|
||||
|
||||
def iter_building_group(self) -> Iterator[TheaterGroundObject[Any]]:
|
||||
def iter_building_group(self) -> Iterator[BuildingGroundObject]:
|
||||
for tgo in self.control_point.ground_objects:
|
||||
if tgo.obj_name == self.obj_name and not tgo.is_dead:
|
||||
if (
|
||||
tgo.obj_name == self.obj_name
|
||||
and not tgo.is_dead
|
||||
and isinstance(tgo, BuildingGroundObject)
|
||||
):
|
||||
yield tgo
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
def strike_targets(self) -> List[BuildingGroundObject]:
|
||||
return list(self.iter_building_group())
|
||||
|
||||
@property
|
||||
|
||||
@ -340,7 +340,9 @@ class AirliftPlanner:
|
||||
transfer.transport = transport
|
||||
|
||||
self.package.add_flight(flight)
|
||||
planner = FlightPlanBuilder(self.game, self.package, self.for_player)
|
||||
planner = FlightPlanBuilder(
|
||||
self.package, self.game.coalition_for(self.for_player), self.game.theater
|
||||
)
|
||||
planner.populate_flight_plan(flight)
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
return flight_size
|
||||
|
||||
@ -14,26 +14,26 @@ from typing import (
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from game.commander import TheaterCommander
|
||||
from game.commander.missionproposals import ProposedFlight, ProposedMission, EscortType
|
||||
from game.commander.objectivefinder import ObjectiveFinder
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.factions.faction import Faction
|
||||
from game.infos.information import Information
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.profiling import logged_duration, MultiEventTracer
|
||||
from game.profiling import MultiEventTracer
|
||||
from game.settings import Settings
|
||||
from game.squadrons import AirWing, Squadron
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
ConflictTheater,
|
||||
)
|
||||
from game.threatzones import ThreatZones
|
||||
from game.utils import nautical_miles
|
||||
from gen.ato import Package
|
||||
from gen.ato import Package, AirTaskingOrder
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import (
|
||||
ClosestAirfields,
|
||||
ObjectiveDistanceCache,
|
||||
)
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
@ -44,7 +44,7 @@ from gen.flights.traveltime import TotEstimator
|
||||
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.coalition import Coalition
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
|
||||
|
||||
@ -201,6 +201,68 @@ class PackageBuilder:
|
||||
self.package.remove_flight(flight)
|
||||
|
||||
|
||||
class MissionScheduler:
|
||||
def __init__(self, coalition: Coalition, desired_mission_length: timedelta) -> None:
|
||||
self.coalition = coalition
|
||||
self.desired_mission_length = desired_mission_length
|
||||
|
||||
def schedule_missions(self) -> None:
|
||||
"""Identifies and plans mission for the turn."""
|
||||
|
||||
def start_time_generator(
|
||||
count: int, earliest: int, latest: int, margin: int
|
||||
) -> Iterator[timedelta]:
|
||||
interval = (latest - earliest) // count
|
||||
for time in range(earliest, latest, interval):
|
||||
error = random.randint(-margin, margin)
|
||||
yield timedelta(seconds=max(0, time + error))
|
||||
|
||||
dca_types = {
|
||||
FlightType.BARCAP,
|
||||
FlightType.TARCAP,
|
||||
}
|
||||
|
||||
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
|
||||
non_dca_packages = [
|
||||
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
|
||||
]
|
||||
|
||||
start_time = start_time_generator(
|
||||
count=len(non_dca_packages),
|
||||
earliest=5 * 60,
|
||||
latest=int(self.desired_mission_length.total_seconds()),
|
||||
margin=5 * 60,
|
||||
)
|
||||
for package in self.coalition.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
if package.primary_task in dca_types:
|
||||
previous_end_time = previous_cap_end_time[package.target]
|
||||
if tot > previous_end_time:
|
||||
# Can't get there exactly on time, so get there ASAP. This
|
||||
# will typically only happen for the first CAP at each
|
||||
# target.
|
||||
package.time_over_target = tot
|
||||
else:
|
||||
package.time_over_target = previous_end_time
|
||||
|
||||
departure_time = package.mission_departure_time
|
||||
# Should be impossible for CAPs
|
||||
if departure_time is None:
|
||||
logging.error(f"Could not determine mission end time for {package}")
|
||||
continue
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
elif package.auto_asap:
|
||||
package.set_tot_asap()
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
# mission start. This makes it more worthwhile to attack enemy
|
||||
# airfields to hit grounded aircraft, since they're more likely
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) + tot
|
||||
|
||||
|
||||
class CoalitionMissionPlanner:
|
||||
"""Coalition flight planning AI.
|
||||
|
||||
@ -224,18 +286,46 @@ class CoalitionMissionPlanner:
|
||||
TODO: Stance and doctrine-specific planning behavior.
|
||||
"""
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
self.objective_finder = ObjectiveFinder(self.game, self.is_player)
|
||||
self.ato = self.game.coalition_for(is_player).ato
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
self.procurement_requests = self.game.procurement_requests_for(self.is_player)
|
||||
self.faction: Faction = self.game.faction_for(self.is_player)
|
||||
def __init__(
|
||||
self,
|
||||
coalition: Coalition,
|
||||
theater: ConflictTheater,
|
||||
aircraft_inventory: GlobalAircraftInventory,
|
||||
settings: Settings,
|
||||
) -> None:
|
||||
self.coalition = coalition
|
||||
self.theater = theater
|
||||
self.aircraft_inventory = aircraft_inventory
|
||||
self.player_missions_asap = settings.auto_ato_player_missions_asap
|
||||
self.default_start_type = settings.default_start_type
|
||||
|
||||
@property
|
||||
def is_player(self) -> bool:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def ato(self) -> AirTaskingOrder:
|
||||
return self.coalition.ato
|
||||
|
||||
@property
|
||||
def air_wing(self) -> AirWing:
|
||||
return self.coalition.air_wing
|
||||
|
||||
@property
|
||||
def doctrine(self) -> Doctrine:
|
||||
return self.faction.doctrine
|
||||
return self.coalition.doctrine
|
||||
|
||||
@property
|
||||
def threat_zones(self) -> ThreatZones:
|
||||
return self.coalition.opponent.threat_zone
|
||||
|
||||
def add_procurement_request(
|
||||
self, request: AircraftProcurementRequest, priority: bool
|
||||
) -> None:
|
||||
if priority:
|
||||
self.coalition.procurement_requests.insert(0, request)
|
||||
else:
|
||||
self.coalition.procurement_requests.append(request)
|
||||
|
||||
def air_wing_can_plan(self, mission_type: FlightType) -> bool:
|
||||
"""Returns True if it is possible for the air wing to plan this mission type.
|
||||
@ -245,21 +335,7 @@ class CoalitionMissionPlanner:
|
||||
also possible for the player to exclude mission types from their squadron
|
||||
designs.
|
||||
"""
|
||||
return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type)
|
||||
|
||||
def fulfill_missions(self) -> None:
|
||||
"""Identifies and plans mission for the turn."""
|
||||
player = "Blue" if self.is_player else "Red"
|
||||
with logged_duration(f"{player} mission scheduling"):
|
||||
self.stagger_missions()
|
||||
|
||||
for cp in self.objective_finder.friendly_control_points():
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
self.message("Unused aircraft", f"{available} {aircraft} from {cp}")
|
||||
|
||||
coalition_text = "player" if self.is_player else "opfor"
|
||||
logging.debug(f"Planned {len(self.ato.packages)} {coalition_text} missions")
|
||||
return self.air_wing.can_auto_plan(mission_type)
|
||||
|
||||
def plan_flight(
|
||||
self,
|
||||
@ -277,12 +353,9 @@ class CoalitionMissionPlanner:
|
||||
task_capability=flight.task,
|
||||
number=flight.num_aircraft,
|
||||
)
|
||||
if for_reserves:
|
||||
# Reserves are planned for critical missions, so prioritize
|
||||
# those orders over aircraft needed for non-critical missions.
|
||||
self.procurement_requests.insert(0, purchase_order)
|
||||
else:
|
||||
self.procurement_requests.append(purchase_order)
|
||||
# Reserves are planned for critical missions, so prioritize those orders
|
||||
# over aircraft needed for non-critical missions.
|
||||
self.add_procurement_request(purchase_order, priority=for_reserves)
|
||||
|
||||
def scrub_mission_missing_aircraft(
|
||||
self,
|
||||
@ -300,10 +373,9 @@ class CoalitionMissionPlanner:
|
||||
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
|
||||
builder.release_planned_aircraft()
|
||||
desc = "reserve aircraft" if reserves else "aircraft"
|
||||
self.message(
|
||||
"Insufficient aircraft",
|
||||
logging.debug(
|
||||
f"Not enough {desc} in range for {mission.location.name} "
|
||||
f"capable of: {missing_types_str}",
|
||||
f"capable of: {missing_types_str}"
|
||||
)
|
||||
|
||||
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
||||
@ -325,12 +397,12 @@ class CoalitionMissionPlanner:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
builder = PackageBuilder(
|
||||
mission.location,
|
||||
self.objective_finder.closest_airfields_to(mission.location),
|
||||
self.game.aircraft_inventory,
|
||||
self.game.air_wing_for(self.is_player),
|
||||
ObjectiveDistanceCache.get_closest_airfields(mission.location),
|
||||
self.aircraft_inventory,
|
||||
self.air_wing,
|
||||
self.is_player,
|
||||
self.game.country_for(self.is_player),
|
||||
self.game.settings.default_start_type,
|
||||
self.coalition.country_name,
|
||||
self.default_start_type,
|
||||
mission.asap,
|
||||
)
|
||||
|
||||
@ -374,7 +446,7 @@ class CoalitionMissionPlanner:
|
||||
# the other flights in the package. Escorts will not be able to
|
||||
# contribute to this.
|
||||
flight_plan_builder = FlightPlanBuilder(
|
||||
self.game, builder.package, self.is_player
|
||||
builder.package, self.coalition, self.theater
|
||||
)
|
||||
for flight in builder.package.flights:
|
||||
with tracer.trace("Flight plan population"):
|
||||
@ -410,75 +482,8 @@ class CoalitionMissionPlanner:
|
||||
with tracer.trace("Flight plan population"):
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
|
||||
if package.has_players and self.game.settings.auto_ato_player_missions_asap:
|
||||
if package.has_players and self.player_missions_asap:
|
||||
package.auto_asap = True
|
||||
package.set_tot_asap()
|
||||
|
||||
self.ato.add_package(package)
|
||||
|
||||
def stagger_missions(self) -> None:
|
||||
def start_time_generator(
|
||||
count: int, earliest: int, latest: int, margin: int
|
||||
) -> Iterator[timedelta]:
|
||||
interval = (latest - earliest) // count
|
||||
for time in range(earliest, latest, interval):
|
||||
error = random.randint(-margin, margin)
|
||||
yield timedelta(seconds=max(0, time + error))
|
||||
|
||||
dca_types = {
|
||||
FlightType.BARCAP,
|
||||
FlightType.TARCAP,
|
||||
}
|
||||
|
||||
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
|
||||
non_dca_packages = [
|
||||
p for p in self.ato.packages if p.primary_task not in dca_types
|
||||
]
|
||||
|
||||
start_time = start_time_generator(
|
||||
count=len(non_dca_packages),
|
||||
earliest=5 * 60,
|
||||
latest=int(
|
||||
self.game.settings.desired_player_mission_duration.total_seconds()
|
||||
),
|
||||
margin=5 * 60,
|
||||
)
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
if package.primary_task in dca_types:
|
||||
previous_end_time = previous_cap_end_time[package.target]
|
||||
if tot > previous_end_time:
|
||||
# Can't get there exactly on time, so get there ASAP. This
|
||||
# will typically only happen for the first CAP at each
|
||||
# target.
|
||||
package.time_over_target = tot
|
||||
else:
|
||||
package.time_over_target = previous_end_time
|
||||
|
||||
departure_time = package.mission_departure_time
|
||||
# Should be impossible for CAPs
|
||||
if departure_time is None:
|
||||
logging.error(f"Could not determine mission end time for {package}")
|
||||
continue
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
elif package.auto_asap:
|
||||
package.set_tot_asap()
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
# mission start. This makes it more worthwhile to attack enemy
|
||||
# airfields to hit grounded aircraft, since they're more likely
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) + tot
|
||||
|
||||
def message(self, title: str, text: str) -> None:
|
||||
"""Emits a planning message to the player.
|
||||
|
||||
If the mission planner belongs to the players coalition, this emits a
|
||||
message to the info panel.
|
||||
"""
|
||||
if self.is_player:
|
||||
self.game.informations.append(Information(title, text, self.game.turn))
|
||||
else:
|
||||
logging.info(f"{title}: {text}")
|
||||
|
||||
@ -28,8 +28,14 @@ from game.theater import (
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
NavalControlPoint,
|
||||
ConflictTheater,
|
||||
)
|
||||
from game.theater.theatergroundobject import EwrGroundObject, NavalGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
EwrGroundObject,
|
||||
NavalGroundObject,
|
||||
BuildingGroundObject,
|
||||
)
|
||||
from game.threatzones import ThreatZones
|
||||
from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
@ -38,8 +44,8 @@ from .waypointbuilder import StrikeTarget, WaypointBuilder
|
||||
from ..conflictgen import Conflict, FRONTLINE_LENGTH
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
from game.coalition import Coalition
|
||||
from game.transfers import Convoy
|
||||
|
||||
INGRESS_TYPES = {
|
||||
@ -864,7 +870,9 @@ class CustomFlightPlan(FlightPlan):
|
||||
class FlightPlanBuilder:
|
||||
"""Generates flight plans for flights."""
|
||||
|
||||
def __init__(self, game: Game, package: Package, is_player: bool) -> None:
|
||||
def __init__(
|
||||
self, package: Package, coalition: Coalition, theater: ConflictTheater
|
||||
) -> None:
|
||||
# TODO: Plan similar altitudes for the in-country leg of the mission.
|
||||
# Waypoint altitudes for a given flight *shouldn't* differ too much
|
||||
# between the join and split points, so we don't need speeds for each
|
||||
@ -872,11 +880,21 @@ class FlightPlanBuilder:
|
||||
# hold too well right now since nothing is stopping each waypoint from
|
||||
# jumping 20k feet each time, but that's a huge waste of energy we
|
||||
# should be avoiding anyway.
|
||||
self.game = game
|
||||
self.package = package
|
||||
self.is_player = is_player
|
||||
self.doctrine: Doctrine = self.game.faction_for(self.is_player).doctrine
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
self.coalition = coalition
|
||||
self.theater = theater
|
||||
|
||||
@property
|
||||
def is_player(self) -> bool:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def doctrine(self) -> Doctrine:
|
||||
return self.coalition.doctrine
|
||||
|
||||
@property
|
||||
def threat_zones(self) -> ThreatZones:
|
||||
return self.coalition.opponent.threat_zone
|
||||
|
||||
def populate_flight_plan(
|
||||
self,
|
||||
@ -1022,7 +1040,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
def preferred_join_point(self) -> Optional[Point]:
|
||||
path = self.game.navmesh_for(self.is_player).shortest_path(
|
||||
path = self.coalition.nav_mesh.shortest_path(
|
||||
self.package_airfield().position, self.package.target.position
|
||||
)
|
||||
for point in reversed(path):
|
||||
@ -1043,26 +1061,16 @@ class FlightPlanBuilder:
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
targets: List[StrikeTarget] = []
|
||||
if len(location.groups) > 0 and location.dcs_identifier == "AA":
|
||||
if isinstance(location, BuildingGroundObject):
|
||||
# A building "group" is implemented as multiple TGOs with the same name.
|
||||
for building in location.strike_targets:
|
||||
targets.append(StrikeTarget(building.category, building))
|
||||
else:
|
||||
# TODO: Replace with DEAD?
|
||||
# Strike missions on SEAD targets target units.
|
||||
for g in location.groups:
|
||||
for j, u in enumerate(g.units):
|
||||
targets.append(StrikeTarget(f"{u.type} #{j}", u))
|
||||
else:
|
||||
# TODO: Does this actually happen?
|
||||
# ConflictTheater is built with the belief that multiple ground
|
||||
# objects have the same name. If that's the case,
|
||||
# TheaterGroundObject needs some refactoring because it behaves very
|
||||
# differently for SAM sites than it does for strike targets.
|
||||
buildings = self.game.theater.find_ground_objects_by_obj_name(
|
||||
location.obj_name
|
||||
)
|
||||
for building in buildings:
|
||||
if building.is_dead:
|
||||
continue
|
||||
|
||||
targets.append(StrikeTarget(building.category, building))
|
||||
|
||||
return self.strike_flightplan(
|
||||
flight, location, FlightWaypointType.INGRESS_STRIKE, targets
|
||||
@ -1083,7 +1091,7 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
patrol_alt = feet(25000)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
orbit = builder.orbit(orbit_location, patrol_alt)
|
||||
|
||||
return AwacsFlightPlan(
|
||||
@ -1175,7 +1183,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
|
||||
|
||||
return BarCapFlightPlan(
|
||||
@ -1211,7 +1219,7 @@ class FlightPlanBuilder:
|
||||
heading, -self.doctrine.sweep_distance.meters
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
@ -1251,7 +1259,7 @@ class FlightPlanBuilder:
|
||||
altitude = feet(1500)
|
||||
altitude_is_agl = True
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
|
||||
pickup = None
|
||||
nav_to_pickup = []
|
||||
@ -1373,9 +1381,7 @@ class FlightPlanBuilder:
|
||||
self, origin: Point, front_line: FrontLine
|
||||
) -> Tuple[Point, Point]:
|
||||
# Find targets waypoints
|
||||
ingress, heading, distance = Conflict.frontline_vector(
|
||||
front_line, self.game.theater
|
||||
)
|
||||
ingress, heading, distance = Conflict.frontline_vector(front_line, self.theater)
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
orbit_center = center.point_from_heading(
|
||||
heading - 90,
|
||||
@ -1414,7 +1420,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
# Create points
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
|
||||
if isinstance(location, FrontLine):
|
||||
orbit0p, orbit1p = self.racetrack_for_frontline(
|
||||
@ -1545,7 +1551,7 @@ class FlightPlanBuilder:
|
||||
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
ingress, target, egress = builder.escort(
|
||||
self.package.waypoints.ingress,
|
||||
self.package.target,
|
||||
@ -1588,9 +1594,7 @@ class FlightPlanBuilder:
|
||||
if not isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
ingress, heading, distance = Conflict.frontline_vector(
|
||||
location, self.game.theater
|
||||
)
|
||||
ingress, heading, distance = Conflict.frontline_vector(location, self.theater)
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
egress = ingress.point_from_heading(heading, distance)
|
||||
|
||||
@ -1599,7 +1603,7 @@ class FlightPlanBuilder:
|
||||
if egress_distance < ingress_distance:
|
||||
ingress, egress = egress, ingress
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
|
||||
return CasFlightPlan(
|
||||
package=self.package,
|
||||
@ -1655,7 +1659,7 @@ class FlightPlanBuilder:
|
||||
orbit_heading - 90, racetrack_half_distance
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
|
||||
tanker_type = flight.unit_type
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
@ -1776,7 +1780,7 @@ class FlightPlanBuilder:
|
||||
flight: The flight to generate the landing waypoint for.
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
builder = WaypointBuilder(flight, self.coalition)
|
||||
return builder.land(arrival)
|
||||
|
||||
def strike_flightplan(
|
||||
@ -1788,7 +1792,7 @@ class FlightPlanBuilder:
|
||||
lead_time: timedelta = timedelta(),
|
||||
) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
|
||||
builder = WaypointBuilder(flight, self.coalition, targets)
|
||||
|
||||
target_waypoints: List[FlightWaypoint] = []
|
||||
if targets is not None:
|
||||
|
||||
@ -15,10 +15,10 @@ from typing import (
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group, VehicleGroup, ShipGroup
|
||||
from dcs.unitgroup import VehicleGroup, ShipGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.coalition import Coalition
|
||||
from game.transfers import MultiGroupTransport
|
||||
|
||||
from game.theater import (
|
||||
@ -43,17 +43,15 @@ class WaypointBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
flight: Flight,
|
||||
game: Game,
|
||||
player: bool,
|
||||
coalition: Coalition,
|
||||
targets: Optional[List[StrikeTarget]] = None,
|
||||
) -> None:
|
||||
self.flight = flight
|
||||
self.conditions = game.conditions
|
||||
self.doctrine = game.faction_for(player).doctrine
|
||||
self.threat_zones = game.threat_zone_for(not player)
|
||||
self.navmesh = game.navmesh_for(player)
|
||||
self.doctrine = coalition.doctrine
|
||||
self.threat_zones = coalition.opponent.threat_zone
|
||||
self.navmesh = coalition.nav_mesh
|
||||
self.targets = targets
|
||||
self._bullseye = game.bullseye_for(player)
|
||||
self._bullseye = coalition.bullseye
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
|
||||
@ -180,7 +180,7 @@ class QPackageDialog(QDialog):
|
||||
self.game.aircraft_inventory.claim_for_flight(flight)
|
||||
self.package_model.add_flight(flight)
|
||||
planner = FlightPlanBuilder(
|
||||
self.game, self.package_model.package, is_player=True
|
||||
self.package_model.package, self.game.blue, self.game.theater
|
||||
)
|
||||
try:
|
||||
planner.populate_flight_plan(flight)
|
||||
|
||||
@ -100,6 +100,6 @@ class FlightAirfieldDisplay(QGroupBox):
|
||||
|
||||
def update_flight_plan(self) -> None:
|
||||
planner = FlightPlanBuilder(
|
||||
self.game, self.package_model.package, is_player=True
|
||||
self.package_model.package, self.game.blue, self.game.theater
|
||||
)
|
||||
planner.populate_flight_plan(self.flight)
|
||||
|
||||
@ -37,7 +37,7 @@ class QFlightWaypointTab(QFrame):
|
||||
self.game = game
|
||||
self.package = package
|
||||
self.flight = flight
|
||||
self.planner = FlightPlanBuilder(self.game, package, is_player=True)
|
||||
self.planner = FlightPlanBuilder(package, game.blue, game.theater)
|
||||
|
||||
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
|
||||
self.rtb_waypoint: Optional[QPushButton] = None
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user