Reduce mission planning dependence on Game.

This commit is contained in:
Dan Albert 2021-07-13 15:20:42 -07:00
parent 17c19d453b
commit 24f6aff8c8
13 changed files with 212 additions and 190 deletions

View File

@ -11,7 +11,7 @@ from game.navmesh import NavMesh
from game.profiling import logged_duration, MultiEventTracer from game.profiling import logged_duration, MultiEventTracer
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.transfers import PendingTransfers 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: if TYPE_CHECKING:
from game import Game from game import Game
@ -181,13 +181,20 @@ class Coalition:
def plan_missions(self) -> None: def plan_missions(self) -> None:
color = "Blue" if self.player else "Red" color = "Blue" if self.player else "Red"
with MultiEventTracer() as tracer: 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 planning"):
with tracer.trace(f"{color} mission identification"): with tracer.trace(f"{color} mission identification"):
commander = TheaterCommander(self.game, self.player) commander = TheaterCommander(self.game, self.player)
commander.plan_missions(mission_planner, tracer) commander.plan_missions(mission_planner, tracer)
with tracer.trace(f"{color} mission fulfillment"): with tracer.trace(f"{color} mission scheduling"):
mission_planner.fulfill_missions() MissionScheduler(
self, self.game.settings.desired_player_mission_duration
).schedule_missions()
def plan_procurement(self) -> None: def plan_procurement(self) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much

View File

@ -18,12 +18,12 @@ from game.theater.theatergroundobject import (
IadsGroundObject, IadsGroundObject,
NavalGroundObject, NavalGroundObject,
) )
from game.transfers import CargoShip, Convoy
from game.utils import meters, nautical_miles from game.utils import meters, nautical_miles
from gen.flights.closestairfields import ObjectiveDistanceCache, ClosestAirfields from gen.flights.closestairfields import ObjectiveDistanceCache, ClosestAirfields
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.transfers import CargoShip, Convoy
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget) MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)

View File

@ -8,4 +8,4 @@ from game.htn import CompoundTask, Method
class ProtectAirSpace(CompoundTask[TheaterState]): class ProtectAirSpace(CompoundTask[TheaterState]):
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]: def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
for cp in state.vulnerable_control_points: for cp in state.vulnerable_control_points:
yield [PlanBarcap(cp)] yield [PlanBarcap(cp, state.barcap_rounds)]

View File

@ -17,6 +17,7 @@ if TYPE_CHECKING:
@dataclass @dataclass
class PlanBarcap(TheaterCommanderTask): class PlanBarcap(TheaterCommanderTask):
target: ControlPoint target: ControlPoint
rounds: int
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if state.player and not state.ato_automation_enabled: if state.player and not state.ato_automation_enabled:
@ -29,19 +30,7 @@ class PlanBarcap(TheaterCommanderTask):
def execute( def execute(
self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer self, mission_planner: CoalitionMissionPlanner, tracer: MultiEventTracer
) -> None: ) -> None:
# Plan enough rounds of CAP that the target has coverage over the expected for _ in range(self.rounds):
# 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,
):
mission_planner.plan_mission( mission_planner.plan_mission(
ProposedMission( ProposedMission(
self.target, self.target,

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import dataclasses import dataclasses
import itertools import itertools
import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Union, Optional from typing import TYPE_CHECKING, Any, Union, Optional
@ -18,11 +19,11 @@ from game.theater.theatergroundobject import (
VehicleGroupGroundObject, VehicleGroupGroundObject,
) )
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.transfers import Convoy, CargoShip
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.transfers import Convoy, CargoShip
@dataclass @dataclass
@ -30,6 +31,7 @@ class TheaterState(WorldState["TheaterState"]):
player: bool player: bool
stance_automation_enabled: bool stance_automation_enabled: bool
ato_automation_enabled: bool ato_automation_enabled: bool
barcap_rounds: int
vulnerable_control_points: list[ControlPoint] vulnerable_control_points: list[ControlPoint]
active_front_lines: list[FrontLine] active_front_lines: list[FrontLine]
front_line_stances: dict[FrontLine, Optional[CombatStance]] front_line_stances: dict[FrontLine, Optional[CombatStance]]
@ -86,6 +88,7 @@ class TheaterState(WorldState["TheaterState"]):
player=self.player, player=self.player,
stance_automation_enabled=self.stance_automation_enabled, stance_automation_enabled=self.stance_automation_enabled,
ato_automation_enabled=self.ato_automation_enabled, ato_automation_enabled=self.ato_automation_enabled,
barcap_rounds=self.barcap_rounds,
vulnerable_control_points=list(self.vulnerable_control_points), vulnerable_control_points=list(self.vulnerable_control_points),
active_front_lines=list(self.active_front_lines), active_front_lines=list(self.active_front_lines),
front_line_stances=dict(self.front_line_stances), 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_stance = game.settings.automate_front_line_stance
auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled auto_ato = game.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
ordered_capturable_points = finder.prioritized_unisolated_points() 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( return TheaterState(
player=player, player=player,
stance_automation_enabled=auto_stance, stance_automation_enabled=auto_stance,
ato_automation_enabled=auto_ato, ato_automation_enabled=auto_ato,
barcap_rounds=barcap_rounds,
vulnerable_control_points=list(finder.vulnerable_control_points()), vulnerable_control_points=list(finder.vulnerable_control_points()),
active_front_lines=list(finder.front_lines()), active_front_lines=list(finder.front_lines()),
front_line_stances={f: None for f in finder.front_lines()}, front_line_stances={f: None for f in finder.front_lines()},

View File

@ -4,7 +4,7 @@ import itertools
import logging import logging
from abc import ABC from abc import ABC
from collections import Sequence 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.mapping import Point
from dcs.triggers import TriggerZone from dcs.triggers import TriggerZone
@ -257,13 +257,17 @@ class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
def kill(self) -> None: def kill(self) -> None:
self._dead = True 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: 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 yield tgo
@property @property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]: def strike_targets(self) -> List[BuildingGroundObject]:
return list(self.iter_building_group()) return list(self.iter_building_group())
@property @property

View File

@ -340,7 +340,9 @@ class AirliftPlanner:
transfer.transport = transport transfer.transport = transport
self.package.add_flight(flight) 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) planner.populate_flight_plan(flight)
self.game.aircraft_inventory.claim_for_flight(flight) self.game.aircraft_inventory.claim_for_flight(flight)
return flight_size return flight_size

View File

@ -14,26 +14,26 @@ from typing import (
Tuple, Tuple,
) )
from game.commander import TheaterCommander
from game.commander.missionproposals import ProposedFlight, ProposedMission, EscortType from game.commander.missionproposals import ProposedFlight, ProposedMission, EscortType
from game.commander.objectivefinder import ObjectiveFinder
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.dcs.aircrafttype import AircraftType 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.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.squadrons import AirWing, Squadron
from game.theater import ( from game.theater import (
ControlPoint, ControlPoint,
MissionTarget, MissionTarget,
OffMapSpawn, OffMapSpawn,
ConflictTheater,
) )
from game.threatzones import ThreatZones
from game.utils import nautical_miles 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.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import ( from gen.flights.closestairfields import (
ClosestAirfields, ClosestAirfields,
ObjectiveDistanceCache,
) )
from gen.flights.flight import ( from gen.flights.flight import (
Flight, Flight,
@ -44,7 +44,7 @@ from gen.flights.traveltime import TotEstimator
# Avoid importing some types that cause circular imports unless type checking. # Avoid importing some types that cause circular imports unless type checking.
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game.coalition import Coalition
from game.inventory import GlobalAircraftInventory from game.inventory import GlobalAircraftInventory
@ -201,6 +201,68 @@ class PackageBuilder:
self.package.remove_flight(flight) 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: class CoalitionMissionPlanner:
"""Coalition flight planning AI. """Coalition flight planning AI.
@ -224,18 +286,46 @@ class CoalitionMissionPlanner:
TODO: Stance and doctrine-specific planning behavior. TODO: Stance and doctrine-specific planning behavior.
""" """
def __init__(self, game: Game, is_player: bool) -> None: def __init__(
self.game = game self,
self.is_player = is_player coalition: Coalition,
self.objective_finder = ObjectiveFinder(self.game, self.is_player) theater: ConflictTheater,
self.ato = self.game.coalition_for(is_player).ato aircraft_inventory: GlobalAircraftInventory,
self.threat_zones = self.game.threat_zone_for(not self.is_player) settings: Settings,
self.procurement_requests = self.game.procurement_requests_for(self.is_player) ) -> None:
self.faction: Faction = self.game.faction_for(self.is_player) 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 @property
def doctrine(self) -> Doctrine: 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: 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. """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 also possible for the player to exclude mission types from their squadron
designs. designs.
""" """
return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type) return self.air_wing.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")
def plan_flight( def plan_flight(
self, self,
@ -277,12 +353,9 @@ class CoalitionMissionPlanner:
task_capability=flight.task, task_capability=flight.task,
number=flight.num_aircraft, number=flight.num_aircraft,
) )
if for_reserves: # Reserves are planned for critical missions, so prioritize those orders
# Reserves are planned for critical missions, so prioritize # over aircraft needed for non-critical missions.
# those orders over aircraft needed for non-critical missions. self.add_procurement_request(purchase_order, priority=for_reserves)
self.procurement_requests.insert(0, purchase_order)
else:
self.procurement_requests.append(purchase_order)
def scrub_mission_missing_aircraft( def scrub_mission_missing_aircraft(
self, self,
@ -300,10 +373,9 @@ class CoalitionMissionPlanner:
missing_types_str = ", ".join(sorted([t.name for t in missing_types])) missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
builder.release_planned_aircraft() builder.release_planned_aircraft()
desc = "reserve aircraft" if reserves else "aircraft" desc = "reserve aircraft" if reserves else "aircraft"
self.message( logging.debug(
"Insufficient aircraft",
f"Not enough {desc} in range for {mission.location.name} " 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]: 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.""" """Allocates aircraft for a proposed mission and adds it to the ATO."""
builder = PackageBuilder( builder = PackageBuilder(
mission.location, mission.location,
self.objective_finder.closest_airfields_to(mission.location), ObjectiveDistanceCache.get_closest_airfields(mission.location),
self.game.aircraft_inventory, self.aircraft_inventory,
self.game.air_wing_for(self.is_player), self.air_wing,
self.is_player, self.is_player,
self.game.country_for(self.is_player), self.coalition.country_name,
self.game.settings.default_start_type, self.default_start_type,
mission.asap, mission.asap,
) )
@ -374,7 +446,7 @@ class CoalitionMissionPlanner:
# the other flights in the package. Escorts will not be able to # the other flights in the package. Escorts will not be able to
# contribute to this. # contribute to this.
flight_plan_builder = FlightPlanBuilder( flight_plan_builder = FlightPlanBuilder(
self.game, builder.package, self.is_player builder.package, self.coalition, self.theater
) )
for flight in builder.package.flights: for flight in builder.package.flights:
with tracer.trace("Flight plan population"): with tracer.trace("Flight plan population"):
@ -410,75 +482,8 @@ class CoalitionMissionPlanner:
with tracer.trace("Flight plan population"): with tracer.trace("Flight plan population"):
flight_plan_builder.populate_flight_plan(flight) 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.auto_asap = True
package.set_tot_asap() package.set_tot_asap()
self.ato.add_package(package) 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}")

View File

@ -28,8 +28,14 @@ from game.theater import (
SamGroundObject, SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
NavalControlPoint, 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 game.utils import Distance, Speed, feet, meters, nautical_miles, knots
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
@ -38,8 +44,8 @@ from .waypointbuilder import StrikeTarget, WaypointBuilder
from ..conflictgen import Conflict, FRONTLINE_LENGTH from ..conflictgen import Conflict, FRONTLINE_LENGTH
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game
from gen.ato import Package from gen.ato import Package
from game.coalition import Coalition
from game.transfers import Convoy from game.transfers import Convoy
INGRESS_TYPES = { INGRESS_TYPES = {
@ -864,7 +870,9 @@ class CustomFlightPlan(FlightPlan):
class FlightPlanBuilder: class FlightPlanBuilder:
"""Generates flight plans for flights.""" """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. # TODO: Plan similar altitudes for the in-country leg of the mission.
# Waypoint altitudes for a given flight *shouldn't* differ too much # 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 # 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 # 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 # jumping 20k feet each time, but that's a huge waste of energy we
# should be avoiding anyway. # should be avoiding anyway.
self.game = game
self.package = package self.package = package
self.is_player = is_player self.coalition = coalition
self.doctrine: Doctrine = self.game.faction_for(self.is_player).doctrine self.theater = theater
self.threat_zones = self.game.threat_zone_for(not self.is_player)
@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( def populate_flight_plan(
self, self,
@ -1022,7 +1040,7 @@ class FlightPlanBuilder:
) )
def preferred_join_point(self) -> Optional[Point]: 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 self.package_airfield().position, self.package.target.position
) )
for point in reversed(path): for point in reversed(path):
@ -1043,26 +1061,16 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
targets: List[StrikeTarget] = [] 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? # TODO: Replace with DEAD?
# Strike missions on SEAD targets target units. # Strike missions on SEAD targets target units.
for g in location.groups: for g in location.groups:
for j, u in enumerate(g.units): for j, u in enumerate(g.units):
targets.append(StrikeTarget(f"{u.type} #{j}", u)) 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( return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_STRIKE, targets flight, location, FlightWaypointType.INGRESS_STRIKE, targets
@ -1083,7 +1091,7 @@ class FlightPlanBuilder:
else: else:
patrol_alt = feet(25000) patrol_alt = feet(25000)
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.coalition)
orbit = builder.orbit(orbit_location, patrol_alt) orbit = builder.orbit(orbit_location, patrol_alt)
return AwacsFlightPlan( 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) start, end = builder.race_track(start_pos, end_pos, patrol_alt)
return BarCapFlightPlan( return BarCapFlightPlan(
@ -1211,7 +1219,7 @@ class FlightPlanBuilder:
heading, -self.doctrine.sweep_distance.meters 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) start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
hold = builder.hold(self._hold_point(flight)) hold = builder.hold(self._hold_point(flight))
@ -1251,7 +1259,7 @@ class FlightPlanBuilder:
altitude = feet(1500) altitude = feet(1500)
altitude_is_agl = True altitude_is_agl = True
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.coalition)
pickup = None pickup = None
nav_to_pickup = [] nav_to_pickup = []
@ -1373,9 +1381,7 @@ class FlightPlanBuilder:
self, origin: Point, front_line: FrontLine self, origin: Point, front_line: FrontLine
) -> Tuple[Point, Point]: ) -> Tuple[Point, Point]:
# Find targets waypoints # Find targets waypoints
ingress, heading, distance = Conflict.frontline_vector( ingress, heading, distance = Conflict.frontline_vector(front_line, self.theater)
front_line, self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
orbit_center = center.point_from_heading( orbit_center = center.point_from_heading(
heading - 90, heading - 90,
@ -1414,7 +1420,7 @@ class FlightPlanBuilder:
) )
# Create points # Create points
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.coalition)
if isinstance(location, FrontLine): if isinstance(location, FrontLine):
orbit0p, orbit1p = self.racetrack_for_frontline( orbit0p, orbit1p = self.racetrack_for_frontline(
@ -1545,7 +1551,7 @@ class FlightPlanBuilder:
def generate_escort(self, flight: Flight) -> StrikeFlightPlan: def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
assert self.package.waypoints is not None 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( ingress, target, egress = builder.escort(
self.package.waypoints.ingress, self.package.waypoints.ingress,
self.package.target, self.package.target,
@ -1588,9 +1594,7 @@ class FlightPlanBuilder:
if not isinstance(location, FrontLine): if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
ingress, heading, distance = Conflict.frontline_vector( ingress, heading, distance = Conflict.frontline_vector(location, self.theater)
location, self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance) egress = ingress.point_from_heading(heading, distance)
@ -1599,7 +1603,7 @@ class FlightPlanBuilder:
if egress_distance < ingress_distance: if egress_distance < ingress_distance:
ingress, egress = egress, ingress ingress, egress = egress, ingress
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.coalition)
return CasFlightPlan( return CasFlightPlan(
package=self.package, package=self.package,
@ -1655,7 +1659,7 @@ class FlightPlanBuilder:
orbit_heading - 90, racetrack_half_distance orbit_heading - 90, racetrack_half_distance
) )
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.coalition)
tanker_type = flight.unit_type tanker_type = flight.unit_type
if tanker_type.patrol_altitude is not None: if tanker_type.patrol_altitude is not None:
@ -1776,7 +1780,7 @@ class FlightPlanBuilder:
flight: The flight to generate the landing waypoint for. flight: The flight to generate the landing waypoint for.
arrival: Arrival airfield or carrier. arrival: Arrival airfield or carrier.
""" """
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.coalition)
return builder.land(arrival) return builder.land(arrival)
def strike_flightplan( def strike_flightplan(
@ -1788,7 +1792,7 @@ class FlightPlanBuilder:
lead_time: timedelta = timedelta(), lead_time: timedelta = timedelta(),
) -> StrikeFlightPlan: ) -> StrikeFlightPlan:
assert self.package.waypoints is not None 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] = [] target_waypoints: List[FlightWaypoint] = []
if targets is not None: if targets is not None:

View File

@ -15,10 +15,10 @@ from typing import (
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import Group, VehicleGroup, ShipGroup from dcs.unitgroup import VehicleGroup, ShipGroup
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game.coalition import Coalition
from game.transfers import MultiGroupTransport from game.transfers import MultiGroupTransport
from game.theater import ( from game.theater import (
@ -43,17 +43,15 @@ class WaypointBuilder:
def __init__( def __init__(
self, self,
flight: Flight, flight: Flight,
game: Game, coalition: Coalition,
player: bool,
targets: Optional[List[StrikeTarget]] = None, targets: Optional[List[StrikeTarget]] = None,
) -> None: ) -> None:
self.flight = flight self.flight = flight
self.conditions = game.conditions self.doctrine = coalition.doctrine
self.doctrine = game.faction_for(player).doctrine self.threat_zones = coalition.opponent.threat_zone
self.threat_zones = game.threat_zone_for(not player) self.navmesh = coalition.nav_mesh
self.navmesh = game.navmesh_for(player)
self.targets = targets self.targets = targets
self._bullseye = game.bullseye_for(player) self._bullseye = coalition.bullseye
@property @property
def is_helo(self) -> bool: def is_helo(self) -> bool:

View File

@ -180,7 +180,7 @@ class QPackageDialog(QDialog):
self.game.aircraft_inventory.claim_for_flight(flight) self.game.aircraft_inventory.claim_for_flight(flight)
self.package_model.add_flight(flight) self.package_model.add_flight(flight)
planner = FlightPlanBuilder( planner = FlightPlanBuilder(
self.game, self.package_model.package, is_player=True self.package_model.package, self.game.blue, self.game.theater
) )
try: try:
planner.populate_flight_plan(flight) planner.populate_flight_plan(flight)

View File

@ -100,6 +100,6 @@ class FlightAirfieldDisplay(QGroupBox):
def update_flight_plan(self) -> None: def update_flight_plan(self) -> None:
planner = FlightPlanBuilder( 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) planner.populate_flight_plan(self.flight)

View File

@ -37,7 +37,7 @@ class QFlightWaypointTab(QFrame):
self.game = game self.game = game
self.package = package self.package = package
self.flight = flight 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.flight_waypoint_list: Optional[QFlightWaypointList] = None
self.rtb_waypoint: Optional[QPushButton] = None self.rtb_waypoint: Optional[QPushButton] = None