mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'dcs-liberation:develop' into develop
This commit is contained in:
commit
7a459fd5b8
18
changelog.md
18
changelog.md
@ -4,6 +4,10 @@ Saves from 3.x are not compatible with 5.0.
|
|||||||
|
|
||||||
## Features/Improvements
|
## Features/Improvements
|
||||||
|
|
||||||
|
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
|
||||||
|
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
|
||||||
|
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
# 4.1.0
|
# 4.1.0
|
||||||
@ -13,17 +17,27 @@ Saves from 4.0.0 are compatible with 4.1.0.
|
|||||||
## Features/Improvements
|
## Features/Improvements
|
||||||
|
|
||||||
* **[Campaign]** Air defense sites now generate a fixed number of launchers per type.
|
* **[Campaign]** Air defense sites now generate a fixed number of launchers per type.
|
||||||
|
* **[Campaign]** Added support for Mariana Islands map.
|
||||||
* **[Mission Generation]** Improvements for better support of the Skynet Plugin and long range SAMs are now acting as EWR
|
* **[Mission Generation]** Improvements for better support of the Skynet Plugin and long range SAMs are now acting as EWR
|
||||||
* **[Plugins]** Increased time JTAC Autolase messages stay visible on the UI.
|
* **[Plugins]** Increased time JTAC Autolase messages stay visible on the UI.
|
||||||
* **[UI]** Added ability to take notes and have those notes appear as a kneeboard page.
|
* **[UI]** Added ability to take notes and have those notes appear as a kneeboard page.
|
||||||
* **[UI]** Hovering over the weather information now dispalys the cloud base (meters and feet).
|
* **[UI]** Hovering over the weather information now dispalys the cloud base (meters and feet).
|
||||||
* **[UI]** Google search link added to unit information when there is no information provided.
|
* **[UI]** Google search link added to unit information when there is no information provided.
|
||||||
* **[UI]** Control point name displayed with ground object group name on map.
|
* **[UI]** Control point name displayed with ground object group name on map.
|
||||||
|
* **[UI]** Buy or Replace will now show the correct price for generated ground objects like sams.
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
* **[UI]** Statistics window tick marks are now always integers.
|
|
||||||
* **[Mission Generation]** The lua data for other plugins is now generated correctly
|
* **[Campaign]** Fixed the Silkworm generator to include launchers and not all radars.
|
||||||
|
* **[Economy]** EWRs can now be bought and sold for the correct price and can no longer be used to generate money
|
||||||
* **[Flight Planning]** Fixed potential issue with angles > 360° or < 0° being generated when summing two angles.
|
* **[Flight Planning]** Fixed potential issue with angles > 360° or < 0° being generated when summing two angles.
|
||||||
|
* **[Mission Generation]** The lua data for other plugins is now generated correctly
|
||||||
|
* **[Mission Generation]** Fixed problem with opfor planning missions against sold ground objects like SAMs
|
||||||
|
* **[Mission Generation]** The legacy always-available tanker option no longer prevents mission creation.
|
||||||
|
* **[Mission Generation]** Fix occasional KeyError preventing mission generation when all units of the same type in a convoy were killed.
|
||||||
|
* **[UI]** Statistics window tick marks are now always integers.
|
||||||
|
* **[UI]** Statistics window now shows the correct info for the turn
|
||||||
|
* **[UI]** Toggling custom loadout for an aircraft with no preset loadouts no longer breaks the flight.
|
||||||
|
|
||||||
# 4.0.0
|
# 4.0.0
|
||||||
|
|
||||||
|
|||||||
220
game/coalition.py
Normal file
220
game/coalition.py
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Optional
|
||||||
|
|
||||||
|
from dcs import Point
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
|
from game.commander import TheaterCommander
|
||||||
|
from game.commander.missionscheduler import MissionScheduler
|
||||||
|
from game.income import Income
|
||||||
|
from game.inventory import GlobalAircraftInventory
|
||||||
|
from game.navmesh import NavMesh
|
||||||
|
from game.profiling import logged_duration, MultiEventTracer
|
||||||
|
from game.threatzones import ThreatZones
|
||||||
|
from game.transfers import PendingTransfers
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.factions.faction import Faction
|
||||||
|
from game.procurement import AircraftProcurementRequest, ProcurementAi
|
||||||
|
from game.squadrons import AirWing
|
||||||
|
from game.theater.bullseye import Bullseye
|
||||||
|
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||||
|
from gen import AirTaskingOrder
|
||||||
|
|
||||||
|
|
||||||
|
class Coalition:
|
||||||
|
def __init__(
|
||||||
|
self, game: Game, faction: Faction, budget: float, player: bool
|
||||||
|
) -> None:
|
||||||
|
self.game = game
|
||||||
|
self.player = player
|
||||||
|
self.faction = faction
|
||||||
|
self.budget = budget
|
||||||
|
self.ato = AirTaskingOrder()
|
||||||
|
self.transit_network = TransitNetwork()
|
||||||
|
self.procurement_requests: list[AircraftProcurementRequest] = []
|
||||||
|
self.bullseye = Bullseye(Point(0, 0))
|
||||||
|
self.faker = Faker(self.faction.locales)
|
||||||
|
self.air_wing = AirWing(game, self)
|
||||||
|
self.transfers = PendingTransfers(game, player)
|
||||||
|
|
||||||
|
# Late initialized because the two coalitions in the game are mutually
|
||||||
|
# dependent, so must be both constructed before this property can be set.
|
||||||
|
self._opponent: Optional[Coalition] = None
|
||||||
|
|
||||||
|
# Volatile properties that are not persisted to the save file since they can be
|
||||||
|
# recomputed on load. Keeping this data out of the save file makes save compat
|
||||||
|
# breaks less frequent. Each of these properties has a non-underscore-prefixed
|
||||||
|
# @property that should be used for non-Optional access.
|
||||||
|
#
|
||||||
|
# All of these are late-initialized (whether via on_load or called later), but
|
||||||
|
# will be non-None after the game has finished loading.
|
||||||
|
self._threat_zone: Optional[ThreatZones] = None
|
||||||
|
self._navmesh: Optional[NavMesh] = None
|
||||||
|
self.on_load()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def doctrine(self) -> Doctrine:
|
||||||
|
return self.faction.doctrine
|
||||||
|
|
||||||
|
@property
|
||||||
|
def coalition_id(self) -> int:
|
||||||
|
if self.player:
|
||||||
|
return 2
|
||||||
|
return 1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def country_name(self) -> str:
|
||||||
|
return self.faction.country
|
||||||
|
|
||||||
|
@property
|
||||||
|
def opponent(self) -> Coalition:
|
||||||
|
assert self._opponent is not None
|
||||||
|
return self._opponent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def threat_zone(self) -> ThreatZones:
|
||||||
|
assert self._threat_zone is not None
|
||||||
|
return self._threat_zone
|
||||||
|
|
||||||
|
@property
|
||||||
|
def nav_mesh(self) -> NavMesh:
|
||||||
|
assert self._navmesh is not None
|
||||||
|
return self._navmesh
|
||||||
|
|
||||||
|
@property
|
||||||
|
def aircraft_inventory(self) -> GlobalAircraftInventory:
|
||||||
|
return self.game.aircraft_inventory
|
||||||
|
|
||||||
|
def __getstate__(self) -> dict[str, Any]:
|
||||||
|
state = self.__dict__.copy()
|
||||||
|
# Avoid persisting any volatile types that can be deterministically
|
||||||
|
# recomputed on load for the sake of save compatibility.
|
||||||
|
del state["_threat_zone"]
|
||||||
|
del state["_navmesh"]
|
||||||
|
del state["faker"]
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
|
self.__dict__.update(state)
|
||||||
|
# Regenerate any state that was not persisted.
|
||||||
|
self.on_load()
|
||||||
|
|
||||||
|
def on_load(self) -> None:
|
||||||
|
self.faker = Faker(self.faction.locales)
|
||||||
|
|
||||||
|
def set_opponent(self, opponent: Coalition) -> None:
|
||||||
|
if self._opponent is not None:
|
||||||
|
raise RuntimeError("Double-initialization of Coalition.opponent")
|
||||||
|
self._opponent = opponent
|
||||||
|
|
||||||
|
def adjust_budget(self, amount: float) -> None:
|
||||||
|
self.budget += amount
|
||||||
|
|
||||||
|
def compute_threat_zones(self) -> None:
|
||||||
|
self._threat_zone = ThreatZones.for_faction(self.game, self.player)
|
||||||
|
|
||||||
|
def compute_nav_meshes(self) -> None:
|
||||||
|
self._navmesh = NavMesh.from_threat_zones(
|
||||||
|
self.opponent.threat_zone, self.game.theater
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_transit_network(self) -> None:
|
||||||
|
self.transit_network = TransitNetworkBuilder(
|
||||||
|
self.game.theater, self.player
|
||||||
|
).build()
|
||||||
|
|
||||||
|
def set_bullseye(self, bullseye: Bullseye) -> None:
|
||||||
|
self.bullseye = bullseye
|
||||||
|
|
||||||
|
def end_turn(self) -> None:
|
||||||
|
"""Processes coalition-specific turn finalization.
|
||||||
|
|
||||||
|
For more information on turn finalization in general, see the documentation for
|
||||||
|
`Game.finish_turn`.
|
||||||
|
"""
|
||||||
|
self.air_wing.replenish()
|
||||||
|
self.budget += Income(self.game, self.player).total
|
||||||
|
|
||||||
|
# Need to recompute before transfers and deliveries to account for captures.
|
||||||
|
# This happens in in initialize_turn as well, because cheating doesn't advance a
|
||||||
|
# turn but can capture bases so we need to recompute there as well.
|
||||||
|
self.update_transit_network()
|
||||||
|
|
||||||
|
# Must happen *before* unit deliveries are handled, or else new units will spawn
|
||||||
|
# one hop ahead. ControlPoint.process_turn handles unit deliveries. The
|
||||||
|
# coalition-specific turn-end happens before the theater-wide turn-end, so this
|
||||||
|
# is handled correctly.
|
||||||
|
self.transfers.perform_transfers()
|
||||||
|
|
||||||
|
def initialize_turn(self) -> None:
|
||||||
|
"""Processes coalition-specific turn initialization.
|
||||||
|
|
||||||
|
For more information on turn initialization in general, see the documentation
|
||||||
|
for `Game.initialize_turn`.
|
||||||
|
"""
|
||||||
|
# Needs to happen *before* planning transfers so we don't cancel them.
|
||||||
|
self.ato.clear()
|
||||||
|
self.air_wing.reset()
|
||||||
|
self.refund_outstanding_orders()
|
||||||
|
self.procurement_requests.clear()
|
||||||
|
|
||||||
|
with logged_duration("Transit network identification"):
|
||||||
|
self.update_transit_network()
|
||||||
|
with logged_duration("Procurement of airlift assets"):
|
||||||
|
self.transfers.order_airlift_assets()
|
||||||
|
with logged_duration("Transport planning"):
|
||||||
|
self.transfers.plan_transports()
|
||||||
|
|
||||||
|
self.plan_missions()
|
||||||
|
self.plan_procurement()
|
||||||
|
|
||||||
|
def refund_outstanding_orders(self) -> None:
|
||||||
|
# TODO: Split orders between air and ground units.
|
||||||
|
# This isn't quite right. If the player has ground purchases automated we should
|
||||||
|
# be refunding the ground units, and if they have air automated but not ground
|
||||||
|
# we should be refunding air units.
|
||||||
|
if self.player and not self.game.settings.automate_aircraft_reinforcements:
|
||||||
|
return
|
||||||
|
|
||||||
|
for cp in self.game.theater.control_points_for(self.player):
|
||||||
|
cp.pending_unit_deliveries.refund_all(self)
|
||||||
|
|
||||||
|
def plan_missions(self) -> None:
|
||||||
|
color = "Blue" if self.player else "Red"
|
||||||
|
with MultiEventTracer() as tracer:
|
||||||
|
with tracer.trace(f"{color} mission planning"):
|
||||||
|
with tracer.trace(f"{color} mission identification"):
|
||||||
|
TheaterCommander(self.game, self.player).plan_missions(tracer)
|
||||||
|
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
|
||||||
|
# more of the budget that turn. Otherwise budget (after repairs) is split evenly
|
||||||
|
# between air and ground. For the default starting budget of 2000 this gives 600
|
||||||
|
# to ground forces and 1400 to aircraft. After that the budget will be spent
|
||||||
|
# proportionally based on how much is already invested.
|
||||||
|
|
||||||
|
if self.player:
|
||||||
|
manage_runways = self.game.settings.automate_runway_repair
|
||||||
|
manage_front_line = self.game.settings.automate_front_line_reinforcements
|
||||||
|
manage_aircraft = self.game.settings.automate_aircraft_reinforcements
|
||||||
|
else:
|
||||||
|
manage_runways = True
|
||||||
|
manage_front_line = True
|
||||||
|
manage_aircraft = True
|
||||||
|
|
||||||
|
self.budget = ProcurementAi(
|
||||||
|
self.game,
|
||||||
|
self.player,
|
||||||
|
self.faction,
|
||||||
|
manage_runways,
|
||||||
|
manage_front_line,
|
||||||
|
manage_aircraft,
|
||||||
|
).spend_budget(self.budget)
|
||||||
1
game/commander/__init__.py
Normal file
1
game/commander/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .theatercommander import TheaterCommander
|
||||||
76
game/commander/aircraftallocator.py
Normal file
76
game/commander/aircraftallocator.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from game.commander.missionproposals import ProposedFlight
|
||||||
|
from game.inventory import GlobalAircraftInventory
|
||||||
|
from game.squadrons import AirWing, Squadron
|
||||||
|
from game.theater import ControlPoint
|
||||||
|
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||||
|
from gen.flights.closestairfields import ClosestAirfields
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
class AircraftAllocator:
|
||||||
|
"""Finds suitable aircraft for proposed missions."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
air_wing: AirWing,
|
||||||
|
closest_airfields: ClosestAirfields,
|
||||||
|
global_inventory: GlobalAircraftInventory,
|
||||||
|
is_player: bool,
|
||||||
|
) -> None:
|
||||||
|
self.air_wing = air_wing
|
||||||
|
self.closest_airfields = closest_airfields
|
||||||
|
self.global_inventory = global_inventory
|
||||||
|
self.is_player = is_player
|
||||||
|
|
||||||
|
def find_squadron_for_flight(
|
||||||
|
self, flight: ProposedFlight
|
||||||
|
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||||
|
"""Finds aircraft suitable for the given mission.
|
||||||
|
|
||||||
|
Searches for aircraft capable of performing the given mission within the
|
||||||
|
maximum allowed range. If insufficient aircraft are available for the
|
||||||
|
mission, None is returned.
|
||||||
|
|
||||||
|
Airfields are searched ordered nearest to farthest from the target and
|
||||||
|
searched twice. The first search looks for aircraft which prefer the
|
||||||
|
mission type, and the second search looks for any aircraft which are
|
||||||
|
capable of the mission type. For example, an F-14 from a nearby carrier
|
||||||
|
will be preferred for the CAP of an airfield that has only F-16s, but if
|
||||||
|
the carrier has only F/A-18s the F-16s will be used for CAP instead.
|
||||||
|
|
||||||
|
Note that aircraft *will* be removed from the global inventory on
|
||||||
|
success. This is to ensure that the same aircraft are not matched twice
|
||||||
|
on subsequent calls. If the found aircraft are not used, the caller is
|
||||||
|
responsible for returning them to the inventory.
|
||||||
|
"""
|
||||||
|
return self.find_aircraft_for_task(flight, flight.task)
|
||||||
|
|
||||||
|
def find_aircraft_for_task(
|
||||||
|
self, flight: ProposedFlight, task: FlightType
|
||||||
|
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||||
|
types = aircraft_for_task(task)
|
||||||
|
airfields_in_range = self.closest_airfields.operational_airfields_within(
|
||||||
|
flight.max_distance
|
||||||
|
)
|
||||||
|
|
||||||
|
for airfield in airfields_in_range:
|
||||||
|
if not airfield.is_friendly(self.is_player):
|
||||||
|
continue
|
||||||
|
inventory = self.global_inventory.for_control_point(airfield)
|
||||||
|
for aircraft in types:
|
||||||
|
if not airfield.can_operate(aircraft):
|
||||||
|
continue
|
||||||
|
if inventory.available(aircraft) < flight.num_aircraft:
|
||||||
|
continue
|
||||||
|
# Valid location with enough aircraft available. Find a squadron to fit
|
||||||
|
# the role.
|
||||||
|
squadrons = self.air_wing.auto_assignable_for_task_with_type(
|
||||||
|
aircraft, task
|
||||||
|
)
|
||||||
|
for squadron in squadrons:
|
||||||
|
if squadron.can_provide_pilots(flight.num_aircraft):
|
||||||
|
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||||
|
return airfield, squadron
|
||||||
|
return None
|
||||||
52
game/commander/garrisons.py
Normal file
52
game/commander/garrisons.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.theater import ControlPoint
|
||||||
|
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||||
|
from game.utils import meters
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Garrisons:
|
||||||
|
blocking_capture: list[VehicleGroupGroundObject]
|
||||||
|
defending_front_line: list[VehicleGroupGroundObject]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def in_priority_order(self) -> Iterator[VehicleGroupGroundObject]:
|
||||||
|
yield from self.blocking_capture
|
||||||
|
yield from self.defending_front_line
|
||||||
|
|
||||||
|
def eliminate(self, garrison: VehicleGroupGroundObject) -> None:
|
||||||
|
if garrison in self.blocking_capture:
|
||||||
|
self.blocking_capture.remove(garrison)
|
||||||
|
if garrison in self.defending_front_line:
|
||||||
|
self.defending_front_line.remove(garrison)
|
||||||
|
|
||||||
|
def __contains__(self, item: VehicleGroupGroundObject) -> bool:
|
||||||
|
return item in self.in_priority_order
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_control_point(cls, control_point: ControlPoint) -> Garrisons:
|
||||||
|
"""Categorize garrison groups based on target priority.
|
||||||
|
|
||||||
|
Any garrisons blocking base capture are the highest priority.
|
||||||
|
"""
|
||||||
|
blocking = []
|
||||||
|
defending = []
|
||||||
|
garrisons = [
|
||||||
|
tgo
|
||||||
|
for tgo in control_point.ground_objects
|
||||||
|
if isinstance(tgo, VehicleGroupGroundObject) and not tgo.is_dead
|
||||||
|
]
|
||||||
|
for garrison in garrisons:
|
||||||
|
if (
|
||||||
|
meters(garrison.distance_to(control_point))
|
||||||
|
< ControlPoint.CAPTURE_DISTANCE
|
||||||
|
):
|
||||||
|
blocking.append(garrison)
|
||||||
|
else:
|
||||||
|
defending.append(garrison)
|
||||||
|
|
||||||
|
return Garrisons(blocking, defending)
|
||||||
62
game/commander/missionproposals.py
Normal file
62
game/commander/missionproposals.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from dataclasses import field, dataclass
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from game.theater import MissionTarget
|
||||||
|
from game.utils import Distance
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
class EscortType(Enum):
|
||||||
|
AirToAir = auto()
|
||||||
|
Sead = auto()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ProposedFlight:
|
||||||
|
"""A flight outline proposed by the mission planner.
|
||||||
|
|
||||||
|
Proposed flights haven't been assigned specific aircraft yet. They have only
|
||||||
|
a task, a required number of aircraft, and a maximum distance allowed
|
||||||
|
between the objective and the departure airfield.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: The flight's role.
|
||||||
|
task: FlightType
|
||||||
|
|
||||||
|
#: The number of aircraft required.
|
||||||
|
num_aircraft: int
|
||||||
|
|
||||||
|
#: The maximum distance between the objective and the departure airfield.
|
||||||
|
max_distance: Distance
|
||||||
|
|
||||||
|
#: The type of threat this flight defends against if it is an escort. Escort
|
||||||
|
#: flights will be pruned if the rest of the package is not threatened by
|
||||||
|
#: the threat they defend against. If this flight is not an escort, this
|
||||||
|
#: field is None.
|
||||||
|
escort_type: Optional[EscortType] = field(default=None)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.task} {self.num_aircraft} ship"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ProposedMission:
|
||||||
|
"""A mission outline proposed by the mission planner.
|
||||||
|
|
||||||
|
Proposed missions haven't been assigned aircraft yet. They have only an
|
||||||
|
objective location and a list of proposed flights that are required for the
|
||||||
|
mission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: The mission objective.
|
||||||
|
location: MissionTarget
|
||||||
|
|
||||||
|
#: The proposed flights that are required for the mission.
|
||||||
|
flights: list[ProposedFlight]
|
||||||
|
|
||||||
|
asap: bool = field(default=False)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
flights = ", ".join([str(f) for f in self.flights])
|
||||||
|
return f"{self.location.name}: {flights}"
|
||||||
76
game/commander/missionscheduler.py
Normal file
76
game/commander/missionscheduler.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Iterator, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
|
from game.theater import MissionTarget
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
from gen.flights.traveltime import TotEstimator
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.coalition import Coalition
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
246
game/commander/objectivefinder.py
Normal file
246
game/commander/objectivefinder.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import operator
|
||||||
|
from collections import Iterator, Iterable
|
||||||
|
from typing import TypeVar, TYPE_CHECKING
|
||||||
|
|
||||||
|
from game.theater import (
|
||||||
|
ControlPoint,
|
||||||
|
OffMapSpawn,
|
||||||
|
MissionTarget,
|
||||||
|
Fob,
|
||||||
|
FrontLine,
|
||||||
|
Airfield,
|
||||||
|
)
|
||||||
|
from game.theater.theatergroundobject import (
|
||||||
|
BuildingGroundObject,
|
||||||
|
IadsGroundObject,
|
||||||
|
NavalGroundObject,
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectiveFinder:
|
||||||
|
"""Identifies potential objectives for the mission planner."""
|
||||||
|
|
||||||
|
# TODO: Merge into doctrine.
|
||||||
|
AIRFIELD_THREAT_RANGE = nautical_miles(150)
|
||||||
|
SAM_THREAT_RANGE = nautical_miles(100)
|
||||||
|
|
||||||
|
def __init__(self, game: Game, is_player: bool) -> None:
|
||||||
|
self.game = game
|
||||||
|
self.is_player = is_player
|
||||||
|
|
||||||
|
def enemy_air_defenses(self) -> Iterator[IadsGroundObject]:
|
||||||
|
"""Iterates over all enemy SAM sites."""
|
||||||
|
for cp in self.enemy_control_points():
|
||||||
|
for ground_object in cp.ground_objects:
|
||||||
|
if ground_object.is_dead:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(ground_object, IadsGroundObject):
|
||||||
|
yield ground_object
|
||||||
|
|
||||||
|
def enemy_ships(self) -> Iterator[NavalGroundObject]:
|
||||||
|
for cp in self.enemy_control_points():
|
||||||
|
for ground_object in cp.ground_objects:
|
||||||
|
if not isinstance(ground_object, NavalGroundObject):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ground_object.is_dead:
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield ground_object
|
||||||
|
|
||||||
|
def threatening_ships(self) -> Iterator[NavalGroundObject]:
|
||||||
|
"""Iterates over enemy ships near friendly control points.
|
||||||
|
|
||||||
|
Groups are sorted by their closest proximity to any friendly control
|
||||||
|
point (airfield or fleet).
|
||||||
|
"""
|
||||||
|
return self._targets_by_range(self.enemy_ships())
|
||||||
|
|
||||||
|
def _targets_by_range(
|
||||||
|
self, targets: Iterable[MissionTargetType]
|
||||||
|
) -> Iterator[MissionTargetType]:
|
||||||
|
target_ranges: list[tuple[MissionTargetType, float]] = []
|
||||||
|
for target in targets:
|
||||||
|
ranges: list[float] = []
|
||||||
|
for cp in self.friendly_control_points():
|
||||||
|
ranges.append(target.distance_to(cp))
|
||||||
|
target_ranges.append((target, min(ranges)))
|
||||||
|
|
||||||
|
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
|
||||||
|
for target, _range in target_ranges:
|
||||||
|
yield target
|
||||||
|
|
||||||
|
def strike_targets(self) -> Iterator[BuildingGroundObject]:
|
||||||
|
"""Iterates over enemy strike targets.
|
||||||
|
|
||||||
|
Targets are sorted by their closest proximity to any friendly control
|
||||||
|
point (airfield or fleet).
|
||||||
|
"""
|
||||||
|
targets: list[tuple[BuildingGroundObject, float]] = []
|
||||||
|
# Building objectives are made of several individual TGOs (one per
|
||||||
|
# building).
|
||||||
|
found_targets: set[str] = set()
|
||||||
|
for enemy_cp in self.enemy_control_points():
|
||||||
|
for ground_object in enemy_cp.ground_objects:
|
||||||
|
# TODO: Reuse ground_object.mission_types.
|
||||||
|
# The mission types for ground objects are currently not
|
||||||
|
# accurate because we include things like strike and BAI for all
|
||||||
|
# targets since they have different planning behavior (waypoint
|
||||||
|
# generation is better for players with strike when the targets
|
||||||
|
# are stationary, AI behavior against weaker air defenses is
|
||||||
|
# better with BAI), so that's not a useful filter. Once we have
|
||||||
|
# better control over planning profiles and target dependent
|
||||||
|
# loadouts we can clean this up.
|
||||||
|
if not isinstance(ground_object, BuildingGroundObject):
|
||||||
|
# Other group types (like ships, SAMs, garrisons, etc) have better
|
||||||
|
# suited mission types like anti-ship, DEAD, and BAI.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(enemy_cp, Fob) and ground_object.is_control_point:
|
||||||
|
# This is the FOB structure itself. Can't be repaired or
|
||||||
|
# targeted by the player, so shouldn't be targetable by the
|
||||||
|
# AI.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ground_object.is_dead:
|
||||||
|
continue
|
||||||
|
if ground_object.name in found_targets:
|
||||||
|
continue
|
||||||
|
ranges: list[float] = []
|
||||||
|
for friendly_cp in self.friendly_control_points():
|
||||||
|
ranges.append(ground_object.distance_to(friendly_cp))
|
||||||
|
targets.append((ground_object, min(ranges)))
|
||||||
|
found_targets.add(ground_object.name)
|
||||||
|
targets = sorted(targets, key=operator.itemgetter(1))
|
||||||
|
for target, _range in targets:
|
||||||
|
yield target
|
||||||
|
|
||||||
|
def front_lines(self) -> Iterator[FrontLine]:
|
||||||
|
"""Iterates over all active front lines in the theater."""
|
||||||
|
yield from self.game.theater.conflicts()
|
||||||
|
|
||||||
|
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
|
||||||
|
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
|
||||||
|
|
||||||
|
Vulnerability is defined as any enemy CP within threat range of of the
|
||||||
|
CP.
|
||||||
|
"""
|
||||||
|
for cp in self.friendly_control_points():
|
||||||
|
if isinstance(cp, OffMapSpawn):
|
||||||
|
# Off-map spawn locations don't need protection.
|
||||||
|
continue
|
||||||
|
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||||
|
airfields_in_threat_range = (
|
||||||
|
airfields_in_proximity.operational_airfields_within(
|
||||||
|
self.AIRFIELD_THREAT_RANGE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for airfield in airfields_in_threat_range:
|
||||||
|
if not airfield.is_friendly(self.is_player):
|
||||||
|
yield cp
|
||||||
|
break
|
||||||
|
|
||||||
|
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
|
||||||
|
airfields = []
|
||||||
|
for control_point in self.enemy_control_points():
|
||||||
|
if not isinstance(control_point, Airfield):
|
||||||
|
continue
|
||||||
|
if control_point.base.total_aircraft >= min_aircraft:
|
||||||
|
airfields.append(control_point)
|
||||||
|
return self._targets_by_range(airfields)
|
||||||
|
|
||||||
|
def convoys(self) -> Iterator[Convoy]:
|
||||||
|
for front_line in self.front_lines():
|
||||||
|
yield from self.game.coalition_for(
|
||||||
|
self.is_player
|
||||||
|
).transfers.convoys.travelling_to(
|
||||||
|
front_line.control_point_hostile_to(self.is_player)
|
||||||
|
)
|
||||||
|
|
||||||
|
def cargo_ships(self) -> Iterator[CargoShip]:
|
||||||
|
for front_line in self.front_lines():
|
||||||
|
yield from self.game.coalition_for(
|
||||||
|
self.is_player
|
||||||
|
).transfers.cargo_ships.travelling_to(
|
||||||
|
front_line.control_point_hostile_to(self.is_player)
|
||||||
|
)
|
||||||
|
|
||||||
|
def friendly_control_points(self) -> Iterator[ControlPoint]:
|
||||||
|
"""Iterates over all friendly control points."""
|
||||||
|
return (
|
||||||
|
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
|
||||||
|
)
|
||||||
|
|
||||||
|
def farthest_friendly_control_point(self) -> ControlPoint:
|
||||||
|
"""Finds the friendly control point that is farthest from any threats."""
|
||||||
|
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
|
farthest = None
|
||||||
|
max_distance = meters(0)
|
||||||
|
for cp in self.friendly_control_points():
|
||||||
|
if isinstance(cp, OffMapSpawn):
|
||||||
|
continue
|
||||||
|
distance = threat_zones.distance_to_threat(cp.position)
|
||||||
|
if distance > max_distance:
|
||||||
|
farthest = cp
|
||||||
|
max_distance = distance
|
||||||
|
|
||||||
|
if farthest is None:
|
||||||
|
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||||
|
return farthest
|
||||||
|
|
||||||
|
def closest_friendly_control_point(self) -> ControlPoint:
|
||||||
|
"""Finds the friendly control point that is closest to any threats."""
|
||||||
|
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
|
closest = None
|
||||||
|
min_distance = meters(math.inf)
|
||||||
|
for cp in self.friendly_control_points():
|
||||||
|
if isinstance(cp, OffMapSpawn):
|
||||||
|
continue
|
||||||
|
distance = threat_zones.distance_to_threat(cp.position)
|
||||||
|
if distance < min_distance:
|
||||||
|
closest = cp
|
||||||
|
min_distance = distance
|
||||||
|
|
||||||
|
if closest is None:
|
||||||
|
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||||
|
return closest
|
||||||
|
|
||||||
|
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||||
|
"""Iterates over all enemy control points."""
|
||||||
|
return (
|
||||||
|
c
|
||||||
|
for c in self.game.theater.controlpoints
|
||||||
|
if not c.is_friendly(self.is_player)
|
||||||
|
)
|
||||||
|
|
||||||
|
def prioritized_unisolated_points(self) -> list[ControlPoint]:
|
||||||
|
prioritized = []
|
||||||
|
capturable_later = []
|
||||||
|
for cp in self.game.theater.control_points_for(not self.is_player):
|
||||||
|
if cp.is_isolated:
|
||||||
|
continue
|
||||||
|
if cp.has_active_frontline:
|
||||||
|
prioritized.append(cp)
|
||||||
|
else:
|
||||||
|
capturable_later.append(cp)
|
||||||
|
prioritized.extend(self._targets_by_range(capturable_later))
|
||||||
|
return prioritized
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def closest_airfields_to(location: MissionTarget) -> ClosestAirfields:
|
||||||
|
"""Returns the closest airfields to the given location."""
|
||||||
|
return ObjectiveDistanceCache.get_closest_airfields(location)
|
||||||
98
game/commander/packagebuilder.py
Normal file
98
game/commander/packagebuilder.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from game.commander.missionproposals import ProposedFlight
|
||||||
|
from game.dcs.aircrafttype import AircraftType
|
||||||
|
from game.inventory import GlobalAircraftInventory
|
||||||
|
from game.squadrons import AirWing
|
||||||
|
from game.theater import MissionTarget, OffMapSpawn, ControlPoint
|
||||||
|
from game.utils import nautical_miles
|
||||||
|
from gen import Package
|
||||||
|
from game.commander.aircraftallocator import AircraftAllocator
|
||||||
|
from gen.flights.closestairfields import ClosestAirfields
|
||||||
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
|
|
||||||
|
class PackageBuilder:
|
||||||
|
"""Builds a Package for the flights it receives."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
location: MissionTarget,
|
||||||
|
closest_airfields: ClosestAirfields,
|
||||||
|
global_inventory: GlobalAircraftInventory,
|
||||||
|
air_wing: AirWing,
|
||||||
|
is_player: bool,
|
||||||
|
package_country: str,
|
||||||
|
start_type: str,
|
||||||
|
asap: bool,
|
||||||
|
) -> None:
|
||||||
|
self.closest_airfields = closest_airfields
|
||||||
|
self.is_player = is_player
|
||||||
|
self.package_country = package_country
|
||||||
|
self.package = Package(location, auto_asap=asap)
|
||||||
|
self.allocator = AircraftAllocator(
|
||||||
|
air_wing, closest_airfields, global_inventory, is_player
|
||||||
|
)
|
||||||
|
self.global_inventory = global_inventory
|
||||||
|
self.start_type = start_type
|
||||||
|
|
||||||
|
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||||
|
"""Allocates aircraft for the given flight and adds them to the package.
|
||||||
|
|
||||||
|
If no suitable aircraft are available, False is returned. If the failed
|
||||||
|
flight was critical and the rest of the mission will be scrubbed, the
|
||||||
|
caller should return any previously planned flights to the inventory
|
||||||
|
using release_planned_aircraft.
|
||||||
|
"""
|
||||||
|
assignment = self.allocator.find_squadron_for_flight(plan)
|
||||||
|
if assignment is None:
|
||||||
|
return False
|
||||||
|
airfield, squadron = assignment
|
||||||
|
if isinstance(airfield, OffMapSpawn):
|
||||||
|
start_type = "In Flight"
|
||||||
|
else:
|
||||||
|
start_type = self.start_type
|
||||||
|
|
||||||
|
flight = Flight(
|
||||||
|
self.package,
|
||||||
|
self.package_country,
|
||||||
|
squadron,
|
||||||
|
plan.num_aircraft,
|
||||||
|
plan.task,
|
||||||
|
start_type,
|
||||||
|
departure=airfield,
|
||||||
|
arrival=airfield,
|
||||||
|
divert=self.find_divert_field(squadron.aircraft, airfield),
|
||||||
|
)
|
||||||
|
self.package.add_flight(flight)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def find_divert_field(
|
||||||
|
self, aircraft: AircraftType, arrival: ControlPoint
|
||||||
|
) -> Optional[ControlPoint]:
|
||||||
|
divert_limit = nautical_miles(150)
|
||||||
|
for airfield in self.closest_airfields.operational_airfields_within(
|
||||||
|
divert_limit
|
||||||
|
):
|
||||||
|
if airfield.captured != self.is_player:
|
||||||
|
continue
|
||||||
|
if airfield == arrival:
|
||||||
|
continue
|
||||||
|
if not airfield.can_operate(aircraft):
|
||||||
|
continue
|
||||||
|
if isinstance(airfield, OffMapSpawn):
|
||||||
|
continue
|
||||||
|
return airfield
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build(self) -> Package:
|
||||||
|
"""Returns the built package."""
|
||||||
|
return self.package
|
||||||
|
|
||||||
|
def release_planned_aircraft(self) -> None:
|
||||||
|
"""Returns any planned flights to the inventory."""
|
||||||
|
flights = list(self.package.flights)
|
||||||
|
for flight in flights:
|
||||||
|
self.global_inventory.return_from_flight(flight)
|
||||||
|
flight.clear_roster()
|
||||||
|
self.package.remove_flight(flight)
|
||||||
214
game/commander/packagefulfiller.py
Normal file
214
game/commander/packagefulfiller.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Set, Iterable, Dict, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from game.commander.missionproposals import ProposedMission, ProposedFlight, EscortType
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.inventory import GlobalAircraftInventory
|
||||||
|
from game.procurement import AircraftProcurementRequest
|
||||||
|
from game.profiling import MultiEventTracer
|
||||||
|
from game.settings import Settings
|
||||||
|
from game.squadrons import AirWing
|
||||||
|
from game.theater import ConflictTheater
|
||||||
|
from game.threatzones import ThreatZones
|
||||||
|
from gen import AirTaskingOrder, Package
|
||||||
|
from game.commander.packagebuilder import PackageBuilder
|
||||||
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
from gen.flights.flightplan import FlightPlanBuilder
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.coalition import Coalition
|
||||||
|
|
||||||
|
|
||||||
|
class PackageFulfiller:
|
||||||
|
"""Responsible for package aircraft allocation and flight plan layout."""
|
||||||
|
|
||||||
|
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.coalition.doctrine
|
||||||
|
|
||||||
|
@property
|
||||||
|
def threat_zones(self) -> ThreatZones:
|
||||||
|
return self.coalition.opponent.threat_zone
|
||||||
|
|
||||||
|
def add_procurement_request(self, request: AircraftProcurementRequest) -> None:
|
||||||
|
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.
|
||||||
|
|
||||||
|
Not all mission types can be fulfilled by all air wings. Many factions do not
|
||||||
|
have AEW&C aircraft, so they will never be able to plan those missions. It's
|
||||||
|
also possible for the player to exclude mission types from their squadron
|
||||||
|
designs.
|
||||||
|
"""
|
||||||
|
return self.air_wing.can_auto_plan(mission_type)
|
||||||
|
|
||||||
|
def plan_flight(
|
||||||
|
self,
|
||||||
|
mission: ProposedMission,
|
||||||
|
flight: ProposedFlight,
|
||||||
|
builder: PackageBuilder,
|
||||||
|
missing_types: Set[FlightType],
|
||||||
|
) -> None:
|
||||||
|
if not builder.plan_flight(flight):
|
||||||
|
missing_types.add(flight.task)
|
||||||
|
purchase_order = AircraftProcurementRequest(
|
||||||
|
near=mission.location,
|
||||||
|
range=flight.max_distance,
|
||||||
|
task_capability=flight.task,
|
||||||
|
number=flight.num_aircraft,
|
||||||
|
)
|
||||||
|
# Reserves are planned for critical missions, so prioritize those orders
|
||||||
|
# over aircraft needed for non-critical missions.
|
||||||
|
self.add_procurement_request(purchase_order)
|
||||||
|
|
||||||
|
def scrub_mission_missing_aircraft(
|
||||||
|
self,
|
||||||
|
mission: ProposedMission,
|
||||||
|
builder: PackageBuilder,
|
||||||
|
missing_types: Set[FlightType],
|
||||||
|
not_attempted: Iterable[ProposedFlight],
|
||||||
|
) -> None:
|
||||||
|
# Try to plan the rest of the mission just so we can count the missing
|
||||||
|
# types to buy.
|
||||||
|
for flight in not_attempted:
|
||||||
|
self.plan_flight(mission, flight, builder, missing_types)
|
||||||
|
|
||||||
|
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
|
||||||
|
builder.release_planned_aircraft()
|
||||||
|
color = "Blue" if self.is_player else "Red"
|
||||||
|
logging.debug(
|
||||||
|
f"{color}: not enough aircraft in range for {mission.location.name} "
|
||||||
|
f"capable of: {missing_types_str}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
||||||
|
threats = defaultdict(bool)
|
||||||
|
for flight in builder.package.flights:
|
||||||
|
if self.threat_zones.waypoints_threatened_by_aircraft(
|
||||||
|
flight.flight_plan.escorted_waypoints()
|
||||||
|
):
|
||||||
|
threats[EscortType.AirToAir] = True
|
||||||
|
if self.threat_zones.waypoints_threatened_by_radar_sam(
|
||||||
|
list(flight.flight_plan.escorted_waypoints())
|
||||||
|
):
|
||||||
|
threats[EscortType.Sead] = True
|
||||||
|
return threats
|
||||||
|
|
||||||
|
def plan_mission(
|
||||||
|
self, mission: ProposedMission, tracer: MultiEventTracer
|
||||||
|
) -> Optional[Package]:
|
||||||
|
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||||
|
builder = PackageBuilder(
|
||||||
|
mission.location,
|
||||||
|
ObjectiveDistanceCache.get_closest_airfields(mission.location),
|
||||||
|
self.aircraft_inventory,
|
||||||
|
self.air_wing,
|
||||||
|
self.is_player,
|
||||||
|
self.coalition.country_name,
|
||||||
|
self.default_start_type,
|
||||||
|
mission.asap,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attempt to plan all the main elements of the mission first. Escorts
|
||||||
|
# will be planned separately so we can prune escorts for packages that
|
||||||
|
# are not expected to encounter that type of threat.
|
||||||
|
missing_types: Set[FlightType] = set()
|
||||||
|
escorts = []
|
||||||
|
for proposed_flight in mission.flights:
|
||||||
|
if not self.air_wing_can_plan(proposed_flight.task):
|
||||||
|
# This air wing can never plan this mission type because they do not
|
||||||
|
# have compatible aircraft or squadrons. Skip fulfillment so that we
|
||||||
|
# don't place the purchase request.
|
||||||
|
continue
|
||||||
|
if proposed_flight.escort_type is not None:
|
||||||
|
# Escorts are planned after the primary elements of the package.
|
||||||
|
# If the package does not need escorts they may be pruned.
|
||||||
|
escorts.append(proposed_flight)
|
||||||
|
continue
|
||||||
|
with tracer.trace("Flight planning"):
|
||||||
|
self.plan_flight(mission, proposed_flight, builder, missing_types)
|
||||||
|
|
||||||
|
if missing_types:
|
||||||
|
self.scrub_mission_missing_aircraft(
|
||||||
|
mission, builder, missing_types, escorts
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not builder.package.flights:
|
||||||
|
# The non-escort part of this mission is unplannable by this faction. Scrub
|
||||||
|
# the mission and do not attempt planning escorts because there's no reason
|
||||||
|
# to buy them because this mission will never be planned.
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Create flight plans for the main flights of the package so we can
|
||||||
|
# determine threats. This is done *after* creating all of the flights
|
||||||
|
# rather than as each flight is added because the flight plan for
|
||||||
|
# flights that will rendezvous with their package will be affected by
|
||||||
|
# the other flights in the package. Escorts will not be able to
|
||||||
|
# contribute to this.
|
||||||
|
flight_plan_builder = FlightPlanBuilder(
|
||||||
|
builder.package, self.coalition, self.theater
|
||||||
|
)
|
||||||
|
for flight in builder.package.flights:
|
||||||
|
with tracer.trace("Flight plan population"):
|
||||||
|
flight_plan_builder.populate_flight_plan(flight)
|
||||||
|
|
||||||
|
needed_escorts = self.check_needed_escorts(builder)
|
||||||
|
for escort in escorts:
|
||||||
|
# This list was generated from the not None set, so this should be
|
||||||
|
# impossible.
|
||||||
|
assert escort.escort_type is not None
|
||||||
|
if needed_escorts[escort.escort_type]:
|
||||||
|
with tracer.trace("Flight planning"):
|
||||||
|
self.plan_flight(mission, escort, builder, missing_types)
|
||||||
|
|
||||||
|
# Check again for unavailable aircraft. If the escort was required and
|
||||||
|
# none were found, scrub the mission.
|
||||||
|
if missing_types:
|
||||||
|
self.scrub_mission_missing_aircraft(
|
||||||
|
mission, builder, missing_types, escorts
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
package = builder.build()
|
||||||
|
# Add flight plans for escorts.
|
||||||
|
for flight in package.flights:
|
||||||
|
if not flight.flight_plan.waypoints:
|
||||||
|
with tracer.trace("Flight plan population"):
|
||||||
|
flight_plan_builder.populate_flight_plan(flight)
|
||||||
|
|
||||||
|
if package.has_players and self.player_missions_asap:
|
||||||
|
package.auto_asap = True
|
||||||
|
package.set_tot_asap()
|
||||||
|
|
||||||
|
return package
|
||||||
11
game/commander/tasks/compound/aewcsupport.py
Normal file
11
game/commander/tasks/compound/aewcsupport.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.aewc import PlanAewc
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
class PlanAewcSupport(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for target in state.aewc_targets:
|
||||||
|
yield [PlanAewc(target)]
|
||||||
15
game/commander/tasks/compound/attackairinfrastructure.py
Normal file
15
game/commander/tasks/compound/attackairinfrastructure.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.oca import PlanOcaStrike
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AttackAirInfrastructure(CompoundTask[TheaterState]):
|
||||||
|
aircraft_cold_start: bool
|
||||||
|
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for garrison in state.oca_targets:
|
||||||
|
yield [PlanOcaStrike(garrison, self.aircraft_cold_start)]
|
||||||
15
game/commander/tasks/compound/attackbuildings.py
Normal file
15
game/commander/tasks/compound/attackbuildings.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.strike import PlanStrike
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
class AttackBuildings(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for building in state.strike_targets:
|
||||||
|
# Ammo depots are targeted based on the needs of the front line by
|
||||||
|
# ReduceEnemyFrontLineCapacity. No reason to target them before that front
|
||||||
|
# line is active.
|
||||||
|
if not building.is_ammo_depot:
|
||||||
|
yield [PlanStrike(building)]
|
||||||
12
game/commander/tasks/compound/attackgarrisons.py
Normal file
12
game/commander/tasks/compound/attackgarrisons.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.bai import PlanBai
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
class AttackGarrisons(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for garrisons in state.enemy_garrisons.values():
|
||||||
|
for garrison in garrisons.in_priority_order:
|
||||||
|
yield [PlanBai(garrison)]
|
||||||
51
game/commander/tasks/compound/capturebase.py
Normal file
51
game/commander/tasks/compound/capturebase.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.compound.destroyenemygroundunits import (
|
||||||
|
DestroyEnemyGroundUnits,
|
||||||
|
)
|
||||||
|
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
|
||||||
|
ReduceEnemyFrontLineCapacity,
|
||||||
|
)
|
||||||
|
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
from game.theater import FrontLine, ControlPoint
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CaptureBase(CompoundTask[TheaterState]):
|
||||||
|
front_line: FrontLine
|
||||||
|
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
yield [BreakthroughAttack(self.front_line, state.context.coalition.player)]
|
||||||
|
yield [DestroyEnemyGroundUnits(self.front_line)]
|
||||||
|
if self.worth_destroying_ammo_depots(state):
|
||||||
|
yield [ReduceEnemyFrontLineCapacity(self.enemy_cp(state))]
|
||||||
|
|
||||||
|
def enemy_cp(self, state: TheaterState) -> ControlPoint:
|
||||||
|
return self.front_line.control_point_hostile_to(state.context.coalition.player)
|
||||||
|
|
||||||
|
def units_deployable(self, state: TheaterState, player: bool) -> int:
|
||||||
|
cp = self.front_line.control_point_friendly_to(player)
|
||||||
|
ammo_depots = list(state.ammo_dumps_at(cp))
|
||||||
|
return cp.deployable_front_line_units_with(len(ammo_depots))
|
||||||
|
|
||||||
|
def unit_cap(self, state: TheaterState, player: bool) -> int:
|
||||||
|
cp = self.front_line.control_point_friendly_to(player)
|
||||||
|
ammo_depots = list(state.ammo_dumps_at(cp))
|
||||||
|
return cp.front_line_capacity_with(len(ammo_depots))
|
||||||
|
|
||||||
|
def enemy_has_ammo_dumps(self, state: TheaterState) -> bool:
|
||||||
|
return bool(state.ammo_dumps_at(self.enemy_cp(state)))
|
||||||
|
|
||||||
|
def worth_destroying_ammo_depots(self, state: TheaterState) -> bool:
|
||||||
|
if not self.enemy_has_ammo_dumps(state):
|
||||||
|
return False
|
||||||
|
|
||||||
|
friendly_cap = self.unit_cap(state, state.context.coalition.player)
|
||||||
|
enemy_deployable = self.units_deployable(state, state.context.coalition.player)
|
||||||
|
|
||||||
|
# If the enemy can currently deploy 50% more units than we possibly could, it's
|
||||||
|
# worth killing an ammo depot.
|
||||||
|
return enemy_deployable / friendly_cap > 1.5
|
||||||
13
game/commander/tasks/compound/capturebases.py
Normal file
13
game/commander/tasks/compound/capturebases.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.compound.capturebase import CaptureBase
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class CaptureBases(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for front in state.active_front_lines:
|
||||||
|
yield [CaptureBase(front)]
|
||||||
19
game/commander/tasks/compound/defendbase.py
Normal file
19
game/commander/tasks/compound/defendbase.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.cas import PlanCas
|
||||||
|
from game.commander.tasks.primitive.defensivestance import DefensiveStance
|
||||||
|
from game.commander.tasks.primitive.retreatstance import RetreatStance
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
from game.theater import FrontLine
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DefendBase(CompoundTask[TheaterState]):
|
||||||
|
front_line: FrontLine
|
||||||
|
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
yield [DefensiveStance(self.front_line, state.context.coalition.player)]
|
||||||
|
yield [RetreatStance(self.front_line, state.context.coalition.player)]
|
||||||
|
yield [PlanCas(self.front_line)]
|
||||||
13
game/commander/tasks/compound/defendbases.py
Normal file
13
game/commander/tasks/compound/defendbases.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.compound.defendbase import DefendBase
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DefendBases(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for front in state.active_front_lines:
|
||||||
|
yield [DefendBase(front)]
|
||||||
24
game/commander/tasks/compound/degradeiads.py
Normal file
24
game/commander/tasks/compound/degradeiads.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.antiship import PlanAntiShip
|
||||||
|
from game.commander.tasks.primitive.dead import PlanDead
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
||||||
|
|
||||||
|
|
||||||
|
class DegradeIads(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for air_defense in state.threatening_air_defenses:
|
||||||
|
yield [self.plan_against(air_defense)]
|
||||||
|
for detector in state.detecting_air_defenses:
|
||||||
|
yield [self.plan_against(detector)]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def plan_against(
|
||||||
|
target: Union[IadsGroundObject, NavalGroundObject]
|
||||||
|
) -> Union[PlanDead, PlanAntiShip]:
|
||||||
|
if isinstance(target, IadsGroundObject):
|
||||||
|
return PlanDead(target)
|
||||||
|
return PlanAntiShip(target)
|
||||||
19
game/commander/tasks/compound/destroyenemygroundunits.py
Normal file
19
game/commander/tasks/compound/destroyenemygroundunits.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.aggressiveattack import AggressiveAttack
|
||||||
|
from game.commander.tasks.primitive.cas import PlanCas
|
||||||
|
from game.commander.tasks.primitive.eliminationattack import EliminationAttack
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
from game.theater import FrontLine
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class DestroyEnemyGroundUnits(CompoundTask[TheaterState]):
|
||||||
|
front_line: FrontLine
|
||||||
|
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
yield [EliminationAttack(self.front_line, state.context.coalition.player)]
|
||||||
|
yield [AggressiveAttack(self.front_line, state.context.coalition.player)]
|
||||||
|
yield [PlanCas(self.front_line)]
|
||||||
11
game/commander/tasks/compound/frontlinedefense.py
Normal file
11
game/commander/tasks/compound/frontlinedefense.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.cas import PlanCas
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
class FrontLineDefense(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for front_line in state.vulnerable_front_lines:
|
||||||
|
yield [PlanCas(front_line)]
|
||||||
27
game/commander/tasks/compound/interdictreinforcements.py
Normal file
27
game/commander/tasks/compound/interdictreinforcements.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.antishipping import PlanAntiShipping
|
||||||
|
from game.commander.tasks.primitive.convoyinterdiction import PlanConvoyInterdiction
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
class InterdictReinforcements(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
# These will only rarely get planned. When a convoy is travelling multiple legs,
|
||||||
|
# they're targetable after the first leg. The reason for this is that
|
||||||
|
# procurement happens *after* mission planning so that the missions that could
|
||||||
|
# not be filled will guide the procurement process. Procurement is the stage
|
||||||
|
# that convoys are created (because they're created to move ground units that
|
||||||
|
# were just purchased), so we haven't created any yet. Any incomplete transfers
|
||||||
|
# from the previous turn (multi-leg journeys) will still be present though so
|
||||||
|
# they can be targeted.
|
||||||
|
#
|
||||||
|
# Even after this is fixed, the player's convoys that were created through the
|
||||||
|
# UI will never be targeted on the first turn of their journey because the AI
|
||||||
|
# stops planning after the start of the turn. We could potentially fix this by
|
||||||
|
# moving opfor mission planning until the takeoff button is pushed.
|
||||||
|
for convoy in state.enemy_convoys:
|
||||||
|
yield [PlanConvoyInterdiction(convoy)]
|
||||||
|
for ship in state.enemy_shipping:
|
||||||
|
yield [PlanAntiShipping(ship)]
|
||||||
34
game/commander/tasks/compound/nextaction.py
Normal file
34
game/commander/tasks/compound/nextaction.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.compound.attackairinfrastructure import (
|
||||||
|
AttackAirInfrastructure,
|
||||||
|
)
|
||||||
|
from game.commander.tasks.compound.attackbuildings import AttackBuildings
|
||||||
|
from game.commander.tasks.compound.attackgarrisons import AttackGarrisons
|
||||||
|
from game.commander.tasks.compound.capturebases import CaptureBases
|
||||||
|
from game.commander.tasks.compound.defendbases import DefendBases
|
||||||
|
from game.commander.tasks.compound.degradeiads import DegradeIads
|
||||||
|
from game.commander.tasks.compound.interdictreinforcements import (
|
||||||
|
InterdictReinforcements,
|
||||||
|
)
|
||||||
|
from game.commander.tasks.compound.protectairspace import ProtectAirSpace
|
||||||
|
from game.commander.tasks.compound.theatersupport import TheaterSupport
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlanNextAction(CompoundTask[TheaterState]):
|
||||||
|
aircraft_cold_start: bool
|
||||||
|
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
yield [TheaterSupport()]
|
||||||
|
yield [ProtectAirSpace()]
|
||||||
|
yield [CaptureBases()]
|
||||||
|
yield [DefendBases()]
|
||||||
|
yield [InterdictReinforcements()]
|
||||||
|
yield [AttackGarrisons()]
|
||||||
|
yield [AttackAirInfrastructure(self.aircraft_cold_start)]
|
||||||
|
yield [AttackBuildings()]
|
||||||
|
yield [DegradeIads()]
|
||||||
12
game/commander/tasks/compound/protectairspace.py
Normal file
12
game/commander/tasks/compound/protectairspace.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.barcap import PlanBarcap
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
class ProtectAirSpace(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for cp, needed in state.barcaps_needed.items():
|
||||||
|
if needed > 0:
|
||||||
|
yield [PlanBarcap(cp)]
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.strike import PlanStrike
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
from game.theater import ControlPoint
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ReduceEnemyFrontLineCapacity(CompoundTask[TheaterState]):
|
||||||
|
control_point: ControlPoint
|
||||||
|
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for ammo_dump in state.ammo_dumps_at(self.control_point):
|
||||||
|
yield [PlanStrike(ammo_dump)]
|
||||||
11
game/commander/tasks/compound/refuelingsupport.py
Normal file
11
game/commander/tasks/compound/refuelingsupport.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
|
||||||
|
from game.commander.tasks.primitive.refueling import PlanRefueling
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
class PlanRefuelingSupport(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
for target in state.refueling_targets:
|
||||||
|
yield [PlanRefueling(target)]
|
||||||
14
game/commander/tasks/compound/theatersupport.py
Normal file
14
game/commander/tasks/compound/theatersupport.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.compound.aewcsupport import PlanAewcSupport
|
||||||
|
from game.commander.tasks.compound.refuelingsupport import PlanRefuelingSupport
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import CompoundTask, Method
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TheaterSupport(CompoundTask[TheaterState]):
|
||||||
|
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||||
|
yield [PlanAewcSupport()]
|
||||||
|
yield [PlanRefuelingSupport()]
|
||||||
75
game/commander/tasks/frontlinestancetask.py
Normal file
75
game/commander/tasks/frontlinestancetask.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.theater import FrontLine
|
||||||
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.coalition import Coalition
|
||||||
|
|
||||||
|
|
||||||
|
class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
||||||
|
def __init__(self, front_line: FrontLine, player: bool) -> None:
|
||||||
|
self.front_line = front_line
|
||||||
|
self.friendly_cp = self.front_line.control_point_friendly_to(player)
|
||||||
|
self.enemy_cp = self.front_line.control_point_hostile_to(player)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def stance(self) -> CombatStance:
|
||||||
|
...
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def management_allowed(state: TheaterState) -> bool:
|
||||||
|
return (
|
||||||
|
not state.context.coalition.player
|
||||||
|
or state.context.settings.automate_front_line_stance
|
||||||
|
)
|
||||||
|
|
||||||
|
def better_stance_already_set(self, state: TheaterState) -> bool:
|
||||||
|
current_stance = state.front_line_stances[self.front_line]
|
||||||
|
if current_stance is None:
|
||||||
|
return False
|
||||||
|
preference = (
|
||||||
|
CombatStance.RETREAT,
|
||||||
|
CombatStance.DEFENSIVE,
|
||||||
|
CombatStance.AMBUSH,
|
||||||
|
CombatStance.AGGRESSIVE,
|
||||||
|
CombatStance.ELIMINATION,
|
||||||
|
CombatStance.BREAKTHROUGH,
|
||||||
|
)
|
||||||
|
current_rating = preference.index(current_stance)
|
||||||
|
new_rating = preference.index(self.stance)
|
||||||
|
return current_rating >= new_rating
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def have_sufficient_front_line_advantage(self) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ground_force_balance(self) -> float:
|
||||||
|
# TODO: Planned CAS missions should reduce the expected opposing force size.
|
||||||
|
friendly_forces = self.friendly_cp.deployable_front_line_units
|
||||||
|
enemy_forces = self.enemy_cp.deployable_front_line_units
|
||||||
|
if enemy_forces == 0:
|
||||||
|
return math.inf
|
||||||
|
return friendly_forces / enemy_forces
|
||||||
|
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if not self.management_allowed(state):
|
||||||
|
return False
|
||||||
|
if self.better_stance_already_set(state):
|
||||||
|
return False
|
||||||
|
return self.have_sufficient_front_line_advantage
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.front_line_stances[self.front_line] = self.stance
|
||||||
|
|
||||||
|
def execute(self, coalition: Coalition) -> None:
|
||||||
|
self.friendly_cp.stances[self.enemy_cp.id] = self.stance
|
||||||
171
game/commander/tasks/packageplanningtask.py
Normal file
171
game/commander/tasks/packageplanningtask.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
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, 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 fulfill_mission(self, state: TheaterState) -> bool:
|
||||||
|
self.propose_flights(state.context.coalition.doctrine)
|
||||||
|
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, 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_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
|
||||||
28
game/commander/tasks/primitive/aewc.py
Normal file
28
game/commander/tasks/primitive/aewc.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater import MissionTarget
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanAewc(PackagePlanningTask[MissionTarget]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if not super().preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return self.target in state.aewc_targets
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.aewc_targets.remove(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.AEWC, 1, doctrine.mission_ranges.aewc)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def asap(self) -> bool:
|
||||||
|
# Supports all the early CAP flights, so should be in the air ASAP.
|
||||||
|
return True
|
||||||
14
game/commander/tasks/primitive/aggressiveattack.py
Normal file
14
game/commander/tasks/primitive/aggressiveattack.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||||
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
|
||||||
|
|
||||||
|
class AggressiveAttack(FrontLineStanceTask):
|
||||||
|
@property
|
||||||
|
def stance(self) -> CombatStance:
|
||||||
|
return CombatStance.AGGRESSIVE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def have_sufficient_front_line_advantage(self) -> bool:
|
||||||
|
return self.ground_force_balance >= 0.8
|
||||||
32
game/commander/tasks/primitive/antiship.py
Normal file
32
game/commander/tasks/primitive/antiship.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.missionproposals import EscortType
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater.theatergroundobject import NavalGroundObject
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if self.target not in state.threatening_air_defenses:
|
||||||
|
return False
|
||||||
|
if not self.target_area_preconditions_met(state, ignore_iads=True):
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.eliminate_ship(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.ANTISHIP, 2, doctrine.mission_ranges.offensive)
|
||||||
|
self.propose_flight(
|
||||||
|
FlightType.ESCORT,
|
||||||
|
2,
|
||||||
|
doctrine.mission_ranges.offensive,
|
||||||
|
EscortType.AirToAir,
|
||||||
|
)
|
||||||
26
game/commander/tasks/primitive/antishipping.py
Normal file
26
game/commander/tasks/primitive/antishipping.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.transfers import CargoShip
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if self.target not in state.enemy_shipping:
|
||||||
|
return False
|
||||||
|
if not self.target_area_preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.enemy_shipping.remove(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.ANTISHIP, 2, doctrine.mission_ranges.offensive)
|
||||||
|
self.propose_common_escorts(doctrine)
|
||||||
26
game/commander/tasks/primitive/bai.py
Normal file
26
game/commander/tasks/primitive/bai.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if not state.has_garrison(self.target):
|
||||||
|
return False
|
||||||
|
if not self.target_area_preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.eliminate_garrison(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive)
|
||||||
|
self.propose_common_escorts(doctrine)
|
||||||
23
game/commander/tasks/primitive/barcap.py
Normal file
23
game/commander/tasks/primitive/barcap.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater import ControlPoint
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if not state.barcaps_needed[self.target]:
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.barcaps_needed[self.target] -= 1
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.BARCAP, 2, doctrine.mission_ranges.cap)
|
||||||
28
game/commander/tasks/primitive/breakthroughattack.py
Normal file
28
game/commander/tasks/primitive/breakthroughattack.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
|
||||||
|
|
||||||
|
class BreakthroughAttack(FrontLineStanceTask):
|
||||||
|
@property
|
||||||
|
def stance(self) -> CombatStance:
|
||||||
|
return CombatStance.BREAKTHROUGH
|
||||||
|
|
||||||
|
@property
|
||||||
|
def have_sufficient_front_line_advantage(self) -> bool:
|
||||||
|
return self.ground_force_balance >= 2.0
|
||||||
|
|
||||||
|
def opposing_garrisons_eliminated(self, state: TheaterState) -> bool:
|
||||||
|
garrisons = state.enemy_garrisons[self.enemy_cp]
|
||||||
|
return not bool(garrisons.blocking_capture)
|
||||||
|
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if not super().preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return self.opposing_garrisons_eliminated(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
super().apply_effects(state)
|
||||||
|
state.active_front_lines.remove(self.front_line)
|
||||||
24
game/commander/tasks/primitive/cas.py
Normal file
24
game/commander/tasks/primitive/cas.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater import FrontLine
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanCas(PackagePlanningTask[FrontLine]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if self.target not in state.vulnerable_front_lines:
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.vulnerable_front_lines.remove(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.CAS, 2, doctrine.mission_ranges.cas)
|
||||||
|
self.propose_flight(FlightType.TARCAP, 2, doctrine.mission_ranges.cap)
|
||||||
26
game/commander/tasks/primitive/convoyinterdiction.py
Normal file
26
game/commander/tasks/primitive/convoyinterdiction.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.transfers import Convoy
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanConvoyInterdiction(PackagePlanningTask[Convoy]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if self.target not in state.enemy_convoys:
|
||||||
|
return False
|
||||||
|
if not self.target_area_preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.enemy_convoys.remove(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.BAI, 2, doctrine.mission_ranges.offensive)
|
||||||
|
self.propose_common_escorts(doctrine)
|
||||||
58
game/commander/tasks/primitive/dead.py
Normal file
58
game/commander/tasks/primitive/dead.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.missionproposals import EscortType
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater.theatergroundobject import IadsGroundObject
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanDead(PackagePlanningTask[IadsGroundObject]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if (
|
||||||
|
self.target not in state.threatening_air_defenses
|
||||||
|
and self.target not in state.detecting_air_defenses
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
if not self.target_area_preconditions_met(state, ignore_iads=True):
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.eliminate_air_defense(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.DEAD, 2, doctrine.mission_ranges.offensive)
|
||||||
|
|
||||||
|
# Only include SEAD against SAMs that still have emitters. No need to
|
||||||
|
# suppress an EWR, and SEAD isn't useful against a SAM that no longer has a
|
||||||
|
# working track radar.
|
||||||
|
#
|
||||||
|
# For SAMs without track radars and EWRs, we still want a SEAD escort if
|
||||||
|
# needed.
|
||||||
|
#
|
||||||
|
# Note that there is a quirk here: we should potentially be included a SEAD
|
||||||
|
# escort *and* SEAD when the target is a radar SAM but the flight path is
|
||||||
|
# also threatened by SAMs. We don't want to include a SEAD escort if the
|
||||||
|
# package is *only* threatened by the target though. Could be improved, but
|
||||||
|
# needs a decent refactor to the escort planning to do so.
|
||||||
|
if self.target.has_live_radar_sam:
|
||||||
|
self.propose_flight(FlightType.SEAD, 2, doctrine.mission_ranges.offensive)
|
||||||
|
else:
|
||||||
|
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,
|
||||||
|
)
|
||||||
14
game/commander/tasks/primitive/defensivestance.py
Normal file
14
game/commander/tasks/primitive/defensivestance.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||||
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
|
||||||
|
|
||||||
|
class DefensiveStance(FrontLineStanceTask):
|
||||||
|
@property
|
||||||
|
def stance(self) -> CombatStance:
|
||||||
|
return CombatStance.DEFENSIVE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def have_sufficient_front_line_advantage(self) -> bool:
|
||||||
|
return self.ground_force_balance >= 0.5
|
||||||
14
game/commander/tasks/primitive/eliminationattack.py
Normal file
14
game/commander/tasks/primitive/eliminationattack.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||||
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
|
||||||
|
|
||||||
|
class EliminationAttack(FrontLineStanceTask):
|
||||||
|
@property
|
||||||
|
def stance(self) -> CombatStance:
|
||||||
|
return CombatStance.ELIMINATION
|
||||||
|
|
||||||
|
@property
|
||||||
|
def have_sufficient_front_line_advantage(self) -> bool:
|
||||||
|
return self.ground_force_balance >= 1.5
|
||||||
32
game/commander/tasks/primitive/oca.py
Normal file
32
game/commander/tasks/primitive/oca.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater import ControlPoint
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
|
||||||
|
aircraft_cold_start: bool
|
||||||
|
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if self.target not in state.oca_targets:
|
||||||
|
return False
|
||||||
|
if not self.target_area_preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.oca_targets.remove(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.OCA_RUNWAY, 2, doctrine.mission_ranges.offensive)
|
||||||
|
if self.aircraft_cold_start:
|
||||||
|
self.propose_flight(
|
||||||
|
FlightType.OCA_AIRCRAFT, 2, doctrine.mission_ranges.offensive
|
||||||
|
)
|
||||||
|
self.propose_common_escorts(doctrine)
|
||||||
23
game/commander/tasks/primitive/refueling.py
Normal file
23
game/commander/tasks/primitive/refueling.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater import MissionTarget
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanRefueling(PackagePlanningTask[MissionTarget]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if not super().preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return self.target in state.refueling_targets
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.refueling_targets.remove(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.REFUELING, 1, doctrine.mission_ranges.refueling)
|
||||||
14
game/commander/tasks/primitive/retreatstance.py
Normal file
14
game/commander/tasks/primitive/retreatstance.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||||
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
|
||||||
|
|
||||||
|
class RetreatStance(FrontLineStanceTask):
|
||||||
|
@property
|
||||||
|
def stance(self) -> CombatStance:
|
||||||
|
return CombatStance.RETREAT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def have_sufficient_front_line_advantage(self) -> bool:
|
||||||
|
return True
|
||||||
27
game/commander/tasks/primitive/strike.py
Normal file
27
game/commander/tasks/primitive/strike.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]):
|
||||||
|
def preconditions_met(self, state: TheaterState) -> bool:
|
||||||
|
if self.target not in state.strike_targets:
|
||||||
|
return False
|
||||||
|
if not self.target_area_preconditions_met(state):
|
||||||
|
return False
|
||||||
|
return super().preconditions_met(state)
|
||||||
|
|
||||||
|
def apply_effects(self, state: TheaterState) -> None:
|
||||||
|
state.strike_targets.remove(self.target)
|
||||||
|
|
||||||
|
def propose_flights(self, doctrine: Doctrine) -> None:
|
||||||
|
self.propose_flight(FlightType.STRIKE, 2, doctrine.mission_ranges.offensive)
|
||||||
|
self.propose_common_escorts(doctrine)
|
||||||
16
game/commander/tasks/theatercommandertask.py
Normal file
16
game/commander/tasks/theatercommandertask.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import abstractmethod
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import PrimitiveTask
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.coalition import Coalition
|
||||||
|
|
||||||
|
|
||||||
|
class TheaterCommanderTask(PrimitiveTask[TheaterState]):
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self, coalition: Coalition) -> None:
|
||||||
|
...
|
||||||
88
game/commander/theatercommander.py
Normal file
88
game/commander/theatercommander.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""The Theater Commander is the highest level campaign AI.
|
||||||
|
|
||||||
|
Target selection is performed with a hierarchical-task-network (HTN, linked below).
|
||||||
|
These work by giving the planner an initial "task" which decomposes into other tasks
|
||||||
|
until a concrete set of actions is formed. For example, the "capture base" task may
|
||||||
|
decompose in the following manner:
|
||||||
|
|
||||||
|
* Defend
|
||||||
|
* Reinforce front line
|
||||||
|
* Set front line stance to defend
|
||||||
|
* Destroy enemy front line units
|
||||||
|
* Set front line stance to elimination
|
||||||
|
* Plan CAS at front line
|
||||||
|
* Prepare
|
||||||
|
* Destroy enemy IADS
|
||||||
|
* Plan DEAD against SAM Armadillo
|
||||||
|
* ...
|
||||||
|
* Destroy enemy front line units
|
||||||
|
* Set front line stance to elimination
|
||||||
|
* Plan CAS at front line
|
||||||
|
* Inhibit
|
||||||
|
* Destroy enemy unit production infrastructure
|
||||||
|
* Destroy factory at Palmyra
|
||||||
|
* ...
|
||||||
|
* Destroy enemy front line units
|
||||||
|
* Set front line stance to elimination
|
||||||
|
* Plan CAS at front line
|
||||||
|
* Attack
|
||||||
|
* Set front line stance to breakthrough
|
||||||
|
* Destroy enemy front line units
|
||||||
|
* Set front line stance to elimination
|
||||||
|
* Plan CAS at front line
|
||||||
|
|
||||||
|
This is not a reflection of the actual task composition but illustrates the capability
|
||||||
|
of the system. Each task has preconditions which are checked before the task is
|
||||||
|
decomposed. If preconditions are not met the task is ignored and the next is considered.
|
||||||
|
For example the task to destroy the factory at Palmyra might be excluded until the air
|
||||||
|
defenses protecting it are eliminated; or defensive air operations might be excluded if
|
||||||
|
the enemy does not have sufficient air forces, or if the protected target has sufficient
|
||||||
|
SAM coverage.
|
||||||
|
|
||||||
|
Each action updates the world state, which causes each action to account for the result
|
||||||
|
of the tasks executed before it. Above, the preconditions for attacking the factory at
|
||||||
|
Palmyra may not have been met due to the IADS coverage, leading the planning to decide
|
||||||
|
on an attack against the IADS in the area instead. When planning the next task in the
|
||||||
|
same turn, the world state will have been updated to account for the (hopefully)
|
||||||
|
destroyed SAM sites, allowing the planner to choose the mission to attack the factory.
|
||||||
|
|
||||||
|
Preconditions can be aware of previous actions as well. A precondition for "Plan CAS at
|
||||||
|
front line" can be "No CAS missions planned at front line" to avoid over-planning CAS
|
||||||
|
even though it is a primitive task used by many other tasks.
|
||||||
|
|
||||||
|
https://en.wikipedia.org/wiki/Hierarchical_task_network
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from game.commander.tasks.compound.nextaction import PlanNextAction
|
||||||
|
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||||
|
from game.commander.theaterstate import TheaterState
|
||||||
|
from game.htn import Planner
|
||||||
|
from game.profiling import MultiEventTracer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
|
||||||
|
|
||||||
|
class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
|
||||||
|
def __init__(self, game: Game, player: bool) -> None:
|
||||||
|
super().__init__(
|
||||||
|
PlanNextAction(
|
||||||
|
aircraft_cold_start=game.settings.default_start_type == "Cold"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.game = game
|
||||||
|
self.player = player
|
||||||
|
|
||||||
|
def plan_missions(self, tracer: MultiEventTracer) -> None:
|
||||||
|
state = TheaterState.from_game(self.game, self.player, tracer)
|
||||||
|
while True:
|
||||||
|
result = self.plan(state)
|
||||||
|
if result is None:
|
||||||
|
# Planned all viable tasks this turn.
|
||||||
|
return
|
||||||
|
for task in result.tasks:
|
||||||
|
task.execute(self.game.coalition_for(self.player))
|
||||||
|
state = result.end_state
|
||||||
176
game/commander/theaterstate.py
Normal file
176
game/commander/theaterstate.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import itertools
|
||||||
|
import math
|
||||||
|
from collections import Iterator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Union, Optional
|
||||||
|
|
||||||
|
from game.commander.garrisons import Garrisons
|
||||||
|
from game.commander.objectivefinder import ObjectiveFinder
|
||||||
|
from game.htn import WorldState
|
||||||
|
from game.inventory import GlobalAircraftInventory
|
||||||
|
from game.profiling import MultiEventTracer
|
||||||
|
from game.settings import Settings
|
||||||
|
from game.theater import ControlPoint, FrontLine, MissionTarget, ConflictTheater
|
||||||
|
from game.theater.theatergroundobject import (
|
||||||
|
TheaterGroundObject,
|
||||||
|
NavalGroundObject,
|
||||||
|
IadsGroundObject,
|
||||||
|
VehicleGroupGroundObject,
|
||||||
|
BuildingGroundObject,
|
||||||
|
)
|
||||||
|
from game.threatzones import ThreatZones
|
||||||
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
from game.coalition import Coalition
|
||||||
|
from game.transfers import Convoy, CargoShip
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PersistentContext:
|
||||||
|
coalition: Coalition
|
||||||
|
theater: ConflictTheater
|
||||||
|
settings: Settings
|
||||||
|
tracer: MultiEventTracer
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TheaterState(WorldState["TheaterState"]):
|
||||||
|
context: PersistentContext
|
||||||
|
barcaps_needed: dict[ControlPoint, int]
|
||||||
|
active_front_lines: list[FrontLine]
|
||||||
|
front_line_stances: dict[FrontLine, Optional[CombatStance]]
|
||||||
|
vulnerable_front_lines: list[FrontLine]
|
||||||
|
aewc_targets: list[MissionTarget]
|
||||||
|
refueling_targets: list[MissionTarget]
|
||||||
|
enemy_air_defenses: list[IadsGroundObject]
|
||||||
|
threatening_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]]
|
||||||
|
detecting_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]]
|
||||||
|
enemy_convoys: list[Convoy]
|
||||||
|
enemy_shipping: list[CargoShip]
|
||||||
|
enemy_ships: list[NavalGroundObject]
|
||||||
|
enemy_garrisons: dict[ControlPoint, Garrisons]
|
||||||
|
oca_targets: list[ControlPoint]
|
||||||
|
strike_targets: list[TheaterGroundObject[Any]]
|
||||||
|
enemy_barcaps: list[ControlPoint]
|
||||||
|
threat_zones: ThreatZones
|
||||||
|
available_aircraft: GlobalAircraftInventory
|
||||||
|
|
||||||
|
def _rebuild_threat_zones(self) -> None:
|
||||||
|
"""Recreates the theater's threat zones based on the current planned state."""
|
||||||
|
self.threat_zones = ThreatZones.for_threats(
|
||||||
|
self.context.coalition.opponent.doctrine,
|
||||||
|
barcap_locations=self.enemy_barcaps,
|
||||||
|
air_defenses=itertools.chain(self.enemy_air_defenses, self.enemy_ships),
|
||||||
|
)
|
||||||
|
|
||||||
|
def eliminate_air_defense(self, target: IadsGroundObject) -> None:
|
||||||
|
if target in self.threatening_air_defenses:
|
||||||
|
self.threatening_air_defenses.remove(target)
|
||||||
|
if target in self.detecting_air_defenses:
|
||||||
|
self.detecting_air_defenses.remove(target)
|
||||||
|
self.enemy_air_defenses.remove(target)
|
||||||
|
self._rebuild_threat_zones()
|
||||||
|
|
||||||
|
def eliminate_ship(self, target: NavalGroundObject) -> None:
|
||||||
|
if target in self.threatening_air_defenses:
|
||||||
|
self.threatening_air_defenses.remove(target)
|
||||||
|
if target in self.detecting_air_defenses:
|
||||||
|
self.detecting_air_defenses.remove(target)
|
||||||
|
self.enemy_ships.remove(target)
|
||||||
|
self._rebuild_threat_zones()
|
||||||
|
|
||||||
|
def has_garrison(self, target: VehicleGroupGroundObject) -> bool:
|
||||||
|
return target in self.enemy_garrisons[target.control_point]
|
||||||
|
|
||||||
|
def eliminate_garrison(self, target: VehicleGroupGroundObject) -> None:
|
||||||
|
self.enemy_garrisons[target.control_point].eliminate(target)
|
||||||
|
|
||||||
|
def ammo_dumps_at(
|
||||||
|
self, control_point: ControlPoint
|
||||||
|
) -> Iterator[BuildingGroundObject]:
|
||||||
|
for target in self.strike_targets:
|
||||||
|
if target.control_point != control_point:
|
||||||
|
continue
|
||||||
|
if target.is_ammo_depot:
|
||||||
|
assert isinstance(target, BuildingGroundObject)
|
||||||
|
yield target
|
||||||
|
|
||||||
|
def clone(self) -> TheaterState:
|
||||||
|
# Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly
|
||||||
|
# expensive.
|
||||||
|
return TheaterState(
|
||||||
|
context=self.context,
|
||||||
|
barcaps_needed=dict(self.barcaps_needed),
|
||||||
|
active_front_lines=list(self.active_front_lines),
|
||||||
|
front_line_stances=dict(self.front_line_stances),
|
||||||
|
vulnerable_front_lines=list(self.vulnerable_front_lines),
|
||||||
|
aewc_targets=list(self.aewc_targets),
|
||||||
|
refueling_targets=list(self.refueling_targets),
|
||||||
|
enemy_air_defenses=list(self.enemy_air_defenses),
|
||||||
|
enemy_convoys=list(self.enemy_convoys),
|
||||||
|
enemy_shipping=list(self.enemy_shipping),
|
||||||
|
enemy_ships=list(self.enemy_ships),
|
||||||
|
enemy_garrisons={
|
||||||
|
cp: dataclasses.replace(g) for cp, g in self.enemy_garrisons.items()
|
||||||
|
},
|
||||||
|
oca_targets=list(self.oca_targets),
|
||||||
|
strike_targets=list(self.strike_targets),
|
||||||
|
enemy_barcaps=list(self.enemy_barcaps),
|
||||||
|
threat_zones=self.threat_zones,
|
||||||
|
available_aircraft=self.available_aircraft.clone(),
|
||||||
|
# Persistent properties are not copied. These are a way for failed subtasks
|
||||||
|
# to communicate requirements to other tasks. For example, the task to
|
||||||
|
# attack enemy garrisons might fail because the target area has IADS
|
||||||
|
# protection. In that case, the preconditions of PlanBai would fail, but
|
||||||
|
# would add the IADS that prevented it from being planned to the list of
|
||||||
|
# IADS threats so that DegradeIads will consider it a threat later.
|
||||||
|
threatening_air_defenses=self.threatening_air_defenses,
|
||||||
|
detecting_air_defenses=self.detecting_air_defenses,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_game(
|
||||||
|
cls, game: Game, player: bool, tracer: MultiEventTracer
|
||||||
|
) -> TheaterState:
|
||||||
|
coalition = game.coalition_for(player)
|
||||||
|
finder = ObjectiveFinder(game, player)
|
||||||
|
ordered_capturable_points = finder.prioritized_unisolated_points()
|
||||||
|
|
||||||
|
context = PersistentContext(coalition, game.theater, game.settings, tracer)
|
||||||
|
|
||||||
|
# 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 = coalition.doctrine.cap_duration.total_seconds()
|
||||||
|
barcap_rounds = math.ceil(mission_duration / barcap_duration)
|
||||||
|
|
||||||
|
return TheaterState(
|
||||||
|
context=context,
|
||||||
|
barcaps_needed={
|
||||||
|
cp: barcap_rounds for cp in finder.vulnerable_control_points()
|
||||||
|
},
|
||||||
|
active_front_lines=list(finder.front_lines()),
|
||||||
|
front_line_stances={f: None for f in finder.front_lines()},
|
||||||
|
vulnerable_front_lines=list(finder.front_lines()),
|
||||||
|
aewc_targets=[finder.farthest_friendly_control_point()],
|
||||||
|
refueling_targets=[finder.closest_friendly_control_point()],
|
||||||
|
enemy_air_defenses=list(finder.enemy_air_defenses()),
|
||||||
|
threatening_air_defenses=[],
|
||||||
|
detecting_air_defenses=[],
|
||||||
|
enemy_convoys=list(finder.convoys()),
|
||||||
|
enemy_shipping=list(finder.cargo_ships()),
|
||||||
|
enemy_ships=list(finder.enemy_ships()),
|
||||||
|
enemy_garrisons={
|
||||||
|
cp: Garrisons.for_control_point(cp) for cp in ordered_capturable_points
|
||||||
|
},
|
||||||
|
oca_targets=list(finder.oca_targets(min_aircraft=20)),
|
||||||
|
strike_targets=list(finder.strike_targets()),
|
||||||
|
enemy_barcaps=list(game.theater.control_points_for(not player)),
|
||||||
|
threat_zones=game.threat_zone_for(not player),
|
||||||
|
available_aircraft=game.aircraft_inventory.clone(),
|
||||||
|
)
|
||||||
@ -1,9 +1,10 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from dcs.task import Reconnaissance
|
from typing import Any
|
||||||
|
|
||||||
from game.utils import Distance, feet, nautical_miles
|
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.groundunitclass import GroundUnitClass
|
||||||
|
from game.savecompat import has_save_compat_for
|
||||||
|
from game.utils import Distance, feet, nautical_miles
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -17,6 +18,15 @@ class GroundUnitProcurementRatios:
|
|||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class MissionPlannerMaxRanges:
|
||||||
|
cap: Distance = field(default=nautical_miles(100))
|
||||||
|
cas: Distance = field(default=nautical_miles(50))
|
||||||
|
offensive: Distance = field(default=nautical_miles(150))
|
||||||
|
aewc: Distance = field(default=Distance.inf())
|
||||||
|
refueling: Distance = field(default=nautical_miles(200))
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Doctrine:
|
class Doctrine:
|
||||||
cas: bool
|
cas: bool
|
||||||
@ -26,13 +36,21 @@ class Doctrine:
|
|||||||
antiship: bool
|
antiship: bool
|
||||||
|
|
||||||
rendezvous_altitude: Distance
|
rendezvous_altitude: Distance
|
||||||
|
|
||||||
|
#: The minimum distance between the departure airfield and the hold point.
|
||||||
hold_distance: Distance
|
hold_distance: Distance
|
||||||
|
|
||||||
|
#: The minimum distance between the hold point and the join point.
|
||||||
push_distance: Distance
|
push_distance: Distance
|
||||||
|
|
||||||
|
#: The distance between the join point and the ingress point. Only used for the
|
||||||
|
#: fallback flight plan layout (when the departure airfield is near a threat zone).
|
||||||
join_distance: Distance
|
join_distance: Distance
|
||||||
split_distance: Distance
|
|
||||||
ingress_egress_distance: Distance
|
#: The distance between the ingress point (beginning of the attack) and target.
|
||||||
|
ingress_distance: Distance
|
||||||
|
|
||||||
ingress_altitude: Distance
|
ingress_altitude: Distance
|
||||||
egress_altitude: Distance
|
|
||||||
|
|
||||||
min_patrol_altitude: Distance
|
min_patrol_altitude: Distance
|
||||||
max_patrol_altitude: Distance
|
max_patrol_altitude: Distance
|
||||||
@ -65,6 +83,15 @@ class Doctrine:
|
|||||||
|
|
||||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||||
|
|
||||||
|
mission_ranges: MissionPlannerMaxRanges = field(default=MissionPlannerMaxRanges())
|
||||||
|
|
||||||
|
@has_save_compat_for(5)
|
||||||
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
|
if "ingress_distance" not in state:
|
||||||
|
state["ingress_distance"] = state["ingress_egress_distance"]
|
||||||
|
del state["ingress_egress_distance"]
|
||||||
|
self.__dict__.update(state)
|
||||||
|
|
||||||
|
|
||||||
MODERN_DOCTRINE = Doctrine(
|
MODERN_DOCTRINE = Doctrine(
|
||||||
cap=True,
|
cap=True,
|
||||||
@ -76,10 +103,8 @@ MODERN_DOCTRINE = Doctrine(
|
|||||||
hold_distance=nautical_miles(15),
|
hold_distance=nautical_miles(15),
|
||||||
push_distance=nautical_miles(20),
|
push_distance=nautical_miles(20),
|
||||||
join_distance=nautical_miles(20),
|
join_distance=nautical_miles(20),
|
||||||
split_distance=nautical_miles(20),
|
ingress_distance=nautical_miles(45),
|
||||||
ingress_egress_distance=nautical_miles(45),
|
|
||||||
ingress_altitude=feet(20000),
|
ingress_altitude=feet(20000),
|
||||||
egress_altitude=feet(20000),
|
|
||||||
min_patrol_altitude=feet(15000),
|
min_patrol_altitude=feet(15000),
|
||||||
max_patrol_altitude=feet(33000),
|
max_patrol_altitude=feet(33000),
|
||||||
pattern_altitude=feet(5000),
|
pattern_altitude=feet(5000),
|
||||||
@ -114,10 +139,8 @@ COLDWAR_DOCTRINE = Doctrine(
|
|||||||
hold_distance=nautical_miles(10),
|
hold_distance=nautical_miles(10),
|
||||||
push_distance=nautical_miles(10),
|
push_distance=nautical_miles(10),
|
||||||
join_distance=nautical_miles(10),
|
join_distance=nautical_miles(10),
|
||||||
split_distance=nautical_miles(10),
|
ingress_distance=nautical_miles(30),
|
||||||
ingress_egress_distance=nautical_miles(30),
|
|
||||||
ingress_altitude=feet(18000),
|
ingress_altitude=feet(18000),
|
||||||
egress_altitude=feet(18000),
|
|
||||||
min_patrol_altitude=feet(10000),
|
min_patrol_altitude=feet(10000),
|
||||||
max_patrol_altitude=feet(24000),
|
max_patrol_altitude=feet(24000),
|
||||||
pattern_altitude=feet(5000),
|
pattern_altitude=feet(5000),
|
||||||
@ -151,11 +174,9 @@ WWII_DOCTRINE = Doctrine(
|
|||||||
hold_distance=nautical_miles(5),
|
hold_distance=nautical_miles(5),
|
||||||
push_distance=nautical_miles(5),
|
push_distance=nautical_miles(5),
|
||||||
join_distance=nautical_miles(5),
|
join_distance=nautical_miles(5),
|
||||||
split_distance=nautical_miles(5),
|
|
||||||
rendezvous_altitude=feet(10000),
|
rendezvous_altitude=feet(10000),
|
||||||
ingress_egress_distance=nautical_miles(7),
|
ingress_distance=nautical_miles(7),
|
||||||
ingress_altitude=feet(8000),
|
ingress_altitude=feet(8000),
|
||||||
egress_altitude=feet(8000),
|
|
||||||
min_patrol_altitude=feet(4000),
|
min_patrol_altitude=feet(4000),
|
||||||
max_patrol_altitude=feet(15000),
|
max_patrol_altitude=feet(15000),
|
||||||
pattern_altitude=feet(5000),
|
pattern_altitude=feet(5000),
|
||||||
|
|||||||
1261
game/data/weapons.py
1261
game/data/weapons.py
File diff suppressed because it is too large
Load Diff
19
game/db.py
19
game/db.py
@ -29,8 +29,9 @@ from dcs.ships import (
|
|||||||
CV_1143_5,
|
CV_1143_5,
|
||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport
|
from dcs.terrain.terrain import Airport
|
||||||
|
from dcs.unit import Ship
|
||||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||||
from dcs.unittype import UnitType
|
from dcs.unittype import UnitType, FlyingType, ShipType, VehicleType
|
||||||
from dcs.vehicles import (
|
from dcs.vehicles import (
|
||||||
vehicle_map,
|
vehicle_map,
|
||||||
)
|
)
|
||||||
@ -255,7 +256,7 @@ Aircraft livery overrides. Syntax as follows:
|
|||||||
`Identifier` is aircraft identifier (as used troughout the file) and "LiveryName" (with double quotes)
|
`Identifier` is aircraft identifier (as used troughout the file) and "LiveryName" (with double quotes)
|
||||||
is livery name as found in mission editor.
|
is livery name as found in mission editor.
|
||||||
"""
|
"""
|
||||||
PLANE_LIVERY_OVERRIDES = {
|
PLANE_LIVERY_OVERRIDES: dict[Type[FlyingType], str] = {
|
||||||
FA_18C_hornet: "VFA-34", # default livery for the hornet is blue angels one
|
FA_18C_hornet: "VFA-34", # default livery for the hornet is blue angels one
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,7 +329,7 @@ REWARDS = {
|
|||||||
StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
|
StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
|
||||||
|
|
||||||
|
|
||||||
def upgrade_to_supercarrier(unit, name: str):
|
def upgrade_to_supercarrier(unit: Type[ShipType], name: str) -> Type[ShipType]:
|
||||||
if unit == Stennis:
|
if unit == Stennis:
|
||||||
if name == "CVN-71 Theodore Roosevelt":
|
if name == "CVN-71 Theodore Roosevelt":
|
||||||
return CVN_71
|
return CVN_71
|
||||||
@ -361,7 +362,15 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def country_id_from_name(name):
|
def vehicle_type_from_name(name: str) -> Type[VehicleType]:
|
||||||
|
return vehicle_map[name]
|
||||||
|
|
||||||
|
|
||||||
|
def ship_type_from_name(name: str) -> Type[ShipType]:
|
||||||
|
return ship_map[name]
|
||||||
|
|
||||||
|
|
||||||
|
def country_id_from_name(name: str) -> int:
|
||||||
for k, v in country_dict.items():
|
for k, v in country_dict.items():
|
||||||
if v.name == name:
|
if v.name == name:
|
||||||
return k
|
return k
|
||||||
@ -374,7 +383,7 @@ class DefaultLiveries:
|
|||||||
|
|
||||||
|
|
||||||
OH_58D.Liveries = DefaultLiveries
|
OH_58D.Liveries = DefaultLiveries
|
||||||
F_16C_50.Liveries = DefaultLiveries
|
F_16C_50.Liveries = DefaultLiveries # type: ignore
|
||||||
P_51D_30_NA.Liveries = DefaultLiveries
|
P_51D_30_NA.Liveries = DefaultLiveries
|
||||||
Ju_88A4.Liveries = DefaultLiveries
|
Ju_88A4.Liveries = DefaultLiveries
|
||||||
B_17G.Liveries = DefaultLiveries
|
B_17G.Liveries = DefaultLiveries
|
||||||
|
|||||||
@ -105,8 +105,9 @@ class PatrolConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Split into PlaneType and HelicopterType?
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AircraftType(UnitType[FlyingType]):
|
class AircraftType(UnitType[Type[FlyingType]]):
|
||||||
carrier_capable: bool
|
carrier_capable: bool
|
||||||
lha_capable: bool
|
lha_capable: bool
|
||||||
always_keeps_gun: bool
|
always_keeps_gun: bool
|
||||||
@ -144,12 +145,23 @@ class AircraftType(UnitType[FlyingType]):
|
|||||||
return kph(self.dcs_unit_type.max_speed)
|
return kph(self.dcs_unit_type.max_speed)
|
||||||
|
|
||||||
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
|
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
|
||||||
from gen.radios import ChannelInUseError, MHz
|
from gen.radios import ChannelInUseError, kHz
|
||||||
|
|
||||||
if self.intra_flight_radio is not None:
|
if self.intra_flight_radio is not None:
|
||||||
return radio_registry.alloc_for_radio(self.intra_flight_radio)
|
return radio_registry.alloc_for_radio(self.intra_flight_radio)
|
||||||
|
|
||||||
freq = MHz(self.dcs_unit_type.radio_frequency)
|
# The default radio frequency is set in megahertz. For some aircraft, it is a
|
||||||
|
# floating point value. For all current aircraft, adjusting to kilohertz will be
|
||||||
|
# sufficient to convert to an integer.
|
||||||
|
in_khz = float(self.dcs_unit_type.radio_frequency) * 1000
|
||||||
|
if not in_khz.is_integer():
|
||||||
|
logging.warning(
|
||||||
|
f"Found unexpected sub-kHz default radio for {self}: {in_khz} kHz. "
|
||||||
|
"Truncating to integer. The truncated frequency may not be valid for "
|
||||||
|
"the aircraft."
|
||||||
|
)
|
||||||
|
|
||||||
|
freq = kHz(int(in_khz))
|
||||||
try:
|
try:
|
||||||
radio_registry.reserve(freq)
|
radio_registry.reserve(freq)
|
||||||
except ChannelInUseError:
|
except ChannelInUseError:
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from game.dcs.unittype import UnitType
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GroundUnitType(UnitType[VehicleType]):
|
class GroundUnitType(UnitType[Type[VehicleType]]):
|
||||||
unit_class: Optional[GroundUnitClass]
|
unit_class: Optional[GroundUnitClass]
|
||||||
spawn_weight: int
|
spawn_weight: int
|
||||||
|
|
||||||
|
|||||||
@ -4,12 +4,12 @@ from typing import TypeVar, Generic, Type
|
|||||||
|
|
||||||
from dcs.unittype import UnitType as DcsUnitType
|
from dcs.unittype import UnitType as DcsUnitType
|
||||||
|
|
||||||
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
|
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class UnitType(Generic[DcsUnitTypeT]):
|
class UnitType(Generic[DcsUnitTypeT]):
|
||||||
dcs_unit_type: Type[DcsUnitTypeT]
|
dcs_unit_type: DcsUnitTypeT
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
year_introduced: str
|
year_introduced: str
|
||||||
|
|||||||
@ -15,9 +15,9 @@ from typing import (
|
|||||||
Iterator,
|
Iterator,
|
||||||
List,
|
List,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
from game import db
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.theater import Airfield, ControlPoint
|
from game.theater import Airfield, ControlPoint
|
||||||
@ -77,8 +77,8 @@ class GroundLosses:
|
|||||||
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||||
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||||
|
|
||||||
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
player_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||||
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
enemy_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||||
|
|
||||||
player_buildings: List[Building] = field(default_factory=list)
|
player_buildings: List[Building] = field(default_factory=list)
|
||||||
enemy_buildings: List[Building] = field(default_factory=list)
|
enemy_buildings: List[Building] = field(default_factory=list)
|
||||||
@ -104,8 +104,9 @@ class StateData:
|
|||||||
#: Names of vehicle (and ship) units that were killed during the mission.
|
#: Names of vehicle (and ship) units that were killed during the mission.
|
||||||
killed_ground_units: List[str]
|
killed_ground_units: List[str]
|
||||||
|
|
||||||
#: Names of static units that were destroyed during the mission.
|
#: List of descriptions of destroyed statics. Format of each element is a mapping of
|
||||||
destroyed_statics: List[str]
|
#: the coordinate type ("x", "y", "z", "type", "orientation") to the value.
|
||||||
|
destroyed_statics: List[dict[str, Union[float, str]]]
|
||||||
|
|
||||||
#: Mangled names of bases that were captured during the mission.
|
#: Mangled names of bases that were captured during the mission.
|
||||||
base_capture_events: List[str]
|
base_capture_events: List[str]
|
||||||
@ -134,10 +135,8 @@ class Debriefing:
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
|
|
||||||
self.player_country = game.player_country
|
self.player_country = game.blue.country_name
|
||||||
self.enemy_country = game.enemy_country
|
self.enemy_country = game.red.country_name
|
||||||
self.player_country_id = db.country_id_from_name(game.player_country)
|
|
||||||
self.enemy_country_id = db.country_id_from_name(game.enemy_country)
|
|
||||||
|
|
||||||
self.air_losses = self.dead_aircraft()
|
self.air_losses = self.dead_aircraft()
|
||||||
self.ground_losses = self.dead_ground_units()
|
self.ground_losses = self.dead_ground_units()
|
||||||
@ -164,7 +163,7 @@ class Debriefing:
|
|||||||
yield from self.ground_losses.enemy_airlifts
|
yield from self.ground_losses.enemy_airlifts
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
|
def ground_object_losses(self) -> Iterator[GroundObjectUnit[Any]]:
|
||||||
yield from self.ground_losses.player_ground_objects
|
yield from self.ground_losses.player_ground_objects
|
||||||
yield from self.ground_losses.enemy_ground_objects
|
yield from self.ground_losses.enemy_ground_objects
|
||||||
|
|
||||||
@ -370,13 +369,13 @@ class PollDebriefingFileThread(threading.Thread):
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
|
|
||||||
def stop(self):
|
def stop(self) -> None:
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
def stopped(self):
|
def stopped(self) -> bool:
|
||||||
return self._stop_event.is_set()
|
return self._stop_event.is_set()
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
if os.path.isfile("state.json"):
|
if os.path.isfile("state.json"):
|
||||||
last_modified = os.path.getmtime("state.json")
|
last_modified = os.path.getmtime("state.json")
|
||||||
else:
|
else:
|
||||||
@ -401,7 +400,7 @@ class PollDebriefingFileThread(threading.Thread):
|
|||||||
|
|
||||||
|
|
||||||
def wait_for_debriefing(
|
def wait_for_debriefing(
|
||||||
callback: Callable[[Debriefing], None], game: Game, unit_map
|
callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
|
||||||
) -> PollDebriefingFileThread:
|
) -> PollDebriefingFileThread:
|
||||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.theater import ConflictTheater
|
|
||||||
|
|
||||||
|
|
||||||
class AirWarEvent(Event):
|
class AirWarEvent(Event):
|
||||||
"""Event handler for the air battle"""
|
"""Event handler for the air battle"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "AirWar"
|
return "AirWar"
|
||||||
|
|||||||
@ -5,7 +5,6 @@ from typing import List, TYPE_CHECKING, Type
|
|||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.task import Task
|
from dcs.task import Task
|
||||||
from dcs.unittype import VehicleType
|
|
||||||
|
|
||||||
from game import persistency
|
from game import persistency
|
||||||
from game.debriefing import AirLosses, Debriefing
|
from game.debriefing import AirLosses, Debriefing
|
||||||
@ -38,13 +37,13 @@ class Event:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
game,
|
game: Game,
|
||||||
from_cp: ControlPoint,
|
from_cp: ControlPoint,
|
||||||
target_cp: ControlPoint,
|
target_cp: ControlPoint,
|
||||||
location: Point,
|
location: Point,
|
||||||
attacker_name: str,
|
attacker_name: str,
|
||||||
defender_name: str,
|
defender_name: str,
|
||||||
):
|
) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
self.from_cp = from_cp
|
self.from_cp = from_cp
|
||||||
self.to_cp = target_cp
|
self.to_cp = target_cp
|
||||||
@ -54,7 +53,7 @@ class Event:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_player_attacking(self) -> bool:
|
def is_player_attacking(self) -> bool:
|
||||||
return self.attacker_name == self.game.player_faction.name
|
return self.attacker_name == self.game.blue.faction.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tasks(self) -> List[Type[Task]]:
|
def tasks(self) -> List[Type[Task]]:
|
||||||
@ -115,10 +114,10 @@ class Event:
|
|||||||
|
|
||||||
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
|
def complete_aircraft_transfers(self, debriefing: Debriefing) -> None:
|
||||||
self._transfer_aircraft(
|
self._transfer_aircraft(
|
||||||
self.game.blue_ato, debriefing.air_losses, for_player=True
|
self.game.blue.ato, debriefing.air_losses, for_player=True
|
||||||
)
|
)
|
||||||
self._transfer_aircraft(
|
self._transfer_aircraft(
|
||||||
self.game.red_ato, debriefing.air_losses, for_player=False
|
self.game.red.ato, debriefing.air_losses, for_player=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||||
@ -155,8 +154,8 @@ class Event:
|
|||||||
pilot.record.missions_flown += 1
|
pilot.record.missions_flown += 1
|
||||||
|
|
||||||
def commit_pilot_experience(self) -> None:
|
def commit_pilot_experience(self) -> None:
|
||||||
self._commit_pilot_experience(self.game.blue_ato)
|
self._commit_pilot_experience(self.game.blue.ato)
|
||||||
self._commit_pilot_experience(self.game.red_ato)
|
self._commit_pilot_experience(self.game.red.ato)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
||||||
@ -220,10 +219,10 @@ class Event:
|
|||||||
for loss in debriefing.ground_object_losses:
|
for loss in debriefing.ground_object_losses:
|
||||||
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
||||||
if not hasattr(loss.group, "units_losts"):
|
if not hasattr(loss.group, "units_losts"):
|
||||||
loss.group.units_losts = []
|
loss.group.units_losts = [] # type: ignore
|
||||||
|
|
||||||
loss.group.units.remove(loss.unit)
|
loss.group.units.remove(loss.unit)
|
||||||
loss.group.units_losts.append(loss.unit)
|
loss.group.units_losts.append(loss.unit) # type: ignore
|
||||||
|
|
||||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||||
for loss in debriefing.building_losses:
|
for loss in debriefing.building_losses:
|
||||||
@ -265,7 +264,7 @@ class Event:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(f"Could not process base capture {captured}")
|
logging.exception(f"Could not process base capture {captured}")
|
||||||
|
|
||||||
def commit(self, debriefing: Debriefing):
|
def commit(self, debriefing: Debriefing) -> None:
|
||||||
logging.info("Committing mission results")
|
logging.info("Committing mission results")
|
||||||
|
|
||||||
self.commit_air_losses(debriefing)
|
self.commit_air_losses(debriefing)
|
||||||
|
|||||||
@ -8,5 +8,5 @@ class FrontlineAttackEvent(Event):
|
|||||||
future unique Event handling
|
future unique Event handling
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "Frontline attack"
|
return "Frontline attack"
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional, Dict, Type, List, Any, Iterator
|
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
|
||||||
|
|
||||||
import dcs
|
import dcs
|
||||||
from dcs.countries import country_dict
|
from dcs.countries import country_dict
|
||||||
@ -25,6 +25,9 @@ from game.data.groundunitclass import GroundUnitClass
|
|||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.theater.start_generator import ModSettings
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Faction:
|
class Faction:
|
||||||
@ -81,10 +84,10 @@ class Faction:
|
|||||||
requirements: Dict[str, str] = field(default_factory=dict)
|
requirements: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
# possible aircraft carrier units
|
# possible aircraft carrier units
|
||||||
aircraft_carrier: List[Type[UnitType]] = field(default_factory=list)
|
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list)
|
||||||
|
|
||||||
# possible helicopter carrier units
|
# possible helicopter carrier units
|
||||||
helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
|
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list)
|
||||||
|
|
||||||
# Possible carrier names
|
# Possible carrier names
|
||||||
carrier_names: List[str] = field(default_factory=list)
|
carrier_names: List[str] = field(default_factory=list)
|
||||||
@ -257,7 +260,7 @@ class Faction:
|
|||||||
if unit.unit_class is unit_class:
|
if unit.unit_class is unit_class:
|
||||||
yield unit
|
yield unit
|
||||||
|
|
||||||
def apply_mod_settings(self, mod_settings) -> Faction:
|
def apply_mod_settings(self, mod_settings: ModSettings) -> Faction:
|
||||||
# aircraft
|
# aircraft
|
||||||
if not mod_settings.a4_skyhawk:
|
if not mod_settings.a4_skyhawk:
|
||||||
self.remove_aircraft("A-4E-C")
|
self.remove_aircraft("A-4E-C")
|
||||||
@ -319,17 +322,17 @@ class Faction:
|
|||||||
self.remove_air_defenses("KS19Generator")
|
self.remove_air_defenses("KS19Generator")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def remove_aircraft(self, name):
|
def remove_aircraft(self, name: str) -> None:
|
||||||
for i in self.aircrafts:
|
for i in self.aircrafts:
|
||||||
if i.dcs_unit_type.id == name:
|
if i.dcs_unit_type.id == name:
|
||||||
self.aircrafts.remove(i)
|
self.aircrafts.remove(i)
|
||||||
|
|
||||||
def remove_air_defenses(self, name):
|
def remove_air_defenses(self, name: str) -> None:
|
||||||
for i in self.air_defenses:
|
for i in self.air_defenses:
|
||||||
if i == name:
|
if i == name:
|
||||||
self.air_defenses.remove(i)
|
self.air_defenses.remove(i)
|
||||||
|
|
||||||
def remove_vehicle(self, name):
|
def remove_vehicle(self, name: str) -> None:
|
||||||
for i in self.frontline_units:
|
for i in self.frontline_units:
|
||||||
if i.dcs_unit_type.id == name:
|
if i.dcs_unit_type.id == name:
|
||||||
self.frontline_units.remove(i)
|
self.frontline_units.remove(i)
|
||||||
@ -342,7 +345,7 @@ def load_ship(name: str) -> Optional[Type[ShipType]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
def load_all_ships(data: list[str]) -> List[Type[ShipType]]:
|
||||||
items = []
|
items = []
|
||||||
for name in data:
|
for name in data:
|
||||||
item = load_ship(name)
|
item = load_ship(name)
|
||||||
|
|||||||
413
game/game.py
413
game/game.py
@ -1,47 +1,41 @@
|
|||||||
from game.dcs.aircrafttype import AircraftType
|
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import random
|
import math
|
||||||
import sys
|
from collections import Iterator
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, List
|
from typing import Any, List, Type, Union, cast
|
||||||
|
|
||||||
from dcs.action import Coalition
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.task import CAP, CAS, PinpointStrike
|
from dcs.task import CAP, CAS, PinpointStrike
|
||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
from game import db
|
|
||||||
from game.inventory import GlobalAircraftInventory
|
from game.inventory import GlobalAircraftInventory
|
||||||
from game.models.game_stats import GameStats
|
from game.models.game_stats import GameStats
|
||||||
from game.plugins import LuaPluginManager
|
from game.plugins import LuaPluginManager
|
||||||
from gen import aircraft, naming
|
from gen import naming
|
||||||
from gen.ato import AirTaskingOrder
|
from gen.ato import AirTaskingOrder
|
||||||
from gen.conflictgen import Conflict
|
from gen.conflictgen import Conflict
|
||||||
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
|
|
||||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
from gen.ground_forces.ai_ground_planner import GroundPlanner
|
||||||
from . import persistency
|
from . import persistency
|
||||||
|
from .coalition import Coalition
|
||||||
from .debriefing import Debriefing
|
from .debriefing import Debriefing
|
||||||
from .event.event import Event
|
from .event.event import Event
|
||||||
from .event.frontlineattack import FrontlineAttackEvent
|
from .event.frontlineattack import FrontlineAttackEvent
|
||||||
from .factions.faction import Faction
|
from .factions.faction import Faction
|
||||||
from .income import Income
|
|
||||||
from .infos.information import Information
|
from .infos.information import Information
|
||||||
from .navmesh import NavMesh
|
from .navmesh import NavMesh
|
||||||
from .procurement import AircraftProcurementRequest, ProcurementAi
|
from .procurement import AircraftProcurementRequest
|
||||||
from .profiling import logged_duration
|
from .profiling import logged_duration
|
||||||
from .settings import Settings, AutoAtoBehavior
|
from .settings import Settings
|
||||||
from .squadrons import AirWing
|
from .squadrons import AirWing
|
||||||
from .theater import ConflictTheater
|
from .theater import ConflictTheater, ControlPoint
|
||||||
from .theater.bullseye import Bullseye
|
from .theater.bullseye import Bullseye
|
||||||
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||||
from .threatzones import ThreatZones
|
from .threatzones import ThreatZones
|
||||||
from .transfers import PendingTransfers
|
|
||||||
from .unitmap import UnitMap
|
from .unitmap import UnitMap
|
||||||
from .weather import Conditions, TimeOfDay
|
from .weather import Conditions, TimeOfDay
|
||||||
|
|
||||||
@ -99,156 +93,106 @@ class Game:
|
|||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.events: List[Event] = []
|
self.events: List[Event] = []
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
self.player_faction = player_faction
|
|
||||||
self.player_country = player_faction.country
|
|
||||||
self.enemy_faction = enemy_faction
|
|
||||||
self.enemy_country = enemy_faction.country
|
|
||||||
# pass_turn() will be called when initialization is complete which will
|
# pass_turn() will be called when initialization is complete which will
|
||||||
# increment this to turn 0 before it reaches the player.
|
# increment this to turn 0 before it reaches the player.
|
||||||
self.turn = -1
|
self.turn = -1
|
||||||
# NB: This is the *start* date. It is never updated.
|
# NB: This is the *start* date. It is never updated.
|
||||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||||
self.game_stats = GameStats()
|
self.game_stats = GameStats()
|
||||||
self.game_stats.update(self)
|
|
||||||
self.notes = ""
|
self.notes = ""
|
||||||
self.ground_planners: dict[int, GroundPlanner] = {}
|
self.ground_planners: dict[int, GroundPlanner] = {}
|
||||||
self.informations = []
|
self.informations = []
|
||||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||||
self.__culling_zones: List[Point] = []
|
self.__culling_zones: List[Point] = []
|
||||||
self.__destroyed_units: List[str] = []
|
self.__destroyed_units: list[dict[str, Union[float, str]]] = []
|
||||||
self.savepath = ""
|
self.savepath = ""
|
||||||
self.budget = player_budget
|
|
||||||
self.enemy_budget = enemy_budget
|
|
||||||
self.current_unit_id = 0
|
self.current_unit_id = 0
|
||||||
self.current_group_id = 0
|
self.current_group_id = 0
|
||||||
self.name_generator = naming.namegen
|
self.name_generator = naming.namegen
|
||||||
|
|
||||||
self.conditions = self.generate_conditions()
|
self.conditions = self.generate_conditions()
|
||||||
|
|
||||||
self.blue_transit_network = TransitNetwork()
|
self.sanitize_sides(player_faction, enemy_faction)
|
||||||
self.red_transit_network = TransitNetwork()
|
self.blue = Coalition(self, player_faction, player_budget, player=True)
|
||||||
|
self.red = Coalition(self, enemy_faction, enemy_budget, player=False)
|
||||||
self.blue_procurement_requests: List[AircraftProcurementRequest] = []
|
self.blue.set_opponent(self.red)
|
||||||
self.red_procurement_requests: List[AircraftProcurementRequest] = []
|
self.red.set_opponent(self.blue)
|
||||||
|
|
||||||
self.blue_ato = AirTaskingOrder()
|
|
||||||
self.red_ato = AirTaskingOrder()
|
|
||||||
|
|
||||||
self.blue_bullseye = Bullseye(Point(0, 0))
|
|
||||||
self.red_bullseye = Bullseye(Point(0, 0))
|
|
||||||
|
|
||||||
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
|
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
|
||||||
|
|
||||||
self.transfers = PendingTransfers(self)
|
|
||||||
|
|
||||||
self.sanitize_sides()
|
|
||||||
|
|
||||||
self.blue_faker = Faker(self.player_faction.locales)
|
|
||||||
self.red_faker = Faker(self.enemy_faction.locales)
|
|
||||||
|
|
||||||
self.blue_air_wing = AirWing(self, player=True)
|
|
||||||
self.red_air_wing = AirWing(self, player=False)
|
|
||||||
|
|
||||||
self.on_load(game_still_initializing=True)
|
self.on_load(game_still_initializing=True)
|
||||||
|
|
||||||
def __getstate__(self) -> dict[str, Any]:
|
|
||||||
state = self.__dict__.copy()
|
|
||||||
# Avoid persisting any volatile types that can be deterministically
|
|
||||||
# recomputed on load for the sake of save compatibility.
|
|
||||||
del state["blue_threat_zone"]
|
|
||||||
del state["red_threat_zone"]
|
|
||||||
del state["blue_navmesh"]
|
|
||||||
del state["red_navmesh"]
|
|
||||||
del state["blue_faker"]
|
|
||||||
del state["red_faker"]
|
|
||||||
return state
|
|
||||||
|
|
||||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
# Regenerate any state that was not persisted.
|
# Regenerate any state that was not persisted.
|
||||||
self.on_load()
|
self.on_load()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def coalitions(self) -> Iterator[Coalition]:
|
||||||
|
yield self.blue
|
||||||
|
yield self.red
|
||||||
|
|
||||||
def ato_for(self, player: bool) -> AirTaskingOrder:
|
def ato_for(self, player: bool) -> AirTaskingOrder:
|
||||||
if player:
|
return self.coalition_for(player).ato
|
||||||
return self.blue_ato
|
|
||||||
return self.red_ato
|
|
||||||
|
|
||||||
def procurement_requests_for(
|
def procurement_requests_for(
|
||||||
self, player: bool
|
self, player: bool
|
||||||
) -> List[AircraftProcurementRequest]:
|
) -> list[AircraftProcurementRequest]:
|
||||||
if player:
|
return self.coalition_for(player).procurement_requests
|
||||||
return self.blue_procurement_requests
|
|
||||||
return self.red_procurement_requests
|
|
||||||
|
|
||||||
def transit_network_for(self, player: bool) -> TransitNetwork:
|
def transit_network_for(self, player: bool) -> TransitNetwork:
|
||||||
if player:
|
return self.coalition_for(player).transit_network
|
||||||
return self.blue_transit_network
|
|
||||||
return self.red_transit_network
|
|
||||||
|
|
||||||
def generate_conditions(self) -> Conditions:
|
def generate_conditions(self) -> Conditions:
|
||||||
return Conditions.generate(
|
return Conditions.generate(
|
||||||
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
|
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
|
||||||
)
|
)
|
||||||
|
|
||||||
def sanitize_sides(self):
|
@staticmethod
|
||||||
|
def sanitize_sides(player_faction: Faction, enemy_faction: Faction) -> None:
|
||||||
"""
|
"""
|
||||||
Make sure the opposing factions are using different countries
|
Make sure the opposing factions are using different countries
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
if self.player_country == self.enemy_country:
|
if player_faction.country == enemy_faction.country:
|
||||||
if self.player_country == "USA":
|
if player_faction.country == "USA":
|
||||||
self.enemy_country = "USAF Aggressors"
|
enemy_faction.country = "USAF Aggressors"
|
||||||
elif self.player_country == "Russia":
|
elif player_faction.country == "Russia":
|
||||||
self.enemy_country = "USSR"
|
enemy_faction.country = "USSR"
|
||||||
else:
|
else:
|
||||||
self.enemy_country = "Russia"
|
enemy_faction.country = "Russia"
|
||||||
|
|
||||||
def faction_for(self, player: bool) -> Faction:
|
def faction_for(self, player: bool) -> Faction:
|
||||||
if player:
|
return self.coalition_for(player).faction
|
||||||
return self.player_faction
|
|
||||||
return self.enemy_faction
|
|
||||||
|
|
||||||
def faker_for(self, player: bool) -> Faker:
|
def faker_for(self, player: bool) -> Faker:
|
||||||
if player:
|
return self.coalition_for(player).faker
|
||||||
return self.blue_faker
|
|
||||||
return self.red_faker
|
|
||||||
|
|
||||||
def air_wing_for(self, player: bool) -> AirWing:
|
def air_wing_for(self, player: bool) -> AirWing:
|
||||||
if player:
|
return self.coalition_for(player).air_wing
|
||||||
return self.blue_air_wing
|
|
||||||
return self.red_air_wing
|
|
||||||
|
|
||||||
def country_for(self, player: bool) -> str:
|
def country_for(self, player: bool) -> str:
|
||||||
if player:
|
return self.coalition_for(player).country_name
|
||||||
return self.player_country
|
|
||||||
return self.enemy_country
|
|
||||||
|
|
||||||
def bullseye_for(self, player: bool) -> Bullseye:
|
def bullseye_for(self, player: bool) -> Bullseye:
|
||||||
if player:
|
return self.coalition_for(player).bullseye
|
||||||
return self.blue_bullseye
|
|
||||||
return self.red_bullseye
|
|
||||||
|
|
||||||
def _roll(self, prob, mult):
|
def _generate_player_event(
|
||||||
if self.settings.version == "dev":
|
self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint
|
||||||
# always generate all events for dev
|
) -> None:
|
||||||
return 100
|
|
||||||
else:
|
|
||||||
return random.randint(1, 100) <= prob * mult
|
|
||||||
|
|
||||||
def _generate_player_event(self, event_class, player_cp, enemy_cp):
|
|
||||||
self.events.append(
|
self.events.append(
|
||||||
event_class(
|
event_class(
|
||||||
self,
|
self,
|
||||||
player_cp,
|
player_cp,
|
||||||
enemy_cp,
|
enemy_cp,
|
||||||
enemy_cp.position,
|
enemy_cp.position,
|
||||||
self.player_faction.name,
|
self.blue.faction.name,
|
||||||
self.enemy_faction.name,
|
self.red.faction.name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _generate_events(self):
|
def _generate_events(self) -> None:
|
||||||
for front_line in self.theater.conflicts():
|
for front_line in self.theater.conflicts():
|
||||||
self._generate_player_event(
|
self._generate_player_event(
|
||||||
FrontlineAttackEvent,
|
FrontlineAttackEvent,
|
||||||
@ -256,27 +200,21 @@ class Game:
|
|||||||
front_line.red_cp,
|
front_line.red_cp,
|
||||||
)
|
)
|
||||||
|
|
||||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
def coalition_for(self, player: bool) -> Coalition:
|
||||||
if player:
|
if player:
|
||||||
self.budget += amount
|
return self.blue
|
||||||
else:
|
return self.red
|
||||||
self.enemy_budget += amount
|
|
||||||
|
|
||||||
def process_player_income(self):
|
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||||
self.budget += Income(self, player=True).total
|
self.coalition_for(player).adjust_budget(amount)
|
||||||
|
|
||||||
def process_enemy_income(self):
|
@staticmethod
|
||||||
# TODO: Clean up save compat.
|
def initiate_event(event: Event) -> UnitMap:
|
||||||
if not hasattr(self, "enemy_budget"):
|
|
||||||
self.enemy_budget = 0
|
|
||||||
self.enemy_budget += Income(self, player=False).total
|
|
||||||
|
|
||||||
def initiate_event(self, event: Event) -> UnitMap:
|
|
||||||
# assert event in self.events
|
# assert event in self.events
|
||||||
logging.info("Generating {} (regular)".format(event))
|
logging.info("Generating {} (regular)".format(event))
|
||||||
return event.generate()
|
return event.generate()
|
||||||
|
|
||||||
def finish_event(self, event: Event, debriefing: Debriefing):
|
def finish_event(self, event: Event, debriefing: Debriefing) -> None:
|
||||||
logging.info("Finishing event {}".format(event))
|
logging.info("Finishing event {}".format(event))
|
||||||
event.commit(debriefing)
|
event.commit(debriefing)
|
||||||
|
|
||||||
@ -285,16 +223,6 @@ class Game:
|
|||||||
else:
|
else:
|
||||||
logging.info("finish_event: event not in the events!")
|
logging.info("finish_event: event not in the events!")
|
||||||
|
|
||||||
def is_player_attack(self, event):
|
|
||||||
if isinstance(event, Event):
|
|
||||||
return (
|
|
||||||
event
|
|
||||||
and event.attacker_name
|
|
||||||
and event.attacker_name == self.player_faction.name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
|
||||||
|
|
||||||
def on_load(self, game_still_initializing: bool = False) -> None:
|
def on_load(self, game_still_initializing: bool = False) -> None:
|
||||||
if not hasattr(self, "name_generator"):
|
if not hasattr(self, "name_generator"):
|
||||||
self.name_generator = naming.namegen
|
self.name_generator = naming.namegen
|
||||||
@ -309,36 +237,50 @@ class Game:
|
|||||||
self.compute_conflicts_position()
|
self.compute_conflicts_position()
|
||||||
if not game_still_initializing:
|
if not game_still_initializing:
|
||||||
self.compute_threat_zones()
|
self.compute_threat_zones()
|
||||||
self.blue_faker = Faker(self.faction_for(player=True).locales)
|
|
||||||
self.red_faker = Faker(self.faction_for(player=False).locales)
|
|
||||||
|
|
||||||
def reset_ato(self) -> None:
|
|
||||||
self.blue_ato.clear()
|
|
||||||
self.red_ato.clear()
|
|
||||||
|
|
||||||
def finish_turn(self, skipped: bool = False) -> None:
|
def finish_turn(self, skipped: bool = False) -> None:
|
||||||
|
"""Finalizes the current turn and advances to the next turn.
|
||||||
|
|
||||||
|
This handles the turn-end portion of passing a turn. Initialization of the next
|
||||||
|
turn is handled by `initialize_turn`. These are separate processes because while
|
||||||
|
turns may be initialized more than once under some circumstances (see the
|
||||||
|
documentation for `initialize_turn`), `finish_turn` performs the work that
|
||||||
|
should be guaranteed to happen only once per turn:
|
||||||
|
|
||||||
|
* Turn counter increment.
|
||||||
|
* Delivering units ordered the previous turn.
|
||||||
|
* Transfer progress.
|
||||||
|
* Squadron replenishment.
|
||||||
|
* Income distribution.
|
||||||
|
* Base strength (front line position) adjustment.
|
||||||
|
* Weather/time-of-day generation.
|
||||||
|
|
||||||
|
Some actions (like transit network assembly) will happen both here and in
|
||||||
|
`initialize_turn`. We need the network to be up to date so we can account for
|
||||||
|
base captures when processing the transfers that occurred last turn, but we also
|
||||||
|
need it to be up to date in the case of a re-initialization in `initialize_turn`
|
||||||
|
(such as to account for a cheat base capture) so that orders are only placed
|
||||||
|
where a supply route exists to the destination. This is a relatively cheap
|
||||||
|
operation so duplicating the effort is not a problem.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skipped: True if the turn was skipped.
|
||||||
|
"""
|
||||||
self.informations.append(
|
self.informations.append(
|
||||||
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
||||||
)
|
)
|
||||||
self.turn += 1
|
self.turn += 1
|
||||||
|
|
||||||
# Need to recompute before transfers and deliveries to account for captures.
|
# The coalition-specific turn finalization *must* happen before unit deliveries,
|
||||||
# This happens in in initialize_turn as well, because cheating doesn't advance a
|
# since the coalition-specific finalization handles transit network updates and
|
||||||
# turn but can capture bases so we need to recompute there as well.
|
# transfer processing. If in the other order, units may be delivered to captured
|
||||||
self.compute_transit_networks()
|
# bases, and freshly delivered units will spawn one leg through their journey.
|
||||||
|
self.blue.end_turn()
|
||||||
|
self.red.end_turn()
|
||||||
|
|
||||||
# Must happen *before* unit deliveries are handled, or else new units will spawn
|
|
||||||
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
|
|
||||||
self.transfers.perform_transfers()
|
|
||||||
|
|
||||||
# Needs to happen *before* planning transfers so we don't cancel them.
|
|
||||||
self.reset_ato()
|
|
||||||
for control_point in self.theater.controlpoints:
|
for control_point in self.theater.controlpoints:
|
||||||
control_point.process_turn(self)
|
control_point.process_turn(self)
|
||||||
|
|
||||||
self.blue_air_wing.replenish()
|
|
||||||
self.red_air_wing.replenish()
|
|
||||||
|
|
||||||
if not skipped:
|
if not skipped:
|
||||||
for cp in self.theater.player_points():
|
for cp in self.theater.player_points():
|
||||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||||
@ -349,14 +291,19 @@ class Game:
|
|||||||
|
|
||||||
self.conditions = self.generate_conditions()
|
self.conditions = self.generate_conditions()
|
||||||
|
|
||||||
self.process_enemy_income()
|
|
||||||
self.process_player_income()
|
|
||||||
|
|
||||||
def begin_turn_0(self) -> None:
|
def begin_turn_0(self) -> None:
|
||||||
|
"""Initialization for the first turn of the game."""
|
||||||
self.turn = 0
|
self.turn = 0
|
||||||
self.initialize_turn()
|
self.initialize_turn()
|
||||||
|
|
||||||
def pass_turn(self, no_action: bool = False) -> None:
|
def pass_turn(self, no_action: bool = False) -> None:
|
||||||
|
"""Ends the current turn and initializes the new turn.
|
||||||
|
|
||||||
|
Called both when skipping a turn or by ending the turn as the result of combat.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
no_action: True if the turn was skipped.
|
||||||
|
"""
|
||||||
logging.info("Pass turn")
|
logging.info("Pass turn")
|
||||||
with logged_duration("Turn finalization"):
|
with logged_duration("Turn finalization"):
|
||||||
self.finish_turn(no_action)
|
self.finish_turn(no_action)
|
||||||
@ -366,7 +313,7 @@ class Game:
|
|||||||
# Autosave progress
|
# Autosave progress
|
||||||
persistency.autosave(self)
|
persistency.autosave(self)
|
||||||
|
|
||||||
def check_win_loss(self):
|
def check_win_loss(self) -> TurnState:
|
||||||
player_airbases = {
|
player_airbases = {
|
||||||
cp for cp in self.theater.player_points() if cp.runway_is_operational()
|
cp for cp in self.theater.player_points() if cp.runway_is_operational()
|
||||||
}
|
}
|
||||||
@ -383,24 +330,50 @@ class Game:
|
|||||||
|
|
||||||
def set_bullseye(self) -> None:
|
def set_bullseye(self) -> None:
|
||||||
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
|
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
|
||||||
self.blue_bullseye = Bullseye(enemy_cp.position)
|
self.blue.bullseye = Bullseye(enemy_cp.position)
|
||||||
self.red_bullseye = Bullseye(player_cp.position)
|
self.red.bullseye = Bullseye(player_cp.position)
|
||||||
|
|
||||||
def initialize_turn(self) -> None:
|
def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None:
|
||||||
|
"""Performs turn initialization for the specified players.
|
||||||
|
|
||||||
|
Turn initialization performs all of the beginning-of-turn actions. *End-of-turn*
|
||||||
|
processing happens in `pass_turn` (despite the name, it's called both for
|
||||||
|
skipping the turn and ending the turn after combat).
|
||||||
|
|
||||||
|
Special care needs to be taken here because initialization can occur more than
|
||||||
|
once per turn. A number of events can require re-initializing a turn:
|
||||||
|
|
||||||
|
* Cheat capture. Bases changing hands invalidates many missions in both ATOs,
|
||||||
|
purchase orders, threat zones, transit networks, etc. Practically speaking,
|
||||||
|
after a base capture the turn needs to be treated as fully new. The game might
|
||||||
|
even be over after a capture.
|
||||||
|
* Cheat front line position. CAS missions are no longer in the correct location,
|
||||||
|
and the ground planner may also need changes.
|
||||||
|
* Selling/buying units at TGOs. Selling a TGO might leave missions in the ATO
|
||||||
|
with invalid targets. Buying a new SAM (or even replacing some units in a SAM)
|
||||||
|
potentially changes the threat zone and may alter mission priorities and
|
||||||
|
flight planning.
|
||||||
|
|
||||||
|
Most of the work is delegated to initialize_turn_for, which handles the
|
||||||
|
coalition-specific turn initialization. In some cases only one coalition will be
|
||||||
|
(re-) initialized. This is the case when buying or selling TGO units, since we
|
||||||
|
don't want to force the player to redo all their planning just because they
|
||||||
|
repaired a SAM, but should replan opfor when that happens. On the other hand,
|
||||||
|
base captures are significant enough (and likely enough to be the first thing
|
||||||
|
the player does in a turn) that we replan blue as well. Front lines are less
|
||||||
|
impactful but also likely to be early, so they also cause a blue replan.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
for_red: True if opfor should be re-initialized.
|
||||||
|
for_blue: True if the player coalition should be re-initialized.
|
||||||
|
"""
|
||||||
self.events = []
|
self.events = []
|
||||||
self._generate_events()
|
self._generate_events()
|
||||||
|
|
||||||
self.set_bullseye()
|
self.set_bullseye()
|
||||||
|
|
||||||
# Update statistics
|
# Update statistics
|
||||||
self.game_stats.update(self)
|
self.game_stats.update(self)
|
||||||
|
|
||||||
self.blue_air_wing.reset()
|
|
||||||
self.red_air_wing.reset()
|
|
||||||
self.aircraft_inventory.reset()
|
|
||||||
for cp in self.theater.controlpoints:
|
|
||||||
self.aircraft_inventory.set_from_control_point(cp)
|
|
||||||
|
|
||||||
# Check for win or loss condition
|
# Check for win or loss condition
|
||||||
turn_state = self.check_win_loss()
|
turn_state = self.check_win_loss()
|
||||||
if turn_state in (TurnState.LOSS, TurnState.WIN):
|
if turn_state in (TurnState.LOSS, TurnState.WIN):
|
||||||
@ -411,59 +384,26 @@ class Game:
|
|||||||
self.compute_conflicts_position()
|
self.compute_conflicts_position()
|
||||||
with logged_duration("Threat zone computation"):
|
with logged_duration("Threat zone computation"):
|
||||||
self.compute_threat_zones()
|
self.compute_threat_zones()
|
||||||
with logged_duration("Transit network identification"):
|
|
||||||
self.compute_transit_networks()
|
# Plan Coalition specific turn
|
||||||
|
if for_blue:
|
||||||
|
self.initialize_turn_for(player=True)
|
||||||
|
if for_red:
|
||||||
|
self.initialize_turn_for(player=False)
|
||||||
|
|
||||||
|
# Plan GroundWar
|
||||||
self.ground_planners = {}
|
self.ground_planners = {}
|
||||||
|
|
||||||
self.blue_procurement_requests.clear()
|
|
||||||
self.red_procurement_requests.clear()
|
|
||||||
|
|
||||||
with logged_duration("Procurement of airlift assets"):
|
|
||||||
self.transfers.order_airlift_assets()
|
|
||||||
with logged_duration("Transport planning"):
|
|
||||||
self.transfers.plan_transports()
|
|
||||||
|
|
||||||
with logged_duration("Blue mission planning"):
|
|
||||||
if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
|
|
||||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
|
||||||
blue_planner.plan_missions()
|
|
||||||
|
|
||||||
with logged_duration("Red mission planning"):
|
|
||||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
|
||||||
red_planner.plan_missions()
|
|
||||||
|
|
||||||
for cp in self.theater.controlpoints:
|
for cp in self.theater.controlpoints:
|
||||||
if cp.has_frontline:
|
if cp.has_frontline:
|
||||||
gplanner = GroundPlanner(cp, self)
|
gplanner = GroundPlanner(cp, self)
|
||||||
gplanner.plan_groundwar()
|
gplanner.plan_groundwar()
|
||||||
self.ground_planners[cp.id] = gplanner
|
self.ground_planners[cp.id] = gplanner
|
||||||
|
|
||||||
self.plan_procurement()
|
def initialize_turn_for(self, player: bool) -> None:
|
||||||
|
self.aircraft_inventory.reset(player)
|
||||||
def plan_procurement(self) -> None:
|
for cp in self.theater.control_points_for(player):
|
||||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
self.aircraft_inventory.set_from_control_point(cp)
|
||||||
# gets much more of the budget that turn. Otherwise budget (after
|
self.coalition_for(player).initialize_turn()
|
||||||
# repairs) is split evenly between air and ground. For the default
|
|
||||||
# starting budget of 2000 this gives 600 to ground forces and 1400 to
|
|
||||||
# aircraft. After that the budget will be spend proportionally based on how much is already invested
|
|
||||||
|
|
||||||
self.budget = ProcurementAi(
|
|
||||||
self,
|
|
||||||
for_player=True,
|
|
||||||
faction=self.player_faction,
|
|
||||||
manage_runways=self.settings.automate_runway_repair,
|
|
||||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
|
||||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
|
||||||
).spend_budget(self.budget)
|
|
||||||
|
|
||||||
self.enemy_budget = ProcurementAi(
|
|
||||||
self,
|
|
||||||
for_player=False,
|
|
||||||
faction=self.enemy_faction,
|
|
||||||
manage_runways=True,
|
|
||||||
manage_front_line=True,
|
|
||||||
manage_aircraft=True,
|
|
||||||
).spend_budget(self.enemy_budget)
|
|
||||||
|
|
||||||
def message(self, text: str) -> None:
|
def message(self, text: str) -> None:
|
||||||
self.informations.append(Information(text, turn=self.turn))
|
self.informations.append(Information(text, turn=self.turn))
|
||||||
@ -476,7 +416,7 @@ class Game:
|
|||||||
def current_day(self) -> date:
|
def current_day(self) -> date:
|
||||||
return self.date + timedelta(days=self.turn // 4)
|
return self.date + timedelta(days=self.turn // 4)
|
||||||
|
|
||||||
def next_unit_id(self):
|
def next_unit_id(self) -> int:
|
||||||
"""
|
"""
|
||||||
Next unit id for pre-generated units
|
Next unit id for pre-generated units
|
||||||
"""
|
"""
|
||||||
@ -490,34 +430,22 @@ class Game:
|
|||||||
self.current_group_id += 1
|
self.current_group_id += 1
|
||||||
return self.current_group_id
|
return self.current_group_id
|
||||||
|
|
||||||
def compute_transit_networks(self) -> None:
|
|
||||||
self.blue_transit_network = self.compute_transit_network_for(player=True)
|
|
||||||
self.red_transit_network = self.compute_transit_network_for(player=False)
|
|
||||||
|
|
||||||
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
|
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
|
||||||
return TransitNetworkBuilder(self.theater, player).build()
|
return TransitNetworkBuilder(self.theater, player).build()
|
||||||
|
|
||||||
def compute_threat_zones(self) -> None:
|
def compute_threat_zones(self) -> None:
|
||||||
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
|
self.blue.compute_threat_zones()
|
||||||
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
|
self.red.compute_threat_zones()
|
||||||
self.blue_navmesh = NavMesh.from_threat_zones(
|
self.blue.compute_nav_meshes()
|
||||||
self.red_threat_zone, self.theater
|
self.red.compute_nav_meshes()
|
||||||
)
|
|
||||||
self.red_navmesh = NavMesh.from_threat_zones(
|
|
||||||
self.blue_threat_zone, self.theater
|
|
||||||
)
|
|
||||||
|
|
||||||
def threat_zone_for(self, player: bool) -> ThreatZones:
|
def threat_zone_for(self, player: bool) -> ThreatZones:
|
||||||
if player:
|
return self.coalition_for(player).threat_zone
|
||||||
return self.blue_threat_zone
|
|
||||||
return self.red_threat_zone
|
|
||||||
|
|
||||||
def navmesh_for(self, player: bool) -> NavMesh:
|
def navmesh_for(self, player: bool) -> NavMesh:
|
||||||
if player:
|
return self.coalition_for(player).nav_mesh
|
||||||
return self.blue_navmesh
|
|
||||||
return self.red_navmesh
|
|
||||||
|
|
||||||
def compute_conflicts_position(self):
|
def compute_conflicts_position(self) -> None:
|
||||||
"""
|
"""
|
||||||
Compute the current conflict center position(s), mainly used for culling calculation
|
Compute the current conflict center position(s), mainly used for culling calculation
|
||||||
:return: List of points of interests
|
:return: List of points of interests
|
||||||
@ -540,7 +468,7 @@ class Game:
|
|||||||
# If there is no conflict take the center point between the two nearest opposing bases
|
# If there is no conflict take the center point between the two nearest opposing bases
|
||||||
if len(zones) == 0:
|
if len(zones) == 0:
|
||||||
cpoint = None
|
cpoint = None
|
||||||
min_distance = sys.maxsize
|
min_distance = math.inf
|
||||||
for cp in self.theater.player_points():
|
for cp in self.theater.player_points():
|
||||||
for cp2 in self.theater.enemy_points():
|
for cp2 in self.theater.enemy_points():
|
||||||
d = cp.position.distance_to_point(cp2.position)
|
d = cp.position.distance_to_point(cp2.position)
|
||||||
@ -558,7 +486,7 @@ class Game:
|
|||||||
if cpoint is not None:
|
if cpoint is not None:
|
||||||
zones.append(cpoint)
|
zones.append(cpoint)
|
||||||
|
|
||||||
packages = itertools.chain(self.blue_ato.packages, self.red_ato.packages)
|
packages = itertools.chain(self.blue.ato.packages, self.red.ato.packages)
|
||||||
for package in packages:
|
for package in packages:
|
||||||
if package.primary_task is FlightType.BARCAP:
|
if package.primary_task is FlightType.BARCAP:
|
||||||
# BARCAPs will be planned at most locations on smaller theaters,
|
# BARCAPs will be planned at most locations on smaller theaters,
|
||||||
@ -576,15 +504,15 @@ class Game:
|
|||||||
|
|
||||||
self.__culling_zones = zones
|
self.__culling_zones = zones
|
||||||
|
|
||||||
def add_destroyed_units(self, data):
|
def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None:
|
||||||
pos = Point(data["x"], data["z"])
|
pos = Point(cast(float, data["x"]), cast(float, data["z"]))
|
||||||
if self.theater.is_on_land(pos):
|
if self.theater.is_on_land(pos):
|
||||||
self.__destroyed_units.append(data)
|
self.__destroyed_units.append(data)
|
||||||
|
|
||||||
def get_destroyed_units(self):
|
def get_destroyed_units(self) -> list[dict[str, Union[float, str]]]:
|
||||||
return self.__destroyed_units
|
return self.__destroyed_units
|
||||||
|
|
||||||
def position_culled(self, pos):
|
def position_culled(self, pos: Point) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if unit can be generated at given position depending on culling performance settings
|
Check if unit can be generated at given position depending on culling performance settings
|
||||||
:param pos: Position you are tryng to spawn stuff at
|
:param pos: Position you are tryng to spawn stuff at
|
||||||
@ -597,38 +525,17 @@ class Game:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_culling_zones(self):
|
def get_culling_zones(self) -> list[Point]:
|
||||||
"""
|
"""
|
||||||
Check culling points
|
Check culling points
|
||||||
:return: List of culling zones
|
:return: List of culling zones
|
||||||
"""
|
"""
|
||||||
return self.__culling_zones
|
return self.__culling_zones
|
||||||
|
|
||||||
# 1 = red, 2 = blue
|
def process_win_loss(self, turn_state: TurnState) -> None:
|
||||||
def get_player_coalition_id(self):
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def get_enemy_coalition_id(self):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def get_player_coalition(self):
|
|
||||||
return Coalition.Blue
|
|
||||||
|
|
||||||
def get_enemy_coalition(self):
|
|
||||||
return Coalition.Red
|
|
||||||
|
|
||||||
def get_player_color(self):
|
|
||||||
return "blue"
|
|
||||||
|
|
||||||
def get_enemy_color(self):
|
|
||||||
return "red"
|
|
||||||
|
|
||||||
def process_win_loss(self, turn_state: TurnState):
|
|
||||||
if turn_state is TurnState.WIN:
|
if turn_state is TurnState.WIN:
|
||||||
return self.message(
|
self.message(
|
||||||
"Congratulations, you are victorious! Start a new campaign to continue."
|
"Congratulations, you are victorious! Start a new campaign to continue."
|
||||||
)
|
)
|
||||||
elif turn_state is TurnState.LOSS:
|
elif turn_state is TurnState.LOSS:
|
||||||
return self.message(
|
self.message("Game Over, you lose. Start a new campaign to continue.")
|
||||||
"Game Over, you lose. Start a new campaign to continue."
|
|
||||||
)
|
|
||||||
|
|||||||
127
game/htn.py
Normal file
127
game/htn.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections import Iterator, deque, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Generic, Optional, TypeVar
|
||||||
|
|
||||||
|
WorldStateT = TypeVar("WorldStateT", bound="WorldState[Any]")
|
||||||
|
|
||||||
|
|
||||||
|
class WorldState(ABC, Generic[WorldStateT]):
|
||||||
|
@abstractmethod
|
||||||
|
def clone(self) -> WorldStateT:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class Task(Generic[WorldStateT]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
Method = Sequence[Task[WorldStateT]]
|
||||||
|
|
||||||
|
|
||||||
|
class PrimitiveTask(Task[WorldStateT], Generic[WorldStateT], ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def preconditions_met(self, state: WorldStateT) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def apply_effects(self, state: WorldStateT) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class CompoundTask(Task[WorldStateT], Generic[WorldStateT], ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def each_valid_method(self, state: WorldStateT) -> Iterator[Method[WorldStateT]]:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
PrimitiveTaskT = TypeVar("PrimitiveTaskT", bound=PrimitiveTask[Any])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanningState(Generic[WorldStateT, PrimitiveTaskT]):
|
||||||
|
state: WorldStateT
|
||||||
|
tasks_to_process: deque[Task[WorldStateT]]
|
||||||
|
plan: list[PrimitiveTaskT]
|
||||||
|
methods: Optional[Iterator[Method[WorldStateT]]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PlanningResult(Generic[WorldStateT, PrimitiveTaskT]):
|
||||||
|
tasks: list[PrimitiveTaskT]
|
||||||
|
end_state: WorldStateT
|
||||||
|
|
||||||
|
|
||||||
|
class PlanningHistory(Generic[WorldStateT, PrimitiveTaskT]):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.states: list[PlanningState[WorldStateT, PrimitiveTaskT]] = []
|
||||||
|
|
||||||
|
def push(self, planning_state: PlanningState[WorldStateT, PrimitiveTaskT]) -> None:
|
||||||
|
self.states.append(planning_state)
|
||||||
|
|
||||||
|
def pop(self) -> PlanningState[WorldStateT, PrimitiveTaskT]:
|
||||||
|
return self.states.pop()
|
||||||
|
|
||||||
|
|
||||||
|
class Planner(Generic[WorldStateT, PrimitiveTaskT]):
|
||||||
|
def __init__(self, main_task: Task[WorldStateT]) -> None:
|
||||||
|
self.main_task = main_task
|
||||||
|
|
||||||
|
def plan(
|
||||||
|
self, initial_state: WorldStateT
|
||||||
|
) -> Optional[PlanningResult[WorldStateT, PrimitiveTaskT]]:
|
||||||
|
planning_state: PlanningState[WorldStateT, PrimitiveTaskT] = PlanningState(
|
||||||
|
initial_state, deque([self.main_task]), [], None
|
||||||
|
)
|
||||||
|
history: PlanningHistory[WorldStateT, PrimitiveTaskT] = PlanningHistory()
|
||||||
|
while planning_state.tasks_to_process:
|
||||||
|
task = planning_state.tasks_to_process.popleft()
|
||||||
|
if isinstance(task, PrimitiveTask):
|
||||||
|
if task.preconditions_met(planning_state.state):
|
||||||
|
task.apply_effects(planning_state.state)
|
||||||
|
# Ignore type erasure. We've already verified that this is a Planner
|
||||||
|
# with a WorldStateT and a PrimitiveTaskT, so we know that the task
|
||||||
|
# list is a list of CompoundTask[WorldStateT] and PrimitiveTaskT. We
|
||||||
|
# could scatter more unions throughout to be more explicit but
|
||||||
|
# there's no way around the type erasure that mypy uses for
|
||||||
|
# isinstance.
|
||||||
|
planning_state.plan.append(task) # type: ignore
|
||||||
|
else:
|
||||||
|
planning_state = history.pop()
|
||||||
|
else:
|
||||||
|
assert isinstance(task, CompoundTask)
|
||||||
|
# If the methods field of our current state is not None that means we're
|
||||||
|
# resuming a prior attempt to execute this task after a subtask of the
|
||||||
|
# previously selected method failed.
|
||||||
|
#
|
||||||
|
# Otherwise this is the first exectution of this task so we need to
|
||||||
|
# create the generator.
|
||||||
|
if planning_state.methods is None:
|
||||||
|
methods = task.each_valid_method(planning_state.state)
|
||||||
|
else:
|
||||||
|
methods = planning_state.methods
|
||||||
|
try:
|
||||||
|
method = next(methods)
|
||||||
|
# Push the current node back onto the stack so that we resume
|
||||||
|
# handling this task when we pop back to this state.
|
||||||
|
resume_tasks: deque[Task[WorldStateT]] = deque([task])
|
||||||
|
resume_tasks.extend(planning_state.tasks_to_process)
|
||||||
|
history.push(
|
||||||
|
PlanningState(
|
||||||
|
planning_state.state.clone(),
|
||||||
|
resume_tasks,
|
||||||
|
planning_state.plan,
|
||||||
|
methods,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
planning_state.methods = None
|
||||||
|
planning_state.tasks_to_process.extendleft(reversed(method))
|
||||||
|
except StopIteration:
|
||||||
|
try:
|
||||||
|
planning_state = history.pop()
|
||||||
|
except IndexError:
|
||||||
|
# No valid plan was found.
|
||||||
|
return None
|
||||||
|
return PlanningResult(planning_state.plan, planning_state.state)
|
||||||
@ -2,13 +2,13 @@ import datetime
|
|||||||
|
|
||||||
|
|
||||||
class Information:
|
class Information:
|
||||||
def __init__(self, title="", text="", turn=0):
|
def __init__(self, title: str = "", text: str = "", turn: int = 0) -> None:
|
||||||
self.title = title
|
self.title = title
|
||||||
self.text = text
|
self.text = text
|
||||||
self.turn = turn
|
self.turn = turn
|
||||||
self.timestamp = datetime.datetime.now()
|
self.timestamp = datetime.datetime.now()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "[{}][{}] {} {}".format(
|
return "[{}][{}] {} {}".format(
|
||||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
if self.timestamp is not None
|
if self.timestamp is not None
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
"""Inventory management APIs."""
|
"""Inventory management APIs."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict, Iterator, Iterable
|
||||||
from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dcs.unittype import FlyingType
|
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
@ -18,7 +16,12 @@ class ControlPointAircraftInventory:
|
|||||||
|
|
||||||
def __init__(self, control_point: ControlPoint) -> None:
|
def __init__(self, control_point: ControlPoint) -> None:
|
||||||
self.control_point = control_point
|
self.control_point = control_point
|
||||||
self.inventory: Dict[AircraftType, int] = defaultdict(int)
|
self.inventory: dict[AircraftType, int] = defaultdict(int)
|
||||||
|
|
||||||
|
def clone(self) -> ControlPointAircraftInventory:
|
||||||
|
new = ControlPointAircraftInventory(self.control_point)
|
||||||
|
new.inventory = self.inventory.copy()
|
||||||
|
return new
|
||||||
|
|
||||||
def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
|
def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
|
||||||
"""Adds aircraft to the inventory.
|
"""Adds aircraft to the inventory.
|
||||||
@ -67,7 +70,7 @@ class ControlPointAircraftInventory:
|
|||||||
yield aircraft
|
yield aircraft
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_aircraft(self) -> Iterator[Tuple[AircraftType, int]]:
|
def all_aircraft(self) -> Iterator[tuple[AircraftType, int]]:
|
||||||
"""Iterates over all available aircraft types, including amounts."""
|
"""Iterates over all available aircraft types, including amounts."""
|
||||||
for aircraft, count in self.inventory.items():
|
for aircraft, count in self.inventory.items():
|
||||||
if count > 0:
|
if count > 0:
|
||||||
@ -82,14 +85,22 @@ class GlobalAircraftInventory:
|
|||||||
"""Game-wide aircraft inventory."""
|
"""Game-wide aircraft inventory."""
|
||||||
|
|
||||||
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
|
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
|
||||||
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
|
self.inventories: dict[ControlPoint, ControlPointAircraftInventory] = {
|
||||||
cp: ControlPointAircraftInventory(cp) for cp in control_points
|
cp: ControlPointAircraftInventory(cp) for cp in control_points
|
||||||
}
|
}
|
||||||
|
|
||||||
def reset(self) -> None:
|
def clone(self) -> GlobalAircraftInventory:
|
||||||
"""Clears all control points and their inventories."""
|
new = GlobalAircraftInventory([])
|
||||||
|
new.inventories = {
|
||||||
|
cp: inventory.clone() for cp, inventory in self.inventories.items()
|
||||||
|
}
|
||||||
|
return new
|
||||||
|
|
||||||
|
def reset(self, for_player: bool) -> None:
|
||||||
|
"""Clears the inventory of every control point owned by the given coalition."""
|
||||||
for inventory in self.inventories.values():
|
for inventory in self.inventories.values():
|
||||||
inventory.clear()
|
if inventory.control_point.captured == for_player:
|
||||||
|
inventory.clear()
|
||||||
|
|
||||||
def set_from_control_point(self, control_point: ControlPoint) -> None:
|
def set_from_control_point(self, control_point: ControlPoint) -> None:
|
||||||
"""Set the control point's aircraft inventory.
|
"""Set the control point's aircraft inventory.
|
||||||
@ -110,7 +121,7 @@ class GlobalAircraftInventory:
|
|||||||
@property
|
@property
|
||||||
def available_types_for_player(self) -> Iterator[AircraftType]:
|
def available_types_for_player(self) -> Iterator[AircraftType]:
|
||||||
"""Iterates over all aircraft types available to the player."""
|
"""Iterates over all aircraft types available to the player."""
|
||||||
seen: Set[AircraftType] = set()
|
seen: set[AircraftType] = set()
|
||||||
for control_point, inventory in self.inventories.items():
|
for control_point, inventory in self.inventories.items():
|
||||||
if control_point.captured:
|
if control_point.captured:
|
||||||
for aircraft in inventory.types_available:
|
for aircraft in inventory.types_available:
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
class DestroyedUnit:
|
|
||||||
"""
|
|
||||||
Store info about a destroyed unit
|
|
||||||
"""
|
|
||||||
|
|
||||||
x: int
|
|
||||||
y: int
|
|
||||||
name: str
|
|
||||||
|
|
||||||
def __init__(self, x, y, name):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.name = name
|
|
||||||
@ -1,4 +1,9 @@
|
|||||||
from typing import List
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
|
||||||
|
|
||||||
class FactionTurnMetadata:
|
class FactionTurnMetadata:
|
||||||
@ -10,7 +15,7 @@ class FactionTurnMetadata:
|
|||||||
vehicles_count: int = 0
|
vehicles_count: int = 0
|
||||||
sam_count: int = 0
|
sam_count: int = 0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.aircraft_count = 0
|
self.aircraft_count = 0
|
||||||
self.vehicles_count = 0
|
self.vehicles_count = 0
|
||||||
self.sam_count = 0
|
self.sam_count = 0
|
||||||
@ -24,7 +29,7 @@ class GameTurnMetadata:
|
|||||||
allied_units: FactionTurnMetadata
|
allied_units: FactionTurnMetadata
|
||||||
enemy_units: FactionTurnMetadata
|
enemy_units: FactionTurnMetadata
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.allied_units = FactionTurnMetadata()
|
self.allied_units = FactionTurnMetadata()
|
||||||
self.enemy_units = FactionTurnMetadata()
|
self.enemy_units = FactionTurnMetadata()
|
||||||
|
|
||||||
@ -34,15 +39,19 @@ class GameStats:
|
|||||||
Store statistics for the current game
|
Store statistics for the current game
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.data_per_turn: List[GameTurnMetadata] = []
|
self.data_per_turn: List[GameTurnMetadata] = []
|
||||||
|
|
||||||
def update(self, game):
|
def update(self, game: Game) -> None:
|
||||||
"""
|
"""
|
||||||
Save data for current turn
|
Save data for current turn
|
||||||
:param game: Game we want to save the data about
|
:param game: Game we want to save the data about
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Remove the current turn if its just an update for this turn
|
||||||
|
if 0 < game.turn < len(self.data_per_turn):
|
||||||
|
del self.data_per_turn[-1]
|
||||||
|
|
||||||
turn_data = GameTurnMetadata()
|
turn_data = GameTurnMetadata()
|
||||||
|
|
||||||
for cp in game.theater.controlpoints:
|
for cp in game.theater.controlpoints:
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, List, Set, TYPE_CHECKING
|
from typing import List, Set, TYPE_CHECKING, cast
|
||||||
|
|
||||||
from dcs import Mission
|
from dcs import Mission
|
||||||
from dcs.action import DoScript, DoScriptFile
|
from dcs.action import DoScript, DoScriptFile
|
||||||
@ -62,7 +62,7 @@ class Operation:
|
|||||||
plugin_scripts: List[str] = []
|
plugin_scripts: List[str] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare(cls, game: Game):
|
def prepare(cls, game: Game) -> None:
|
||||||
with open("resources/default_options.lua", "r") as f:
|
with open("resources/default_options.lua", "r") as f:
|
||||||
options_dict = loads(f.read())["options"]
|
options_dict = loads(f.read())["options"]
|
||||||
cls._set_mission(Mission(game.theater.terrain))
|
cls._set_mission(Mission(game.theater.terrain))
|
||||||
@ -70,20 +70,6 @@ class Operation:
|
|||||||
cls._setup_mission_coalitions()
|
cls._setup_mission_coalitions()
|
||||||
cls.current_mission.options.load_from_dict(options_dict)
|
cls.current_mission.options.load_from_dict(options_dict)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def conflicts(cls) -> Iterable[Conflict]:
|
|
||||||
assert cls.game
|
|
||||||
for frontline in cls.game.theater.conflicts():
|
|
||||||
yield Conflict(
|
|
||||||
cls.game.theater,
|
|
||||||
frontline,
|
|
||||||
cls.game.player_faction.name,
|
|
||||||
cls.game.enemy_faction.name,
|
|
||||||
cls.game.player_country,
|
|
||||||
cls.game.enemy_country,
|
|
||||||
frontline.position,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def air_conflict(cls) -> Conflict:
|
def air_conflict(cls) -> Conflict:
|
||||||
assert cls.game
|
assert cls.game
|
||||||
@ -95,10 +81,10 @@ class Operation:
|
|||||||
return Conflict(
|
return Conflict(
|
||||||
cls.game.theater,
|
cls.game.theater,
|
||||||
FrontLine(player_cp, enemy_cp),
|
FrontLine(player_cp, enemy_cp),
|
||||||
cls.game.player_faction.name,
|
cls.game.blue.faction.name,
|
||||||
cls.game.enemy_faction.name,
|
cls.game.red.faction.name,
|
||||||
cls.game.player_country,
|
cls.current_mission.country(cls.game.blue.country_name),
|
||||||
cls.game.enemy_country,
|
cls.current_mission.country(cls.game.red.country_name),
|
||||||
mid_point,
|
mid_point,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -107,16 +93,16 @@ class Operation:
|
|||||||
cls.current_mission = mission
|
cls.current_mission = mission
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _setup_mission_coalitions(cls):
|
def _setup_mission_coalitions(cls) -> None:
|
||||||
cls.current_mission.coalition["blue"] = Coalition(
|
cls.current_mission.coalition["blue"] = Coalition(
|
||||||
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
|
"blue", bullseye=cls.game.blue.bullseye.to_pydcs()
|
||||||
)
|
)
|
||||||
cls.current_mission.coalition["red"] = Coalition(
|
cls.current_mission.coalition["red"] = Coalition(
|
||||||
"red", bullseye=cls.game.red_bullseye.to_pydcs()
|
"red", bullseye=cls.game.red.bullseye.to_pydcs()
|
||||||
)
|
)
|
||||||
|
|
||||||
p_country = cls.game.player_country
|
p_country = cls.game.blue.country_name
|
||||||
e_country = cls.game.enemy_country
|
e_country = cls.game.red.country_name
|
||||||
cls.current_mission.coalition["blue"].add_country(
|
cls.current_mission.coalition["blue"].add_country(
|
||||||
country_dict[db.country_id_from_name(p_country)]()
|
country_dict[db.country_id_from_name(p_country)]()
|
||||||
)
|
)
|
||||||
@ -163,7 +149,7 @@ class Operation:
|
|||||||
airsupportgen: AirSupportConflictGenerator,
|
airsupportgen: AirSupportConflictGenerator,
|
||||||
jtacs: List[JtacInfo],
|
jtacs: List[JtacInfo],
|
||||||
airgen: AircraftConflictGenerator,
|
airgen: AircraftConflictGenerator,
|
||||||
):
|
) -> None:
|
||||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
|
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
|
||||||
|
|
||||||
gens: List[MissionInfoGenerator] = [
|
gens: List[MissionInfoGenerator] = [
|
||||||
@ -251,7 +237,7 @@ class Operation:
|
|||||||
# beacon list.
|
# beacon list.
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _generate_ground_units(cls):
|
def _generate_ground_units(cls) -> None:
|
||||||
cls.groundobjectgen = GroundObjectsGenerator(
|
cls.groundobjectgen = GroundObjectsGenerator(
|
||||||
cls.current_mission,
|
cls.current_mission,
|
||||||
cls.game,
|
cls.game,
|
||||||
@ -266,18 +252,23 @@ class Operation:
|
|||||||
"""Add destroyed units to the Mission"""
|
"""Add destroyed units to the Mission"""
|
||||||
for d in cls.game.get_destroyed_units():
|
for d in cls.game.get_destroyed_units():
|
||||||
try:
|
try:
|
||||||
utype = db.unit_type_from_name(d["type"])
|
type_name = d["type"]
|
||||||
|
if not isinstance(type_name, str):
|
||||||
|
raise TypeError(
|
||||||
|
"Expected the type of the destroyed static to be a string"
|
||||||
|
)
|
||||||
|
utype = db.unit_type_from_name(type_name)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pos = Point(d["x"], d["z"])
|
pos = Point(cast(float, d["x"]), cast(float, d["z"]))
|
||||||
if (
|
if (
|
||||||
utype is not None
|
utype is not None
|
||||||
and not cls.game.position_culled(pos)
|
and not cls.game.position_culled(pos)
|
||||||
and cls.game.settings.perf_destroyed_units
|
and cls.game.settings.perf_destroyed_units
|
||||||
):
|
):
|
||||||
cls.current_mission.static_group(
|
cls.current_mission.static_group(
|
||||||
country=cls.current_mission.country(cls.game.player_country),
|
country=cls.current_mission.country(cls.game.blue.country_name),
|
||||||
name="",
|
name="",
|
||||||
_type=utype,
|
_type=utype,
|
||||||
hidden=True,
|
hidden=True,
|
||||||
@ -367,18 +358,18 @@ class Operation:
|
|||||||
cls.airgen.clear_parking_slots()
|
cls.airgen.clear_parking_slots()
|
||||||
|
|
||||||
cls.airgen.generate_flights(
|
cls.airgen.generate_flights(
|
||||||
cls.current_mission.country(cls.game.player_country),
|
cls.current_mission.country(cls.game.blue.country_name),
|
||||||
cls.game.blue_ato,
|
cls.game.blue.ato,
|
||||||
cls.groundobjectgen.runways,
|
cls.groundobjectgen.runways,
|
||||||
)
|
)
|
||||||
cls.airgen.generate_flights(
|
cls.airgen.generate_flights(
|
||||||
cls.current_mission.country(cls.game.enemy_country),
|
cls.current_mission.country(cls.game.red.country_name),
|
||||||
cls.game.red_ato,
|
cls.game.red.ato,
|
||||||
cls.groundobjectgen.runways,
|
cls.groundobjectgen.runways,
|
||||||
)
|
)
|
||||||
cls.airgen.spawn_unused_aircraft(
|
cls.airgen.spawn_unused_aircraft(
|
||||||
cls.current_mission.country(cls.game.player_country),
|
cls.current_mission.country(cls.game.blue.country_name),
|
||||||
cls.current_mission.country(cls.game.enemy_country),
|
cls.current_mission.country(cls.game.red.country_name),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -389,10 +380,10 @@ class Operation:
|
|||||||
player_cp = front_line.blue_cp
|
player_cp = front_line.blue_cp
|
||||||
enemy_cp = front_line.red_cp
|
enemy_cp = front_line.red_cp
|
||||||
conflict = Conflict.frontline_cas_conflict(
|
conflict = Conflict.frontline_cas_conflict(
|
||||||
cls.game.player_faction.name,
|
cls.game.blue.faction.name,
|
||||||
cls.game.enemy_faction.name,
|
cls.game.red.faction.name,
|
||||||
cls.current_mission.country(cls.game.player_country),
|
cls.current_mission.country(cls.game.blue.country_name),
|
||||||
cls.current_mission.country(cls.game.enemy_country),
|
cls.current_mission.country(cls.game.red.country_name),
|
||||||
front_line,
|
front_line,
|
||||||
cls.game.theater,
|
cls.game.theater,
|
||||||
)
|
)
|
||||||
@ -406,6 +397,7 @@ class Operation:
|
|||||||
player_gp,
|
player_gp,
|
||||||
enemy_gp,
|
enemy_gp,
|
||||||
player_cp.stances[enemy_cp.id],
|
player_cp.stances[enemy_cp.id],
|
||||||
|
enemy_cp.stances[player_cp.id],
|
||||||
cls.unit_map,
|
cls.unit_map,
|
||||||
)
|
)
|
||||||
ground_conflict_gen.generate()
|
ground_conflict_gen.generate()
|
||||||
@ -418,7 +410,7 @@ class Operation:
|
|||||||
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
|
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reset_naming_ids(cls):
|
def reset_naming_ids(cls) -> None:
|
||||||
namegen.reset_numbers()
|
namegen.reset_numbers()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
|
||||||
_dcs_saved_game_folder: Optional[str] = None
|
_dcs_saved_game_folder: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
def setup(user_folder: str):
|
def setup(user_folder: str) -> None:
|
||||||
global _dcs_saved_game_folder
|
global _dcs_saved_game_folder
|
||||||
_dcs_saved_game_folder = user_folder
|
_dcs_saved_game_folder = user_folder
|
||||||
if not save_dir().exists():
|
if not save_dir().exists():
|
||||||
@ -38,7 +42,7 @@ def mission_path_for(name: str) -> str:
|
|||||||
return os.path.join(base_path(), "Missions", name)
|
return os.path.join(base_path(), "Missions", name)
|
||||||
|
|
||||||
|
|
||||||
def load_game(path):
|
def load_game(path: str) -> Optional[Game]:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
try:
|
try:
|
||||||
save = pickle.load(f)
|
save = pickle.load(f)
|
||||||
@ -49,7 +53,7 @@ def load_game(path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def save_game(game) -> bool:
|
def save_game(game: Game) -> bool:
|
||||||
try:
|
try:
|
||||||
with open(_temporary_save_file(), "wb") as f:
|
with open(_temporary_save_file(), "wb") as f:
|
||||||
pickle.dump(game, f)
|
pickle.dump(game, f)
|
||||||
@ -60,7 +64,7 @@ def save_game(game) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def autosave(game) -> bool:
|
def autosave(game: Game) -> bool:
|
||||||
"""
|
"""
|
||||||
Autosave to the autosave location
|
Autosave to the autosave location
|
||||||
:param game: Game to save
|
:param game: Game to save
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class PluginSettings:
|
|||||||
self.settings = Settings()
|
self.settings = Settings()
|
||||||
self.initialize_settings()
|
self.initialize_settings()
|
||||||
|
|
||||||
def set_settings(self, settings: Settings):
|
def set_settings(self, settings: Settings) -> None:
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.initialize_settings()
|
self.initialize_settings()
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ class LuaPlugin(PluginSettings):
|
|||||||
|
|
||||||
return cls(definition)
|
return cls(definition)
|
||||||
|
|
||||||
def set_settings(self, settings: Settings):
|
def set_settings(self, settings: Settings) -> None:
|
||||||
super().set_settings(settings)
|
super().set_settings(settings)
|
||||||
for option in self.definition.options:
|
for option in self.definition.options:
|
||||||
option.set_settings(self.settings)
|
option.set_settings(self.settings)
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
|
|
||||||
class PointWithHeading(Point):
|
class PointWithHeading(Point):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super(PointWithHeading, self).__init__(0, 0)
|
super(PointWithHeading, self).__init__(0, 0)
|
||||||
self.heading = 0
|
self.heading = 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_point(point: Point, heading: int):
|
def from_point(point: Point, heading: int) -> PointWithHeading:
|
||||||
p = PointWithHeading()
|
p = PointWithHeading()
|
||||||
p.x = point.x
|
p.x = point.x
|
||||||
p.y = point.y
|
p.y = point.y
|
||||||
|
|||||||
9
game/positioned.py
Normal file
9
game/positioned.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from dcs import Point
|
||||||
|
|
||||||
|
|
||||||
|
class Positioned(Protocol):
|
||||||
|
@property
|
||||||
|
def position(self) -> Point:
|
||||||
|
raise NotImplementedError
|
||||||
@ -72,7 +72,9 @@ class ProcurementAi:
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
for cp in self.owned_points:
|
for cp in self.owned_points:
|
||||||
cp_ground_units = cp.allocated_ground_units(self.game.transfers)
|
cp_ground_units = cp.allocated_ground_units(
|
||||||
|
self.game.coalition_for(self.is_player).transfers
|
||||||
|
)
|
||||||
armor_investment += cp_ground_units.total_value
|
armor_investment += cp_ground_units.total_value
|
||||||
cp_aircraft = cp.allocated_aircraft(self.game)
|
cp_aircraft = cp.allocated_aircraft(self.game)
|
||||||
aircraft_investment += cp_aircraft.total_value
|
aircraft_investment += cp_aircraft.total_value
|
||||||
@ -316,7 +318,9 @@ class ProcurementAi:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
|
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
|
||||||
allocated = cp.allocated_ground_units(self.game.transfers)
|
allocated = cp.allocated_ground_units(
|
||||||
|
self.game.coalition_for(self.is_player).transfers
|
||||||
|
)
|
||||||
if allocated.total >= purchase_target:
|
if allocated.total >= purchase_target:
|
||||||
# Control point is already sufficiently defended.
|
# Control point is already sufficiently defended.
|
||||||
continue
|
continue
|
||||||
@ -343,7 +347,9 @@ class ProcurementAi:
|
|||||||
if not cp.can_recruit_ground_units(self.game):
|
if not cp.can_recruit_ground_units(self.game):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
allocated = cp.allocated_ground_units(self.game.transfers)
|
allocated = cp.allocated_ground_units(
|
||||||
|
self.game.coalition_for(self.is_player).transfers
|
||||||
|
)
|
||||||
if allocated.total >= self.game.settings.reserves_procurement_target:
|
if allocated.total >= self.game.settings.reserves_procurement_target:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -356,7 +362,9 @@ class ProcurementAi:
|
|||||||
def cost_ratio_of_ground_unit(
|
def cost_ratio_of_ground_unit(
|
||||||
self, control_point: ControlPoint, unit_class: GroundUnitClass
|
self, control_point: ControlPoint, unit_class: GroundUnitClass
|
||||||
) -> float:
|
) -> float:
|
||||||
allocations = control_point.allocated_ground_units(self.game.transfers)
|
allocations = control_point.allocated_ground_units(
|
||||||
|
self.game.coalition_for(self.is_player).transfers
|
||||||
|
)
|
||||||
class_cost = 0
|
class_cost = 0
|
||||||
total_cost = 0
|
total_cost = 0
|
||||||
for unit_type, count in allocations.all.items():
|
for unit_type, count in allocations.all.items():
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import timeit
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Iterator
|
from types import TracebackType
|
||||||
|
from typing import Iterator, Optional, Type
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@ -23,7 +24,12 @@ class MultiEventTracer:
|
|||||||
def __enter__(self) -> MultiEventTracer:
|
def __enter__(self) -> MultiEventTracer:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Optional[Type[BaseException]],
|
||||||
|
exc_val: Optional[BaseException],
|
||||||
|
exc_tb: Optional[TracebackType],
|
||||||
|
) -> None:
|
||||||
for event, duration in self.events.items():
|
for event, duration in self.events.items():
|
||||||
logging.debug("%s took %s", event, duration)
|
logging.debug("%s took %s", event, duration)
|
||||||
|
|
||||||
|
|||||||
48
game/savecompat.py
Normal file
48
game/savecompat.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""Tools for aiding in save compat removal after compatibility breaks."""
|
||||||
|
from collections import Callable
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
|
from game.version import MAJOR_VERSION
|
||||||
|
|
||||||
|
ReturnT = TypeVar("ReturnT")
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedSaveCompatError(RuntimeError):
|
||||||
|
def __init__(self, function_name: str) -> None:
|
||||||
|
super().__init__(
|
||||||
|
f"{function_name} has save compat code for a different major version."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_save_compat_for(
|
||||||
|
major: int,
|
||||||
|
) -> Callable[[Callable[..., ReturnT]], Callable[..., ReturnT]]:
|
||||||
|
"""Declares a function or method as having save compat code for a given version.
|
||||||
|
|
||||||
|
If the function has save compatibility for the current major version, there is no
|
||||||
|
change in behavior.
|
||||||
|
|
||||||
|
If the function has save compatibility for a *different* (future or past) major
|
||||||
|
version, DeprecatedSaveCompatError will be raised during startup. Since a break in
|
||||||
|
save compatibility is the definition of a major version break, there's no need to
|
||||||
|
keep around old save compat code; it only serves to mask initialization bugs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
major: The major version for which the decorated function has save
|
||||||
|
compatibility.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The decorated function or method.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
DeprecatedSaveCompatError: The decorated function has save compat code for
|
||||||
|
another version of liberation, and that code (and the decorator declaring it)
|
||||||
|
should be removed from this branch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func: Callable[..., ReturnT]) -> Callable[..., ReturnT]:
|
||||||
|
if major != MAJOR_VERSION:
|
||||||
|
raise DeprecatedSaveCompatError(func.__name__)
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, Any
|
||||||
|
|
||||||
from dcs.forcedoptions import ForcedOptions
|
from dcs.forcedoptions import ForcedOptions
|
||||||
|
|
||||||
@ -55,6 +55,7 @@ class Settings:
|
|||||||
automate_runway_repair: bool = False
|
automate_runway_repair: bool = False
|
||||||
automate_front_line_reinforcements: bool = False
|
automate_front_line_reinforcements: bool = False
|
||||||
automate_aircraft_reinforcements: bool = False
|
automate_aircraft_reinforcements: bool = False
|
||||||
|
automate_front_line_stance: bool = True
|
||||||
restrict_weapons_by_date: bool = False
|
restrict_weapons_by_date: bool = False
|
||||||
disable_legacy_aewc: bool = True
|
disable_legacy_aewc: bool = True
|
||||||
disable_legacy_tanker: bool = True
|
disable_legacy_tanker: bool = True
|
||||||
@ -104,7 +105,7 @@ class Settings:
|
|||||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
||||||
|
|
||||||
def __setstate__(self, state) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
# __setstate__ is called with the dict of the object being unpickled. We
|
# __setstate__ is called with the dict of the object being unpickled. We
|
||||||
# can provide save compatibility for new settings options (which
|
# can provide save compatibility for new settings options (which
|
||||||
# normally would not be present in the unpickled object) by creating a
|
# normally would not be present in the unpickled object) by creating a
|
||||||
|
|||||||
@ -13,16 +13,18 @@ from typing import (
|
|||||||
Optional,
|
Optional,
|
||||||
Iterator,
|
Iterator,
|
||||||
Sequence,
|
Sequence,
|
||||||
|
Any,
|
||||||
)
|
)
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.settings import AutoAtoBehavior
|
from game.settings import AutoAtoBehavior, Settings
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from game.coalition import Coalition
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
|
||||||
@ -95,16 +97,13 @@ class Squadron:
|
|||||||
init=False, hash=False, compare=False
|
init=False, hash=False, compare=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# We need a reference to the Game so that we can access the Faker without needing to
|
coalition: Coalition = field(hash=False, compare=False)
|
||||||
# persist it to the save game, or having to reconstruct it (it's not cheap) each
|
settings: Settings = field(hash=False, compare=False)
|
||||||
# time we create or load a squadron.
|
|
||||||
game: Game = field(hash=False, compare=False)
|
|
||||||
player: bool
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
|
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
|
||||||
raise ValueError("Squadrons can only be created with active pilots.")
|
raise ValueError("Squadrons can only be created with active pilots.")
|
||||||
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
|
self._recruit_pilots(self.settings.squadron_pilot_limit)
|
||||||
self.auto_assignable_mission_types = set(self.mission_types)
|
self.auto_assignable_mission_types = set(self.mission_types)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -112,9 +111,13 @@ class Squadron:
|
|||||||
return self.name
|
return self.name
|
||||||
return f'{self.name} "{self.nickname}"'
|
return f'{self.name} "{self.nickname}"'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def player(self) -> bool:
|
||||||
|
return self.coalition.player
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pilot_limits_enabled(self) -> bool:
|
def pilot_limits_enabled(self) -> bool:
|
||||||
return self.game.settings.enable_squadron_pilot_limits
|
return self.settings.enable_squadron_pilot_limits
|
||||||
|
|
||||||
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
|
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
|
||||||
if self.pilot_limits_enabled:
|
if self.pilot_limits_enabled:
|
||||||
@ -130,7 +133,7 @@ class Squadron:
|
|||||||
if not self.player:
|
if not self.player:
|
||||||
return self.available_pilots.pop()
|
return self.available_pilots.pop()
|
||||||
|
|
||||||
preference = self.game.settings.auto_ato_behavior
|
preference = self.settings.auto_ato_behavior
|
||||||
|
|
||||||
# No preference, so the first pilot is fine.
|
# No preference, so the first pilot is fine.
|
||||||
if preference is AutoAtoBehavior.Default:
|
if preference is AutoAtoBehavior.Default:
|
||||||
@ -183,7 +186,7 @@ class Squadron:
|
|||||||
return
|
return
|
||||||
|
|
||||||
replenish_count = min(
|
replenish_count = min(
|
||||||
self.game.settings.squadron_replenishment_rate,
|
self.settings.squadron_replenishment_rate,
|
||||||
self._number_of_unfilled_pilot_slots,
|
self._number_of_unfilled_pilot_slots,
|
||||||
)
|
)
|
||||||
if replenish_count > 0:
|
if replenish_count > 0:
|
||||||
@ -196,7 +199,7 @@ class Squadron:
|
|||||||
def send_on_leave(pilot: Pilot) -> None:
|
def send_on_leave(pilot: Pilot) -> None:
|
||||||
pilot.send_on_leave()
|
pilot.send_on_leave()
|
||||||
|
|
||||||
def return_from_leave(self, pilot: Pilot):
|
def return_from_leave(self, pilot: Pilot) -> None:
|
||||||
if not self.has_unfilled_pilot_slots:
|
if not self.has_unfilled_pilot_slots:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Cannot return {pilot} from leave because {self} is full"
|
f"Cannot return {pilot} from leave because {self} is full"
|
||||||
@ -205,7 +208,7 @@ class Squadron:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def faker(self) -> Faker:
|
def faker(self) -> Faker:
|
||||||
return self.game.faker_for(self.player)
|
return self.coalition.faker
|
||||||
|
|
||||||
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
|
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
|
||||||
return [p for p in self.current_roster if p.status == status]
|
return [p for p in self.current_roster if p.status == status]
|
||||||
@ -227,7 +230,7 @@ class Squadron:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _number_of_unfilled_pilot_slots(self) -> int:
|
def _number_of_unfilled_pilot_slots(self) -> int:
|
||||||
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
|
return self.settings.squadron_pilot_limit - len(self.active_pilots)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def number_of_available_pilots(self) -> int:
|
def number_of_available_pilots(self) -> int:
|
||||||
@ -251,7 +254,7 @@ class Squadron:
|
|||||||
return self.current_roster[index]
|
return self.current_roster[index]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
|
def from_yaml(cls, path: Path, game: Game, coalition: Coalition) -> Squadron:
|
||||||
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
@ -286,11 +289,11 @@ class Squadron:
|
|||||||
livery=data.get("livery"),
|
livery=data.get("livery"),
|
||||||
mission_types=tuple(mission_types),
|
mission_types=tuple(mission_types),
|
||||||
pilot_pool=pilots,
|
pilot_pool=pilots,
|
||||||
game=game,
|
coalition=coalition,
|
||||||
player=player,
|
settings=game.settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __setstate__(self, state) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
# TODO: Remove save compat.
|
# TODO: Remove save compat.
|
||||||
if "auto_assignable_mission_types" not in state:
|
if "auto_assignable_mission_types" not in state:
|
||||||
state["auto_assignable_mission_types"] = set(state["mission_types"])
|
state["auto_assignable_mission_types"] = set(state["mission_types"])
|
||||||
@ -298,9 +301,9 @@ class Squadron:
|
|||||||
|
|
||||||
|
|
||||||
class SquadronLoader:
|
class SquadronLoader:
|
||||||
def __init__(self, game: Game, player: bool) -> None:
|
def __init__(self, game: Game, coalition: Coalition) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
self.player = player
|
self.coalition = coalition
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def squadron_directories() -> Iterator[Path]:
|
def squadron_directories() -> Iterator[Path]:
|
||||||
@ -311,8 +314,8 @@ class SquadronLoader:
|
|||||||
|
|
||||||
def load(self) -> dict[AircraftType, list[Squadron]]:
|
def load(self) -> dict[AircraftType, list[Squadron]]:
|
||||||
squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
|
squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
|
||||||
country = self.game.country_for(self.player)
|
country = self.coalition.country_name
|
||||||
faction = self.game.faction_for(self.player)
|
faction = self.coalition.faction
|
||||||
any_country = country.startswith("Combined Joint Task Forces ")
|
any_country = country.startswith("Combined Joint Task Forces ")
|
||||||
for directory in self.squadron_directories():
|
for directory in self.squadron_directories():
|
||||||
for path, squadron in self.load_squadrons_from(directory):
|
for path, squadron in self.load_squadrons_from(directory):
|
||||||
@ -346,7 +349,7 @@ class SquadronLoader:
|
|||||||
for squadron_path in directory.glob("*/*.yaml"):
|
for squadron_path in directory.glob("*/*.yaml"):
|
||||||
try:
|
try:
|
||||||
yield squadron_path, Squadron.from_yaml(
|
yield squadron_path, Squadron.from_yaml(
|
||||||
squadron_path, self.game, self.player
|
squadron_path, self.game, self.coalition
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@ -355,29 +358,28 @@ class SquadronLoader:
|
|||||||
|
|
||||||
|
|
||||||
class AirWing:
|
class AirWing:
|
||||||
def __init__(self, game: Game, player: bool) -> None:
|
def __init__(self, game: Game, coalition: Coalition) -> None:
|
||||||
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.player = player
|
self.squadrons = SquadronLoader(game, coalition).load()
|
||||||
self.squadrons = SquadronLoader(game, player).load()
|
|
||||||
|
|
||||||
count = itertools.count(1)
|
count = itertools.count(1)
|
||||||
for aircraft in game.faction_for(player).aircrafts:
|
for aircraft in coalition.faction.aircrafts:
|
||||||
if aircraft in self.squadrons:
|
if aircraft in self.squadrons:
|
||||||
continue
|
continue
|
||||||
self.squadrons[aircraft] = [
|
self.squadrons[aircraft] = [
|
||||||
Squadron(
|
Squadron(
|
||||||
name=f"Squadron {next(count):03}",
|
name=f"Squadron {next(count):03}",
|
||||||
nickname=self.random_nickname(),
|
nickname=self.random_nickname(),
|
||||||
country=game.country_for(player),
|
country=coalition.country_name,
|
||||||
role="Flying Squadron",
|
role="Flying Squadron",
|
||||||
aircraft=aircraft,
|
aircraft=aircraft,
|
||||||
livery=None,
|
livery=None,
|
||||||
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
||||||
pilot_pool=[],
|
pilot_pool=[],
|
||||||
game=game,
|
coalition=coalition,
|
||||||
player=player,
|
settings=game.settings,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -6,15 +6,15 @@ from game.dcs.aircrafttype import AircraftType
|
|||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
|
|
||||||
BASE_MAX_STRENGTH = 1
|
BASE_MAX_STRENGTH = 1.0
|
||||||
BASE_MIN_STRENGTH = 0
|
BASE_MIN_STRENGTH = 0.0
|
||||||
|
|
||||||
|
|
||||||
class Base:
|
class Base:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.aircraft: dict[AircraftType, int] = {}
|
self.aircraft: dict[AircraftType, int] = {}
|
||||||
self.armor: dict[GroundUnitType, int] = {}
|
self.armor: dict[GroundUnitType, int] = {}
|
||||||
self.strength = 1
|
self.strength = 1.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_aircraft(self) -> int:
|
def total_aircraft(self) -> int:
|
||||||
@ -31,7 +31,7 @@ class Base:
|
|||||||
total += unit_type.price * count
|
total += unit_type.price * count
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def total_units_of_type(self, unit_type: UnitType) -> int:
|
def total_units_of_type(self, unit_type: UnitType[Any]) -> int:
|
||||||
return sum(
|
return sum(
|
||||||
[
|
[
|
||||||
c
|
c
|
||||||
@ -40,7 +40,7 @@ class Base:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def commission_units(self, units: dict[Any, int]):
|
def commission_units(self, units: dict[Any, int]) -> None:
|
||||||
for unit_type, unit_count in units.items():
|
for unit_type, unit_count in units.items():
|
||||||
if unit_count <= 0:
|
if unit_count <= 0:
|
||||||
continue
|
continue
|
||||||
@ -56,7 +56,7 @@ class Base:
|
|||||||
|
|
||||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||||
|
|
||||||
def commit_losses(self, units_lost: dict[Any, int]):
|
def commit_losses(self, units_lost: dict[Any, int]) -> None:
|
||||||
for unit_type, count in units_lost.items():
|
for unit_type, count in units_lost.items():
|
||||||
target_dict: dict[Any, int]
|
target_dict: dict[Any, int]
|
||||||
if unit_type in self.aircraft:
|
if unit_type in self.aircraft:
|
||||||
@ -75,7 +75,7 @@ class Base:
|
|||||||
if target_dict[unit_type] == 0:
|
if target_dict[unit_type] == 0:
|
||||||
del target_dict[unit_type]
|
del target_dict[unit_type]
|
||||||
|
|
||||||
def affect_strength(self, amount):
|
def affect_strength(self, amount: float) -> None:
|
||||||
self.strength += amount
|
self.strength += amount
|
||||||
if self.strength > BASE_MAX_STRENGTH:
|
if self.strength > BASE_MAX_STRENGTH:
|
||||||
self.strength = BASE_MAX_STRENGTH
|
self.strength = BASE_MAX_STRENGTH
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import math
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import Mission
|
from dcs import Mission
|
||||||
from dcs.countries import (
|
from dcs.countries import (
|
||||||
@ -29,14 +29,14 @@ from dcs.terrain import (
|
|||||||
persiangulf,
|
persiangulf,
|
||||||
syria,
|
syria,
|
||||||
thechannel,
|
thechannel,
|
||||||
|
marianaislands,
|
||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport, Terrain
|
from dcs.terrain.terrain import Airport, Terrain
|
||||||
from dcs.unitgroup import (
|
from dcs.unitgroup import (
|
||||||
FlyingGroup,
|
|
||||||
Group,
|
|
||||||
ShipGroup,
|
ShipGroup,
|
||||||
StaticGroup,
|
StaticGroup,
|
||||||
VehicleGroup,
|
VehicleGroup,
|
||||||
|
PlaneGroup,
|
||||||
)
|
)
|
||||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||||
from pyproj import CRS, Transformer
|
from pyproj import CRS, Transformer
|
||||||
@ -56,10 +56,14 @@ from .landmap import Landmap, load_landmap, poly_contains
|
|||||||
from .latlon import LatLon
|
from .latlon import LatLon
|
||||||
from .projections import TransverseMercator
|
from .projections import TransverseMercator
|
||||||
from ..point_with_heading import PointWithHeading
|
from ..point_with_heading import PointWithHeading
|
||||||
|
from ..positioned import Positioned
|
||||||
from ..profiling import logged_duration
|
from ..profiling import logged_duration
|
||||||
from ..scenery_group import SceneryGroup
|
from ..scenery_group import SceneryGroup
|
||||||
from ..utils import Distance, meters
|
from ..utils import Distance, meters
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import TheaterGroundObject
|
||||||
|
|
||||||
SIZE_TINY = 150
|
SIZE_TINY = 150
|
||||||
SIZE_SMALL = 600
|
SIZE_SMALL = 600
|
||||||
SIZE_REGULAR = 1000
|
SIZE_REGULAR = 1000
|
||||||
@ -181,7 +185,7 @@ class MizCampaignLoader:
|
|||||||
def red(self) -> Country:
|
def red(self) -> Country:
|
||||||
return self.country(blue=False)
|
return self.country(blue=False)
|
||||||
|
|
||||||
def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
|
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
|
||||||
for group in self.country(blue).plane_group:
|
for group in self.country(blue).plane_group:
|
||||||
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
@ -305,26 +309,26 @@ class MizCampaignLoader:
|
|||||||
control_point.captured = blue
|
control_point.captured = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = group.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for group in self.carriers(blue):
|
for ship in self.carriers(blue):
|
||||||
# TODO: Name the carrier.
|
# TODO: Name the carrier.
|
||||||
control_point = Carrier(
|
control_point = Carrier(
|
||||||
"carrier", group.position, next(self.control_point_id)
|
"carrier", ship.position, next(self.control_point_id)
|
||||||
)
|
)
|
||||||
control_point.captured = blue
|
control_point.captured = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = ship.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for group in self.lhas(blue):
|
for ship in self.lhas(blue):
|
||||||
# TODO: Name the LHA.db
|
# TODO: Name the LHA.db
|
||||||
control_point = Lha("lha", group.position, next(self.control_point_id))
|
control_point = Lha("lha", ship.position, next(self.control_point_id))
|
||||||
control_point.captured = blue
|
control_point.captured = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = ship.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for group in self.fobs(blue):
|
for fob in self.fobs(blue):
|
||||||
control_point = Fob(
|
control_point = Fob(
|
||||||
str(group.name), group.position, next(self.control_point_id)
|
str(fob.name), fob.position, next(self.control_point_id)
|
||||||
)
|
)
|
||||||
control_point.captured = blue
|
control_point.captured = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = fob.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
return control_points
|
return control_points
|
||||||
@ -385,22 +389,22 @@ class MizCampaignLoader:
|
|||||||
origin, list(reversed(waypoints))
|
origin, list(reversed(waypoints))
|
||||||
)
|
)
|
||||||
|
|
||||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
def objective_info(self, near: Positioned) -> Tuple[ControlPoint, Distance]:
|
||||||
closest = self.theater.closest_control_point(group.position)
|
closest = self.theater.closest_control_point(near.position)
|
||||||
distance = meters(closest.position.distance_to_point(group.position))
|
distance = meters(closest.position.distance_to_point(near.position))
|
||||||
return closest, distance
|
return closest, distance
|
||||||
|
|
||||||
def add_preset_locations(self) -> None:
|
def add_preset_locations(self) -> None:
|
||||||
for group in self.offshore_strike_targets:
|
for static in self.offshore_strike_targets:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.preset_locations.offshore_strike_locations.append(
|
closest.preset_locations.offshore_strike_locations.append(
|
||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.ships:
|
for ship in self.ships:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(ship)
|
||||||
closest.preset_locations.ships.append(
|
closest.preset_locations.ships.append(
|
||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(ship.position, ship.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.missile_sites:
|
for group in self.missile_sites:
|
||||||
@ -451,33 +455,33 @@ class MizCampaignLoader:
|
|||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.helipads:
|
for static in self.helipads:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.helipads.append(
|
closest.helipads.append(
|
||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.factories:
|
for static in self.factories:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.preset_locations.factories.append(
|
closest.preset_locations.factories.append(
|
||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.ammunition_depots:
|
for static in self.ammunition_depots:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.preset_locations.ammunition_depots.append(
|
closest.preset_locations.ammunition_depots.append(
|
||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.strike_targets:
|
for static in self.strike_targets:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.preset_locations.strike_locations.append(
|
closest.preset_locations.strike_locations.append(
|
||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.scenery:
|
for scenery_group in self.scenery:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(scenery_group)
|
||||||
closest.preset_locations.scenery.append(group)
|
closest.preset_locations.scenery.append(scenery_group)
|
||||||
|
|
||||||
def populate_theater(self) -> None:
|
def populate_theater(self) -> None:
|
||||||
for control_point in self.control_points.values():
|
for control_point in self.control_points.values():
|
||||||
@ -504,7 +508,7 @@ class ConflictTheater:
|
|||||||
"""
|
"""
|
||||||
daytime_map: Dict[str, Tuple[int, int]]
|
daytime_map: Dict[str, Tuple[int, int]]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.controlpoints: List[ControlPoint] = []
|
self.controlpoints: List[ControlPoint] = []
|
||||||
self.point_to_ll_transformer = Transformer.from_crs(
|
self.point_to_ll_transformer = Transformer.from_crs(
|
||||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
self.projection_parameters.to_crs(), CRS("WGS84")
|
||||||
@ -536,10 +540,12 @@ class ConflictTheater:
|
|||||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
CRS("WGS84"), self.projection_parameters.to_crs()
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_controlpoint(self, point: ControlPoint):
|
def add_controlpoint(self, point: ControlPoint) -> None:
|
||||||
self.controlpoints.append(point)
|
self.controlpoints.append(point)
|
||||||
|
|
||||||
def find_ground_objects_by_obj_name(self, obj_name):
|
def find_ground_objects_by_obj_name(
|
||||||
|
self, obj_name: str
|
||||||
|
) -> list[TheaterGroundObject[Any]]:
|
||||||
found = []
|
found = []
|
||||||
for cp in self.controlpoints:
|
for cp in self.controlpoints:
|
||||||
for g in cp.ground_objects:
|
for g in cp.ground_objects:
|
||||||
@ -581,12 +587,12 @@ class ConflictTheater:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point:
|
def nearest_land_pos(self, near: Point, extend_dist: int = 50) -> Point:
|
||||||
"""Returns the nearest point inside a land exclusion zone from point
|
"""Returns the nearest point inside a land exclusion zone from point
|
||||||
`extend_dist` determines how far inside the zone the point should be placed"""
|
`extend_dist` determines how far inside the zone the point should be placed"""
|
||||||
if self.is_on_land(point):
|
if self.is_on_land(near):
|
||||||
return point
|
return near
|
||||||
point = geometry.Point(point.x, point.y)
|
point = geometry.Point(near.x, near.y)
|
||||||
nearest_points = []
|
nearest_points = []
|
||||||
if not self.landmap:
|
if not self.landmap:
|
||||||
raise RuntimeError("Landmap not initialized")
|
raise RuntimeError("Landmap not initialized")
|
||||||
@ -698,6 +704,7 @@ class ConflictTheater:
|
|||||||
"Normandy": NormandyTheater,
|
"Normandy": NormandyTheater,
|
||||||
"The Channel": TheChannelTheater,
|
"The Channel": TheChannelTheater,
|
||||||
"Syria": SyriaTheater,
|
"Syria": SyriaTheater,
|
||||||
|
"MarianaIslands": MarianaIslandsTheater,
|
||||||
}
|
}
|
||||||
theater = theaters[data["theater"]]
|
theater = theaters[data["theater"]]
|
||||||
t = theater()
|
t = theater()
|
||||||
@ -856,3 +863,22 @@ class SyriaTheater(ConflictTheater):
|
|||||||
from .syria import PARAMETERS
|
from .syria import PARAMETERS
|
||||||
|
|
||||||
return PARAMETERS
|
return PARAMETERS
|
||||||
|
|
||||||
|
|
||||||
|
class MarianaIslandsTheater(ConflictTheater):
|
||||||
|
terrain = marianaislands.MarianaIslands()
|
||||||
|
overview_image = "marianaislands.gif"
|
||||||
|
|
||||||
|
landmap = load_landmap("resources\\marianaislandslandmap.p")
|
||||||
|
daytime_map = {
|
||||||
|
"dawn": (6, 8),
|
||||||
|
"day": (8, 16),
|
||||||
|
"dusk": (16, 18),
|
||||||
|
"night": (0, 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def projection_parameters(self) -> TransverseMercator:
|
||||||
|
from .marianaislands import PARAMETERS
|
||||||
|
|
||||||
|
return PARAMETERS
|
||||||
|
|||||||
@ -43,6 +43,7 @@ from .missiontarget import MissionTarget
|
|||||||
from .theatergroundobject import (
|
from .theatergroundobject import (
|
||||||
GenericCarrierGroundObject,
|
GenericCarrierGroundObject,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
|
BuildingGroundObject,
|
||||||
)
|
)
|
||||||
from ..dcs.aircrafttype import AircraftType
|
from ..dcs.aircrafttype import AircraftType
|
||||||
from ..dcs.groundunittype import GroundUnitType
|
from ..dcs.groundunittype import GroundUnitType
|
||||||
@ -270,6 +271,9 @@ class ControlPointStatus(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class ControlPoint(MissionTarget, ABC):
|
class ControlPoint(MissionTarget, ABC):
|
||||||
|
# Not sure what distance DCS uses, but assuming it's about 2NM since that's roughly
|
||||||
|
# the distance of the circle on the map.
|
||||||
|
CAPTURE_DISTANCE = nautical_miles(2)
|
||||||
|
|
||||||
position = None # type: Point
|
position = None # type: Point
|
||||||
name = None # type: str
|
name = None # type: str
|
||||||
@ -290,15 +294,15 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
at: db.StartingPosition,
|
at: db.StartingPosition,
|
||||||
size: int,
|
size: int,
|
||||||
importance: float,
|
importance: float,
|
||||||
has_frontline=True,
|
has_frontline: bool = True,
|
||||||
cptype=ControlPointType.AIRBASE,
|
cptype: ControlPointType = ControlPointType.AIRBASE,
|
||||||
):
|
) -> None:
|
||||||
super().__init__(name, position)
|
super().__init__(name, position)
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
self.id = cp_id
|
self.id = cp_id
|
||||||
self.full_name = name
|
self.full_name = name
|
||||||
self.at = at
|
self.at = at
|
||||||
self.connected_objectives: List[TheaterGroundObject] = []
|
self.connected_objectives: List[TheaterGroundObject[Any]] = []
|
||||||
self.preset_locations = PresetLocations()
|
self.preset_locations = PresetLocations()
|
||||||
self.helipads: List[PointWithHeading] = []
|
self.helipads: List[PointWithHeading] = []
|
||||||
|
|
||||||
@ -322,11 +326,11 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
self.target_position: Optional[Point] = None
|
self.target_position: Optional[Point] = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<{__class__}: {self.name}>"
|
return f"<{self.__class__}: {self.name}>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
def ground_objects(self) -> List[TheaterGroundObject[Any]]:
|
||||||
return list(self.connected_objectives)
|
return list(self.connected_objectives)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -334,13 +338,17 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
def heading(self) -> int:
|
def heading(self) -> int:
|
||||||
...
|
...
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_global(self):
|
def is_isolated(self) -> bool:
|
||||||
return not self.connected_points
|
return not self.connected_points
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_global(self) -> bool:
|
||||||
|
return self.is_isolated
|
||||||
|
|
||||||
def transitive_connected_friendly_points(
|
def transitive_connected_friendly_points(
|
||||||
self, seen: Optional[Set[ControlPoint]] = None
|
self, seen: Optional[Set[ControlPoint]] = None
|
||||||
) -> List[ControlPoint]:
|
) -> List[ControlPoint]:
|
||||||
@ -405,21 +413,21 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_carrier(self):
|
def is_carrier(self) -> bool:
|
||||||
"""
|
"""
|
||||||
:return: Whether this control point is an aircraft carrier
|
:return: Whether this control point is an aircraft carrier
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_fleet(self):
|
def is_fleet(self) -> bool:
|
||||||
"""
|
"""
|
||||||
:return: Whether this control point is a boat (mobile)
|
:return: Whether this control point is a boat (mobile)
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_lha(self):
|
def is_lha(self) -> bool:
|
||||||
"""
|
"""
|
||||||
:return: Whether this control point is an LHA
|
:return: Whether this control point is an LHA
|
||||||
"""
|
"""
|
||||||
@ -439,7 +447,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def total_aircraft_parking(self):
|
def total_aircraft_parking(self) -> int:
|
||||||
"""
|
"""
|
||||||
:return: The maximum number of aircraft that can be stored in this
|
:return: The maximum number of aircraft that can be stored in this
|
||||||
control point
|
control point
|
||||||
@ -471,7 +479,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# TODO: Should be naval specific.
|
# TODO: Should be naval specific.
|
||||||
def get_carrier_group_name(self):
|
def get_carrier_group_name(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Get the carrier group name if the airbase is a carrier
|
Get the carrier group name if the airbase is a carrier
|
||||||
:return: Carrier group name
|
:return: Carrier group name
|
||||||
@ -497,10 +505,12 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
def is_connected(self, to) -> bool:
|
def is_connected(self, to: ControlPoint) -> bool:
|
||||||
return to in self.connected_points
|
return to in self.connected_points
|
||||||
|
|
||||||
def find_ground_objects_by_obj_name(self, obj_name):
|
def find_ground_objects_by_obj_name(
|
||||||
|
self, obj_name: str
|
||||||
|
) -> list[TheaterGroundObject[Any]]:
|
||||||
found = []
|
found = []
|
||||||
for g in self.ground_objects:
|
for g in self.ground_objects:
|
||||||
if g.obj_name == obj_name:
|
if g.obj_name == obj_name:
|
||||||
@ -522,7 +532,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
f"vehicles have been captured and sold for ${total}M."
|
f"vehicles have been captured and sold for ${total}M."
|
||||||
)
|
)
|
||||||
|
|
||||||
def retreat_ground_units(self, game: Game):
|
def retreat_ground_units(self, game: Game) -> None:
|
||||||
# When there are multiple valid destinations, deliver units to whichever
|
# When there are multiple valid destinations, deliver units to whichever
|
||||||
# base is least defended first. The closest approximation of unit
|
# base is least defended first. The closest approximation of unit
|
||||||
# strength we have is price
|
# strength we have is price
|
||||||
@ -596,7 +606,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
def capture(self, game: Game, for_player: bool) -> None:
|
def capture(self, game: Game, for_player: bool) -> None:
|
||||||
self.pending_unit_deliveries.refund_all(game)
|
self.pending_unit_deliveries.refund_all(game.coalition_for(for_player))
|
||||||
self.retreat_ground_units(game)
|
self.retreat_ground_units(game)
|
||||||
self.retreat_air_units(game)
|
self.retreat_air_units(game)
|
||||||
self.depopulate_uncapturable_tgos()
|
self.depopulate_uncapturable_tgos()
|
||||||
@ -613,11 +623,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
...
|
...
|
||||||
|
|
||||||
def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
|
def aircraft_transferring(self, game: Game) -> dict[AircraftType, int]:
|
||||||
if self.captured:
|
ato = game.coalition_for(self.captured).ato
|
||||||
ato = game.blue_ato
|
|
||||||
else:
|
|
||||||
ato = game.red_ato
|
|
||||||
|
|
||||||
transferring: defaultdict[AircraftType, int] = defaultdict(int)
|
transferring: defaultdict[AircraftType, int] = defaultdict(int)
|
||||||
for package in ato.packages:
|
for package in ato.packages:
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
@ -725,30 +731,51 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return self.captured != other.captured
|
return self.captured != other.captured
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def frontline_unit_count_limit(self) -> int:
|
def deployable_front_line_units(self) -> int:
|
||||||
|
return self.deployable_front_line_units_with(self.active_ammo_depots_count)
|
||||||
|
|
||||||
|
def deployable_front_line_units_with(self, ammo_depot_count: int) -> int:
|
||||||
|
return min(
|
||||||
|
self.front_line_capacity_with(ammo_depot_count), self.base.total_armor
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def front_line_capacity_with(cls, ammo_depot_count: int) -> int:
|
||||||
return (
|
return (
|
||||||
FREE_FRONTLINE_UNIT_SUPPLY
|
FREE_FRONTLINE_UNIT_SUPPLY
|
||||||
+ self.active_ammo_depots_count * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION
|
+ ammo_depot_count * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frontline_unit_count_limit(self) -> int:
|
||||||
|
return self.front_line_capacity_with(self.active_ammo_depots_count)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_ammo_depots(self) -> Iterator[BuildingGroundObject]:
|
||||||
|
for tgo in self.connected_objectives:
|
||||||
|
if not tgo.is_ammo_depot:
|
||||||
|
continue
|
||||||
|
assert isinstance(tgo, BuildingGroundObject)
|
||||||
|
yield tgo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_ammo_depots(self) -> Iterator[BuildingGroundObject]:
|
||||||
|
for tgo in self.all_ammo_depots:
|
||||||
|
if not tgo.is_dead:
|
||||||
|
yield tgo
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_ammo_depots_count(self) -> int:
|
def active_ammo_depots_count(self) -> int:
|
||||||
"""Return the number of available ammo depots"""
|
"""Return the number of available ammo depots"""
|
||||||
return len(
|
return len(list(self.active_ammo_depots))
|
||||||
[
|
|
||||||
obj
|
|
||||||
for obj in self.connected_objectives
|
|
||||||
if obj.category == "ammo" and not obj.is_dead
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_ammo_depots_count(self) -> int:
|
def total_ammo_depots_count(self) -> int:
|
||||||
"""Return the number of ammo depots, including dead ones"""
|
"""Return the number of ammo depots, including dead ones"""
|
||||||
return len([obj for obj in self.connected_objectives if obj.category == "ammo"])
|
return len(list(self.all_ammo_depots))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -764,8 +791,8 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
class Airfield(ControlPoint):
|
class Airfield(ControlPoint):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, airport: Airport, size: int, importance: float, has_frontline=True
|
self, airport: Airport, size: int, importance: float, has_frontline: bool = True
|
||||||
):
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
airport.id,
|
airport.id,
|
||||||
airport.name,
|
airport.name,
|
||||||
@ -879,9 +906,12 @@ class NavalControlPoint(ControlPoint, ABC):
|
|||||||
def heading(self) -> int:
|
def heading(self) -> int:
|
||||||
return 0 # TODO compute heading
|
return 0 # TODO compute heading
|
||||||
|
|
||||||
def find_main_tgo(self) -> TheaterGroundObject:
|
def find_main_tgo(self) -> GenericCarrierGroundObject:
|
||||||
for g in self.ground_objects:
|
for g in self.ground_objects:
|
||||||
if g.dcs_identifier in ["CARRIER", "LHA"]:
|
if isinstance(g, GenericCarrierGroundObject) and g.dcs_identifier in [
|
||||||
|
"CARRIER",
|
||||||
|
"LHA",
|
||||||
|
]:
|
||||||
return g
|
return g
|
||||||
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
|
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
|
||||||
|
|
||||||
@ -960,7 +990,7 @@ class Carrier(NavalControlPoint):
|
|||||||
raise RuntimeError("Carriers cannot be captured")
|
raise RuntimeError("Carriers cannot be captured")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_carrier(self):
|
def is_carrier(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Iterator, List, Tuple
|
from typing import Iterator, List, Tuple, Any
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
|
|
||||||
@ -66,12 +66,31 @@ class FrontLine(MissionTarget):
|
|||||||
self.segments: List[FrontLineSegment] = [
|
self.segments: List[FrontLineSegment] = [
|
||||||
FrontLineSegment(a, b) for a, b in pairwise(route)
|
FrontLineSegment(a, b) for a, b in pairwise(route)
|
||||||
]
|
]
|
||||||
self.name = f"Front line {blue_point}/{red_point}"
|
super().__init__(
|
||||||
|
f"Front line {blue_point}/{red_point}",
|
||||||
|
self.point_from_a(self._position_distance),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other: Any) -> bool:
|
||||||
|
if not isinstance(other, FrontLine):
|
||||||
|
return False
|
||||||
|
return (self.blue_cp, self.red_cp) == (other.blue_cp, other.red_cp)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((self.blue_cp, self.red_cp))
|
||||||
|
|
||||||
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
|
self.__dict__.update(state)
|
||||||
|
if not hasattr(self, "position"):
|
||||||
|
self.position = self.point_from_a(self._position_distance)
|
||||||
|
|
||||||
|
def control_point_friendly_to(self, player: bool) -> ControlPoint:
|
||||||
|
if player:
|
||||||
|
return self.blue_cp
|
||||||
|
return self.red_cp
|
||||||
|
|
||||||
def control_point_hostile_to(self, player: bool) -> ControlPoint:
|
def control_point_hostile_to(self, player: bool) -> ControlPoint:
|
||||||
if player:
|
return self.control_point_friendly_to(not player)
|
||||||
return self.red_cp
|
|
||||||
return self.blue_cp
|
|
||||||
|
|
||||||
def is_friendly(self, to_player: bool) -> bool:
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
"""Returns True if the objective is in friendly territory."""
|
"""Returns True if the objective is in friendly territory."""
|
||||||
@ -87,14 +106,6 @@ class FrontLine(MissionTarget):
|
|||||||
]
|
]
|
||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
@property
|
|
||||||
def position(self):
|
|
||||||
"""
|
|
||||||
The position where the conflict should occur
|
|
||||||
according to the current strength of each control point.
|
|
||||||
"""
|
|
||||||
return self.point_from_a(self._position_distance)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def points(self) -> Iterator[Point]:
|
def points(self) -> Iterator[Point]:
|
||||||
yield self.segments[0].point_a
|
yield self.segments[0].point_a
|
||||||
@ -107,12 +118,12 @@ class FrontLine(MissionTarget):
|
|||||||
return self.blue_cp, self.red_cp
|
return self.blue_cp, self.red_cp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attack_distance(self):
|
def attack_distance(self) -> float:
|
||||||
"""The total distance of all segments"""
|
"""The total distance of all segments"""
|
||||||
return sum(i.attack_distance for i in self.segments)
|
return sum(i.attack_distance for i in self.segments)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attack_heading(self):
|
def attack_heading(self) -> float:
|
||||||
"""The heading of the active attack segment from player to enemy control point"""
|
"""The heading of the active attack segment from player to enemy control point"""
|
||||||
return self.active_segment.attack_heading
|
return self.active_segment.attack_heading
|
||||||
|
|
||||||
@ -149,6 +160,9 @@ class FrontLine(MissionTarget):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
remaining_dist -= segment.attack_distance
|
remaining_dist -= segment.attack_distance
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Could not find front line point {distance} from {self.blue_cp}"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _position_distance(self) -> float:
|
def _position_distance(self) -> float:
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class Landmap:
|
|||||||
exclusion_zones: MultiPolygon
|
exclusion_zones: MultiPolygon
|
||||||
sea_zones: MultiPolygon
|
sea_zones: MultiPolygon
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self) -> None:
|
||||||
if not self.inclusion_zones.is_valid:
|
if not self.inclusion_zones.is_valid:
|
||||||
raise RuntimeError("Inclusion zones not valid")
|
raise RuntimeError("Inclusion zones not valid")
|
||||||
if not self.exclusion_zones.is_valid:
|
if not self.exclusion_zones.is_valid:
|
||||||
@ -36,13 +36,5 @@ def load_landmap(filename: str) -> Optional[Landmap]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]):
|
def poly_contains(x: float, y: float, poly: Union[MultiPolygon, Polygon]) -> bool:
|
||||||
return poly.contains(geometry.Point(x, y))
|
return poly.contains(geometry.Point(x, y))
|
||||||
|
|
||||||
|
|
||||||
def poly_centroid(poly) -> Tuple[float, float]:
|
|
||||||
x_list = [vertex[0] for vertex in poly]
|
|
||||||
y_list = [vertex[1] for vertex in poly]
|
|
||||||
x = sum(x_list) / len(poly)
|
|
||||||
y = sum(y_list) / len(poly)
|
|
||||||
return (x, y)
|
|
||||||
|
|||||||
8
game/theater/marianaislands.py
Normal file
8
game/theater/marianaislands.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from game.theater.projections import TransverseMercator
|
||||||
|
|
||||||
|
PARAMETERS = TransverseMercator(
|
||||||
|
central_meridian=147,
|
||||||
|
false_easting=238417.99999989968,
|
||||||
|
false_northing=-1491840.000000048,
|
||||||
|
scale_factor=0.9996,
|
||||||
|
)
|
||||||
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import Sequence
|
||||||
from typing import Iterator, TYPE_CHECKING, List, Union
|
from typing import Iterator, TYPE_CHECKING, List, Union
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
@ -20,7 +21,7 @@ class MissionTarget:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.position = position
|
self.position = position
|
||||||
|
|
||||||
def distance_to(self, other: MissionTarget) -> int:
|
def distance_to(self, other: MissionTarget) -> float:
|
||||||
"""Computes the distance to the given mission target."""
|
"""Computes the distance to the given mission target."""
|
||||||
return self.position.distance_to_point(other.position)
|
return self.position.distance_to_point(other.position)
|
||||||
|
|
||||||
@ -45,5 +46,5 @@ class MissionTarget:
|
|||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from dcs.mapping import Point
|
|||||||
from dcs.task import CAP, CAS, PinpointStrike
|
from dcs.task import CAP, CAS, PinpointStrike
|
||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
|
|
||||||
from game import Game, db
|
from game import Game
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from game.scenery_group import SceneryGroup
|
from game.scenery_group import SceneryGroup
|
||||||
from game.theater import Carrier, Lha, PointWithHeading
|
from game.theater import Carrier, Lha, PointWithHeading
|
||||||
@ -171,14 +171,11 @@ class ControlPointGroundObjectGenerator:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def faction_name(self) -> str:
|
def faction_name(self) -> str:
|
||||||
if self.control_point.captured:
|
return self.faction.name
|
||||||
return self.game.player_faction.name
|
|
||||||
else:
|
|
||||||
return self.game.enemy_faction.name
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def faction(self) -> Faction:
|
def faction(self) -> Faction:
|
||||||
return db.FACTIONS[self.faction_name]
|
return self.game.coalition_for(self.control_point.captured).faction
|
||||||
|
|
||||||
def generate(self) -> bool:
|
def generate(self) -> bool:
|
||||||
self.control_point.connected_objectives = []
|
self.control_point.connected_objectives = []
|
||||||
|
|||||||
@ -2,13 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from typing import Iterator, List, TYPE_CHECKING, Union
|
from abc import ABC
|
||||||
|
from collections import Sequence
|
||||||
|
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
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from dcs.unitgroup import Group
|
from dcs.unitgroup import ShipGroup, VehicleGroup
|
||||||
from dcs.unittype import VehicleType
|
|
||||||
|
|
||||||
from .. import db
|
from .. import db
|
||||||
from ..data.radar_db import (
|
from ..data.radar_db import (
|
||||||
@ -47,7 +48,10 @@ NAME_BY_CATEGORY = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TheaterGroundObject(MissionTarget):
|
GroupT = TypeVar("GroupT", ShipGroup, VehicleGroup)
|
||||||
|
|
||||||
|
|
||||||
|
class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@ -66,7 +70,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
self.control_point = control_point
|
self.control_point = control_point
|
||||||
self.dcs_identifier = dcs_identifier
|
self.dcs_identifier = dcs_identifier
|
||||||
self.sea_object = sea_object
|
self.sea_object = sea_object
|
||||||
self.groups: List[Group] = []
|
self.groups: List[GroupT] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_dead(self) -> bool:
|
def is_dead(self) -> bool:
|
||||||
@ -147,7 +151,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
|
def _max_range_of_type(self, group: GroupT, range_type: str) -> Distance:
|
||||||
if not self.might_have_aa:
|
if not self.might_have_aa:
|
||||||
return meters(0)
|
return meters(0)
|
||||||
|
|
||||||
@ -168,15 +172,19 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
def max_detection_range(self) -> Distance:
|
def max_detection_range(self) -> Distance:
|
||||||
return max(self.detection_range(g) for g in self.groups)
|
return max(self.detection_range(g) for g in self.groups)
|
||||||
|
|
||||||
def detection_range(self, group: Group) -> Distance:
|
def detection_range(self, group: GroupT) -> Distance:
|
||||||
return self._max_range_of_type(group, "detection_range")
|
return self._max_range_of_type(group, "detection_range")
|
||||||
|
|
||||||
def max_threat_range(self) -> Distance:
|
def max_threat_range(self) -> Distance:
|
||||||
return max(self.threat_range(g) for g in self.groups)
|
return max(self.threat_range(g) for g in self.groups)
|
||||||
|
|
||||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance:
|
||||||
return self._max_range_of_type(group, "threat_range")
|
return self._max_range_of_type(group, "threat_range")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ammo_depot(self) -> bool:
|
||||||
|
return self.category == "ammo"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_factory(self) -> bool:
|
def is_factory(self) -> bool:
|
||||||
return self.category == "factory"
|
return self.category == "factory"
|
||||||
@ -187,7 +195,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||||
return self.units
|
return self.units
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -206,7 +214,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class BuildingGroundObject(TheaterGroundObject):
|
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@ -217,7 +225,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
|||||||
heading: int,
|
heading: int,
|
||||||
control_point: ControlPoint,
|
control_point: ControlPoint,
|
||||||
dcs_identifier: str,
|
dcs_identifier: str,
|
||||||
is_fob_structure=False,
|
is_fob_structure: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
@ -253,13 +261,17 @@ class BuildingGroundObject(TheaterGroundObject):
|
|||||||
def kill(self) -> None:
|
def kill(self) -> None:
|
||||||
self._dead = True
|
self._dead = True
|
||||||
|
|
||||||
def iter_building_group(self) -> Iterator[TheaterGroundObject]:
|
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
|
||||||
@ -338,7 +350,7 @@ class FactoryGroundObject(BuildingGroundObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class NavalGroundObject(TheaterGroundObject):
|
class NavalGroundObject(TheaterGroundObject[ShipGroup]):
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
@ -407,7 +419,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
|||||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||||
|
|
||||||
|
|
||||||
class MissileSiteGroundObject(TheaterGroundObject):
|
class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -431,14 +443,14 @@ class MissileSiteGroundObject(TheaterGroundObject):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class CoastalSiteGroundObject(TheaterGroundObject):
|
class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
group_id: int,
|
group_id: int,
|
||||||
position: Point,
|
position: Point,
|
||||||
control_point: ControlPoint,
|
control_point: ControlPoint,
|
||||||
heading,
|
heading: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
@ -460,10 +472,19 @@ class CoastalSiteGroundObject(TheaterGroundObject):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class IadsGroundObject(TheaterGroundObject[VehicleGroup], ABC):
|
||||||
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
if not self.is_friendly(for_player):
|
||||||
|
yield FlightType.DEAD
|
||||||
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
|
||||||
# The SamGroundObject represents all type of AA
|
# The SamGroundObject represents all type of AA
|
||||||
# The TGO can have multiple types of units (AAA,SAM,Support...)
|
# The TGO can have multiple types of units (AAA,SAM,Support...)
|
||||||
# Differentiation can be made during generation with the airdefensegroupgenerator
|
# Differentiation can be made during generation with the airdefensegroupgenerator
|
||||||
class SamGroundObject(TheaterGroundObject):
|
class SamGroundObject(IadsGroundObject):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@ -488,39 +509,35 @@ class SamGroundObject(TheaterGroundObject):
|
|||||||
if not self.is_friendly(for_player):
|
if not self.is_friendly(for_player):
|
||||||
yield FlightType.DEAD
|
yield FlightType.DEAD
|
||||||
yield FlightType.SEAD
|
yield FlightType.SEAD
|
||||||
yield from super().mission_types(for_player)
|
for mission_type in super().mission_types(for_player):
|
||||||
|
# We yielded this ourselves to move it to the top of the list. Don't yield
|
||||||
|
# it twice.
|
||||||
|
if mission_type is not FlightType.DEAD:
|
||||||
|
yield mission_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def might_have_aa(self) -> bool:
|
def might_have_aa(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
def threat_range(self, group: VehicleGroup, radar_only: bool = False) -> Distance:
|
||||||
max_non_radar = meters(0)
|
max_non_radar = meters(0)
|
||||||
live_trs = set()
|
live_trs = set()
|
||||||
max_telar_range = meters(0)
|
max_telar_range = meters(0)
|
||||||
launchers = set()
|
launchers = set()
|
||||||
for unit in group.units:
|
for unit in group.units:
|
||||||
unit_type = db.unit_type_from_name(unit.type)
|
unit_type = db.vehicle_type_from_name(unit.type)
|
||||||
if unit_type is None or not issubclass(unit_type, VehicleType):
|
|
||||||
continue
|
|
||||||
if unit_type in TRACK_RADARS:
|
if unit_type in TRACK_RADARS:
|
||||||
live_trs.add(unit_type)
|
live_trs.add(unit_type)
|
||||||
elif unit_type in TELARS:
|
elif unit_type in TELARS:
|
||||||
max_telar_range = max(
|
max_telar_range = max(max_telar_range, meters(unit_type.threat_range))
|
||||||
max_telar_range, meters(getattr(unit_type, "threat_range", 0))
|
|
||||||
)
|
|
||||||
elif unit_type in LAUNCHER_TRACKER_PAIRS:
|
elif unit_type in LAUNCHER_TRACKER_PAIRS:
|
||||||
launchers.add(unit_type)
|
launchers.add(unit_type)
|
||||||
else:
|
else:
|
||||||
max_non_radar = max(
|
max_non_radar = max(max_non_radar, meters(unit_type.threat_range))
|
||||||
max_non_radar, meters(getattr(unit_type, "threat_range", 0))
|
|
||||||
)
|
|
||||||
max_tel_range = meters(0)
|
max_tel_range = meters(0)
|
||||||
for launcher in launchers:
|
for launcher in launchers:
|
||||||
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
|
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
|
||||||
max_tel_range = max(
|
max_tel_range = max(max_tel_range, meters(unit_type.threat_range))
|
||||||
max_tel_range, meters(getattr(launcher, "threat_range"))
|
|
||||||
)
|
|
||||||
if radar_only:
|
if radar_only:
|
||||||
return max(max_tel_range, max_telar_range)
|
return max(max_tel_range, max_telar_range)
|
||||||
else:
|
else:
|
||||||
@ -535,7 +552,7 @@ class SamGroundObject(TheaterGroundObject):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class VehicleGroupGroundObject(TheaterGroundObject):
|
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@ -563,7 +580,7 @@ class VehicleGroupGroundObject(TheaterGroundObject):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class EwrGroundObject(TheaterGroundObject):
|
class EwrGroundObject(IadsGroundObject):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@ -588,13 +605,6 @@ class EwrGroundObject(TheaterGroundObject):
|
|||||||
# Use Group Id and uppercase EWR
|
# Use Group Id and uppercase EWR
|
||||||
return f"{self.faction_color}|EWR|{self.group_id}"
|
return f"{self.faction_color}|EWR|{self.group_id}"
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
|
||||||
from gen.flights.flight import FlightType
|
|
||||||
|
|
||||||
if not self.is_friendly(for_player):
|
|
||||||
yield FlightType.DEAD
|
|
||||||
yield from super().mission_types(for_player)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def might_have_aa(self) -> bool:
|
def might_have_aa(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from functools import singledispatchmethod
|
from functools import singledispatchmethod
|
||||||
from typing import Optional, TYPE_CHECKING, Union, Iterable
|
from typing import Optional, TYPE_CHECKING, Union, Iterable, Any
|
||||||
|
|
||||||
from dcs.mapping import Point as DcsPoint
|
from dcs.mapping import Point as DcsPoint
|
||||||
from shapely.geometry import (
|
from shapely.geometry import (
|
||||||
@ -13,7 +13,8 @@ from shapely.geometry import (
|
|||||||
from shapely.geometry.base import BaseGeometry
|
from shapely.geometry.base import BaseGeometry
|
||||||
from shapely.ops import nearest_points, unary_union
|
from shapely.ops import nearest_points, unary_union
|
||||||
|
|
||||||
from game.theater import ControlPoint, MissionTarget
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
|
||||||
from game.utils import Distance, meters, nautical_miles
|
from game.utils import Distance, meters, nautical_miles
|
||||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
from gen.flights.flight import Flight, FlightWaypoint
|
from gen.flights.flight import Flight, FlightWaypoint
|
||||||
@ -27,7 +28,10 @@ ThreatPoly = Union[MultiPolygon, Polygon]
|
|||||||
|
|
||||||
class ThreatZones:
|
class ThreatZones:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
|
self,
|
||||||
|
airbases: ThreatPoly,
|
||||||
|
air_defenses: ThreatPoly,
|
||||||
|
radar_sam_threats: ThreatPoly,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.airbases = airbases
|
self.airbases = airbases
|
||||||
self.air_defenses = air_defenses
|
self.air_defenses = air_defenses
|
||||||
@ -44,8 +48,10 @@ class ThreatZones:
|
|||||||
boundary = self.closest_boundary(point)
|
boundary = self.closest_boundary(point)
|
||||||
return meters(boundary.distance_to_point(point))
|
return meters(boundary.distance_to_point(point))
|
||||||
|
|
||||||
|
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||||
|
# definitions. The implementation methods are all typed, so should be fine.
|
||||||
@singledispatchmethod
|
@singledispatchmethod
|
||||||
def threatened(self, position) -> bool:
|
def threatened(self, position) -> bool: # type: ignore
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@threatened.register
|
@threatened.register
|
||||||
@ -61,8 +67,10 @@ class ThreatZones:
|
|||||||
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
|
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||||
|
# definitions. The implementation methods are all typed, so should be fine.
|
||||||
@singledispatchmethod
|
@singledispatchmethod
|
||||||
def threatened_by_aircraft(self, target) -> bool:
|
def threatened_by_aircraft(self, target) -> bool: # type: ignore
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@threatened_by_aircraft.register
|
@threatened_by_aircraft.register
|
||||||
@ -75,6 +83,10 @@ class ThreatZones:
|
|||||||
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@threatened_by_aircraft.register
|
||||||
|
def _threatened_by_aircraft_mission_target(self, target: MissionTarget) -> bool:
|
||||||
|
return self.threatened_by_aircraft(self.dcs_to_shapely_point(target.position))
|
||||||
|
|
||||||
def waypoints_threatened_by_aircraft(
|
def waypoints_threatened_by_aircraft(
|
||||||
self, waypoints: Iterable[FlightWaypoint]
|
self, waypoints: Iterable[FlightWaypoint]
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@ -82,8 +94,10 @@ class ThreatZones:
|
|||||||
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
|
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||||
|
# definitions. The implementation methods are all typed, so should be fine.
|
||||||
@singledispatchmethod
|
@singledispatchmethod
|
||||||
def threatened_by_air_defense(self, target) -> bool:
|
def threatened_by_air_defense(self, target) -> bool: # type: ignore
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@threatened_by_air_defense.register
|
@threatened_by_air_defense.register
|
||||||
@ -102,8 +116,10 @@ class ThreatZones:
|
|||||||
self.dcs_to_shapely_point(target.position)
|
self.dcs_to_shapely_point(target.position)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||||
|
# definitions. The implementation methods are all typed, so should be fine.
|
||||||
@singledispatchmethod
|
@singledispatchmethod
|
||||||
def threatened_by_radar_sam(self, target) -> bool:
|
def threatened_by_radar_sam(self, target) -> bool: # type: ignore
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@threatened_by_radar_sam.register
|
@threatened_by_radar_sam.register
|
||||||
@ -134,8 +150,9 @@ class ThreatZones:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def barcap_threat_range(cls, game: Game, control_point: ControlPoint) -> Distance:
|
def barcap_threat_range(
|
||||||
doctrine = game.faction_for(control_point.captured).doctrine
|
cls, doctrine: Doctrine, control_point: ControlPoint
|
||||||
|
) -> Distance:
|
||||||
cap_threat_range = (
|
cap_threat_range = (
|
||||||
doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
|
doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
|
||||||
)
|
)
|
||||||
@ -174,33 +191,59 @@ class ThreatZones:
|
|||||||
"""
|
"""
|
||||||
air_threats = []
|
air_threats = []
|
||||||
air_defenses = []
|
air_defenses = []
|
||||||
radar_sam_threats = []
|
for control_point in game.theater.control_points_for(player):
|
||||||
for control_point in game.theater.controlpoints:
|
air_threats.append(control_point)
|
||||||
if control_point.captured != player:
|
air_defenses.extend(control_point.ground_objects)
|
||||||
continue
|
|
||||||
if control_point.runway_is_operational():
|
|
||||||
point = ShapelyPoint(control_point.position.x, control_point.position.y)
|
|
||||||
cap_threat_range = cls.barcap_threat_range(game, control_point)
|
|
||||||
air_threats.append(point.buffer(cap_threat_range.meters))
|
|
||||||
|
|
||||||
for tgo in control_point.ground_objects:
|
return cls.for_threats(
|
||||||
for group in tgo.groups:
|
game.faction_for(player).doctrine, air_threats, air_defenses
|
||||||
threat_range = tgo.threat_range(group)
|
)
|
||||||
# Any system with a shorter range than this is not worth
|
|
||||||
# even avoiding.
|
@classmethod
|
||||||
if threat_range > nautical_miles(3):
|
def for_threats(
|
||||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
cls,
|
||||||
threat_zone = point.buffer(threat_range.meters)
|
doctrine: Doctrine,
|
||||||
air_defenses.append(threat_zone)
|
barcap_locations: Iterable[ControlPoint],
|
||||||
radar_threat_range = tgo.threat_range(group, radar_only=True)
|
air_defenses: Iterable[TheaterGroundObject[Any]],
|
||||||
if radar_threat_range > nautical_miles(3):
|
) -> ThreatZones:
|
||||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
"""Generates the threat zones projected by the given locations.
|
||||||
threat_zone = point.buffer(threat_range.meters)
|
|
||||||
radar_sam_threats.append(threat_zone)
|
Args:
|
||||||
|
doctrine: The doctrine of the owning coalition.
|
||||||
|
barcap_locations: The locations that will be considered for BARCAP planning.
|
||||||
|
air_defenses: TGOs that may have air defenses.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The threat zones projected by the given locations. If the threat zone
|
||||||
|
belongs to the player, it is the zone that will be avoided by the enemy and
|
||||||
|
vice versa.
|
||||||
|
"""
|
||||||
|
air_threats = []
|
||||||
|
air_defense_threats = []
|
||||||
|
radar_sam_threats = []
|
||||||
|
for barcap in barcap_locations:
|
||||||
|
point = ShapelyPoint(barcap.position.x, barcap.position.y)
|
||||||
|
cap_threat_range = cls.barcap_threat_range(doctrine, barcap)
|
||||||
|
air_threats.append(point.buffer(cap_threat_range.meters))
|
||||||
|
|
||||||
|
for tgo in air_defenses:
|
||||||
|
for group in tgo.groups:
|
||||||
|
threat_range = tgo.threat_range(group)
|
||||||
|
# Any system with a shorter range than this is not worth
|
||||||
|
# even avoiding.
|
||||||
|
if threat_range > nautical_miles(3):
|
||||||
|
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||||
|
threat_zone = point.buffer(threat_range.meters)
|
||||||
|
air_defense_threats.append(threat_zone)
|
||||||
|
radar_threat_range = tgo.threat_range(group, radar_only=True)
|
||||||
|
if radar_threat_range > nautical_miles(3):
|
||||||
|
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||||
|
threat_zone = point.buffer(threat_range.meters)
|
||||||
|
radar_sam_threats.append(threat_zone)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
airbases=unary_union(air_threats),
|
airbases=unary_union(air_threats),
|
||||||
air_defenses=unary_union(air_defenses),
|
air_defenses=unary_union(air_defense_threats),
|
||||||
radar_sam_threats=unary_union(radar_sam_threats),
|
radar_sam_threats=unary_union(radar_sam_threats),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -130,7 +130,10 @@ class TransferOrder:
|
|||||||
def kill_unit(self, unit_type: GroundUnitType) -> None:
|
def kill_unit(self, unit_type: GroundUnitType) -> None:
|
||||||
if unit_type not in self.units or not self.units[unit_type]:
|
if unit_type not in self.units or not self.units[unit_type]:
|
||||||
raise KeyError(f"{self} has no {unit_type} remaining")
|
raise KeyError(f"{self} has no {unit_type} remaining")
|
||||||
self.units[unit_type] -= 1
|
if self.units[unit_type] == 1:
|
||||||
|
del self.units[unit_type]
|
||||||
|
else:
|
||||||
|
self.units[unit_type] -= 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
@ -313,7 +316,9 @@ class AirliftPlanner:
|
|||||||
capacity = flight_size * capacity_each
|
capacity = flight_size * capacity_each
|
||||||
|
|
||||||
if capacity < self.transfer.size:
|
if capacity < self.transfer.size:
|
||||||
transfer = self.game.transfers.split_transfer(self.transfer, capacity)
|
transfer = self.game.coalition_for(
|
||||||
|
self.for_player
|
||||||
|
).transfers.split_transfer(self.transfer, capacity)
|
||||||
else:
|
else:
|
||||||
transfer = self.transfer
|
transfer = self.transfer
|
||||||
|
|
||||||
@ -335,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
|
||||||
@ -516,14 +523,14 @@ class TransportMap(Generic[TransportType]):
|
|||||||
yield from destination_dict.values()
|
yield from destination_dict.values()
|
||||||
|
|
||||||
|
|
||||||
class ConvoyMap(TransportMap):
|
class ConvoyMap(TransportMap[Convoy]):
|
||||||
def create_transport(
|
def create_transport(
|
||||||
self, origin: ControlPoint, destination: ControlPoint
|
self, origin: ControlPoint, destination: ControlPoint
|
||||||
) -> Convoy:
|
) -> Convoy:
|
||||||
return Convoy(origin, destination)
|
return Convoy(origin, destination)
|
||||||
|
|
||||||
|
|
||||||
class CargoShipMap(TransportMap):
|
class CargoShipMap(TransportMap[CargoShip]):
|
||||||
def create_transport(
|
def create_transport(
|
||||||
self, origin: ControlPoint, destination: ControlPoint
|
self, origin: ControlPoint, destination: ControlPoint
|
||||||
) -> CargoShip:
|
) -> CargoShip:
|
||||||
@ -531,8 +538,9 @@ class CargoShipMap(TransportMap):
|
|||||||
|
|
||||||
|
|
||||||
class PendingTransfers:
|
class PendingTransfers:
|
||||||
def __init__(self, game: Game) -> None:
|
def __init__(self, game: Game, player: bool) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
|
self.player = player
|
||||||
self.convoys = ConvoyMap()
|
self.convoys = ConvoyMap()
|
||||||
self.cargo_ships = CargoShipMap()
|
self.cargo_ships = CargoShipMap()
|
||||||
self.pending_transfers: List[TransferOrder] = []
|
self.pending_transfers: List[TransferOrder] = []
|
||||||
@ -589,8 +597,14 @@ class PendingTransfers:
|
|||||||
self.pending_transfers.append(new_transfer)
|
self.pending_transfers.append(new_transfer)
|
||||||
return new_transfer
|
return new_transfer
|
||||||
|
|
||||||
|
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||||
|
# definitions. The implementation methods are all typed, so should be fine.
|
||||||
@singledispatchmethod
|
@singledispatchmethod
|
||||||
def cancel_transport(self, transport, transfer: TransferOrder) -> None:
|
def cancel_transport( # type: ignore
|
||||||
|
self,
|
||||||
|
transport,
|
||||||
|
transfer: TransferOrder,
|
||||||
|
) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cancel_transport.register
|
@cancel_transport.register
|
||||||
@ -600,7 +614,7 @@ class PendingTransfers:
|
|||||||
flight = transport.flight
|
flight = transport.flight
|
||||||
flight.package.remove_flight(flight)
|
flight.package.remove_flight(flight)
|
||||||
if not flight.package.flights:
|
if not flight.package.flights:
|
||||||
self.game.ato_for(transport.player_owned).remove_package(flight.package)
|
self.game.ato_for(self.player).remove_package(flight.package)
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
self.game.aircraft_inventory.return_from_flight(flight)
|
||||||
flight.clear_roster()
|
flight.clear_roster()
|
||||||
|
|
||||||
@ -638,7 +652,7 @@ class PendingTransfers:
|
|||||||
self.arrange_transport(transfer)
|
self.arrange_transport(transfer)
|
||||||
|
|
||||||
def order_airlift_assets(self) -> None:
|
def order_airlift_assets(self) -> None:
|
||||||
for control_point in self.game.theater.controlpoints:
|
for control_point in self.game.theater.control_points_for(self.player):
|
||||||
if self.game.air_wing_for(control_point.captured).can_auto_plan(
|
if self.game.air_wing_for(control_point.captured).can_auto_plan(
|
||||||
FlightType.TRANSPORT
|
FlightType.TRANSPORT
|
||||||
):
|
):
|
||||||
@ -673,7 +687,7 @@ class PendingTransfers:
|
|||||||
# aesthetic.
|
# aesthetic.
|
||||||
gap += 1
|
gap += 1
|
||||||
|
|
||||||
self.game.procurement_requests_for(player=control_point.captured).append(
|
self.game.procurement_requests_for(self.player).append(
|
||||||
AircraftProcurementRequest(
|
AircraftProcurementRequest(
|
||||||
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
|
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
|
||||||
)
|
)
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Optional, TYPE_CHECKING, Any
|
from typing import Optional, TYPE_CHECKING, Any
|
||||||
|
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
|
from .coalition import Coalition
|
||||||
from .dcs.groundunittype import GroundUnitType
|
from .dcs.groundunittype import GroundUnitType
|
||||||
from .dcs.unittype import UnitType
|
from .dcs.unittype import UnitType
|
||||||
from .theater.transitnetwork import (
|
from .theater.transitnetwork import (
|
||||||
@ -28,62 +29,61 @@ class PendingUnitDeliveries:
|
|||||||
self.destination = destination
|
self.destination = destination
|
||||||
|
|
||||||
# Maps unit type to order quantity.
|
# Maps unit type to order quantity.
|
||||||
self.units: dict[UnitType, int] = defaultdict(int)
|
self.units: dict[UnitType[Any], int] = defaultdict(int)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Pending delivery to {self.destination}"
|
return f"Pending delivery to {self.destination}"
|
||||||
|
|
||||||
def order(self, units: dict[UnitType, int]) -> None:
|
def order(self, units: dict[UnitType[Any], int]) -> None:
|
||||||
for k, v in units.items():
|
for k, v in units.items():
|
||||||
self.units[k] += v
|
self.units[k] += v
|
||||||
|
|
||||||
def sell(self, units: dict[UnitType, int]) -> None:
|
def sell(self, units: dict[UnitType[Any], int]) -> None:
|
||||||
for k, v in units.items():
|
for k, v in units.items():
|
||||||
self.units[k] -= v
|
self.units[k] -= v
|
||||||
|
|
||||||
def refund_all(self, game: Game) -> None:
|
def refund_all(self, coalition: Coalition) -> None:
|
||||||
self.refund(game, self.units)
|
self.refund(coalition, self.units)
|
||||||
self.units = defaultdict(int)
|
self.units = defaultdict(int)
|
||||||
|
|
||||||
def refund_ground_units(self, game: Game) -> None:
|
def refund_ground_units(self, coalition: Coalition) -> None:
|
||||||
ground_units: dict[UnitType[Any], int] = {
|
ground_units: dict[UnitType[Any], int] = {
|
||||||
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
|
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
|
||||||
}
|
}
|
||||||
self.refund(game, ground_units)
|
self.refund(coalition, ground_units)
|
||||||
for gu in ground_units.keys():
|
for gu in ground_units.keys():
|
||||||
del self.units[gu]
|
del self.units[gu]
|
||||||
|
|
||||||
def refund(self, game: Game, units: dict[UnitType, int]) -> None:
|
def refund(self, coalition: Coalition, units: dict[UnitType[Any], int]) -> None:
|
||||||
for unit_type, count in units.items():
|
for unit_type, count in units.items():
|
||||||
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
||||||
game.adjust_budget(
|
coalition.adjust_budget(unit_type.price * count)
|
||||||
unit_type.price * count, player=self.destination.captured
|
|
||||||
)
|
|
||||||
|
|
||||||
def pending_orders(self, unit_type: UnitType) -> int:
|
def pending_orders(self, unit_type: UnitType[Any]) -> int:
|
||||||
pending_units = self.units.get(unit_type)
|
pending_units = self.units.get(unit_type)
|
||||||
if pending_units is None:
|
if pending_units is None:
|
||||||
pending_units = 0
|
pending_units = 0
|
||||||
return pending_units
|
return pending_units
|
||||||
|
|
||||||
def available_next_turn(self, unit_type: UnitType) -> int:
|
def available_next_turn(self, unit_type: UnitType[Any]) -> int:
|
||||||
current_units = self.destination.base.total_units_of_type(unit_type)
|
current_units = self.destination.base.total_units_of_type(unit_type)
|
||||||
return self.pending_orders(unit_type) + current_units
|
return self.pending_orders(unit_type) + current_units
|
||||||
|
|
||||||
def process(self, game: Game) -> None:
|
def process(self, game: Game) -> None:
|
||||||
|
coalition = game.coalition_for(self.destination.captured)
|
||||||
ground_unit_source = self.find_ground_unit_source(game)
|
ground_unit_source = self.find_ground_unit_source(game)
|
||||||
if ground_unit_source is None:
|
if ground_unit_source is None:
|
||||||
game.message(
|
game.message(
|
||||||
f"{self.destination.name} lost its source for ground unit "
|
f"{self.destination.name} lost its source for ground unit "
|
||||||
"reinforcements. Refunding purchase price."
|
"reinforcements. Refunding purchase price."
|
||||||
)
|
)
|
||||||
self.refund_ground_units(game)
|
self.refund_ground_units(coalition)
|
||||||
|
|
||||||
bought_units: dict[UnitType, int] = {}
|
bought_units: dict[UnitType[Any], int] = {}
|
||||||
units_needing_transfer: dict[GroundUnitType, int] = {}
|
units_needing_transfer: dict[GroundUnitType, int] = {}
|
||||||
sold_units: dict[UnitType, int] = {}
|
sold_units: dict[UnitType[Any], int] = {}
|
||||||
for unit_type, count in self.units.items():
|
for unit_type, count in self.units.items():
|
||||||
coalition = "Ally" if self.destination.captured else "Enemy"
|
allegiance = "Ally" if self.destination.captured else "Enemy"
|
||||||
d: dict[Any, int]
|
d: dict[Any, int]
|
||||||
if (
|
if (
|
||||||
isinstance(unit_type, GroundUnitType)
|
isinstance(unit_type, GroundUnitType)
|
||||||
@ -98,11 +98,11 @@ class PendingUnitDeliveries:
|
|||||||
if count >= 0:
|
if count >= 0:
|
||||||
d[unit_type] = count
|
d[unit_type] = count
|
||||||
game.message(
|
game.message(
|
||||||
f"{coalition} reinforcements: {unit_type} x {count} at {source}"
|
f"{allegiance} reinforcements: {unit_type} x {count} at {source}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sold_units[unit_type] = -count
|
sold_units[unit_type] = -count
|
||||||
game.message(f"{coalition} sold: {unit_type} x {-count} at {source}")
|
game.message(f"{allegiance} sold: {unit_type} x {-count} at {source}")
|
||||||
|
|
||||||
self.units = defaultdict(int)
|
self.units = defaultdict(int)
|
||||||
self.destination.base.commission_units(bought_units)
|
self.destination.base.commission_units(bought_units)
|
||||||
@ -111,16 +111,19 @@ class PendingUnitDeliveries:
|
|||||||
if units_needing_transfer:
|
if units_needing_transfer:
|
||||||
if ground_unit_source is None:
|
if ground_unit_source is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"ground unit source could not be found for {self.destination} but still tried to "
|
f"Ground unit source could not be found for {self.destination} but "
|
||||||
f"transfer units to there"
|
"still tried to transfer units to there"
|
||||||
)
|
)
|
||||||
ground_unit_source.base.commission_units(units_needing_transfer)
|
ground_unit_source.base.commission_units(units_needing_transfer)
|
||||||
self.create_transfer(game, ground_unit_source, units_needing_transfer)
|
self.create_transfer(coalition, ground_unit_source, units_needing_transfer)
|
||||||
|
|
||||||
def create_transfer(
|
def create_transfer(
|
||||||
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int]
|
self,
|
||||||
|
coalition: Coalition,
|
||||||
|
source: ControlPoint,
|
||||||
|
units: dict[GroundUnitType, int],
|
||||||
) -> None:
|
) -> None:
|
||||||
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
coalition.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
||||||
|
|
||||||
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
|
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
|
||||||
# This is running *after* the turn counter has been incremented, so this is the
|
# This is running *after* the turn counter has been incremented, so this is the
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import math
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, Any, Union, TypeVar, Generic
|
||||||
|
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Vehicle, Ship
|
||||||
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup
|
||||||
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.squadrons import Pilot
|
from game.squadrons import Pilot
|
||||||
@ -27,11 +27,14 @@ class FrontLineUnit:
|
|||||||
origin: ControlPoint
|
origin: ControlPoint
|
||||||
|
|
||||||
|
|
||||||
|
UnitT = TypeVar("UnitT", Ship, Vehicle)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GroundObjectUnit:
|
class GroundObjectUnit(Generic[UnitT]):
|
||||||
ground_object: TheaterGroundObject
|
ground_object: TheaterGroundObject[Any]
|
||||||
group: Group
|
group: MovingGroup[UnitT]
|
||||||
unit: Unit
|
unit: UnitT
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -56,13 +59,13 @@ class UnitMap:
|
|||||||
self.aircraft: Dict[str, FlyingUnit] = {}
|
self.aircraft: Dict[str, FlyingUnit] = {}
|
||||||
self.airfields: Dict[str, Airfield] = {}
|
self.airfields: Dict[str, Airfield] = {}
|
||||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||||
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
self.ground_object_units: Dict[str, GroundObjectUnit[Any]] = {}
|
||||||
self.buildings: Dict[str, Building] = {}
|
self.buildings: Dict[str, Building] = {}
|
||||||
self.convoys: Dict[str, ConvoyUnit] = {}
|
self.convoys: Dict[str, ConvoyUnit] = {}
|
||||||
self.cargo_ships: Dict[str, CargoShip] = {}
|
self.cargo_ships: Dict[str, CargoShip] = {}
|
||||||
self.airlifts: Dict[str, AirliftUnits] = {}
|
self.airlifts: Dict[str, AirliftUnits] = {}
|
||||||
|
|
||||||
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
|
def add_aircraft(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||||
for pilot, unit in zip(flight.roster.pilots, group.units):
|
for pilot, unit in zip(flight.roster.pilots, group.units):
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
@ -85,7 +88,7 @@ class UnitMap:
|
|||||||
return self.airfields.get(name, None)
|
return self.airfields.get(name, None)
|
||||||
|
|
||||||
def add_front_line_units(
|
def add_front_line_units(
|
||||||
self, group: Group, origin: ControlPoint, unit_type: GroundUnitType
|
self, group: VehicleGroup, origin: ControlPoint, unit_type: GroundUnitType
|
||||||
) -> None:
|
) -> None:
|
||||||
for unit in group.units:
|
for unit in group.units:
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
@ -100,9 +103,9 @@ class UnitMap:
|
|||||||
|
|
||||||
def add_ground_object_units(
|
def add_ground_object_units(
|
||||||
self,
|
self,
|
||||||
ground_object: TheaterGroundObject,
|
ground_object: TheaterGroundObject[Any],
|
||||||
persistence_group: Group,
|
persistence_group: Union[ShipGroup, VehicleGroup],
|
||||||
miz_group: Group,
|
miz_group: Union[ShipGroup, VehicleGroup],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Adds a group associated with a TGO to the unit map.
|
"""Adds a group associated with a TGO to the unit map.
|
||||||
|
|
||||||
@ -131,10 +134,10 @@ class UnitMap:
|
|||||||
ground_object, persistence_group, persistent_unit
|
ground_object, persistence_group, persistent_unit
|
||||||
)
|
)
|
||||||
|
|
||||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
|
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit[Any]]:
|
||||||
return self.ground_object_units.get(name, None)
|
return self.ground_object_units.get(name, None)
|
||||||
|
|
||||||
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
|
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
|
||||||
for unit, unit_type in zip(group.units, convoy.iter_units()):
|
for unit, unit_type in zip(group.units, convoy.iter_units()):
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
@ -146,7 +149,7 @@ class UnitMap:
|
|||||||
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
||||||
return self.convoys.get(name, None)
|
return self.convoys.get(name, None)
|
||||||
|
|
||||||
def add_cargo_ship(self, group: Group, ship: CargoShip) -> None:
|
def add_cargo_ship(self, group: ShipGroup, ship: CargoShip) -> None:
|
||||||
if len(group.units) > 1:
|
if len(group.units) > 1:
|
||||||
# Cargo ship "groups" are single units. Killing the one ship kills the whole
|
# Cargo ship "groups" are single units. Killing the one ship kills the whole
|
||||||
# transfer. If we ever want to add escorts or create multiple cargo ships in
|
# transfer. If we ever want to add escorts or create multiple cargo ships in
|
||||||
@ -163,7 +166,9 @@ class UnitMap:
|
|||||||
def cargo_ship(self, name: str) -> Optional[CargoShip]:
|
def cargo_ship(self, name: str) -> Optional[CargoShip]:
|
||||||
return self.cargo_ships.get(name, None)
|
return self.cargo_ships.get(name, None)
|
||||||
|
|
||||||
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
|
def add_airlift_units(
|
||||||
|
self, group: FlyingGroup[Any], transfer: TransferOrder
|
||||||
|
) -> None:
|
||||||
capacity_each = math.ceil(transfer.size / len(group.units))
|
capacity_each = math.ceil(transfer.size / len(group.units))
|
||||||
for idx, transport in enumerate(group.units):
|
for idx, transport in enumerate(group.units):
|
||||||
# Slice the units in groups based on the capacity of each unit. Cargo is
|
# Slice the units in groups based on the capacity of each unit. Cargo is
|
||||||
@ -186,7 +191,9 @@ class UnitMap:
|
|||||||
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
|
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
|
||||||
return self.airlifts.get(name, None)
|
return self.airlifts.get(name, None)
|
||||||
|
|
||||||
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
|
def add_building(
|
||||||
|
self, ground_object: BuildingGroundObject, group: StaticGroup
|
||||||
|
) -> None:
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
# The name of the initiator in the DCS dead event will have " object"
|
# The name of the initiator in the DCS dead event will have " object"
|
||||||
|
|||||||
@ -2,8 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import math
|
import math
|
||||||
|
from collections import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Union
|
from typing import Union, Any
|
||||||
|
|
||||||
METERS_TO_FEET = 3.28084
|
METERS_TO_FEET = 3.28084
|
||||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||||
@ -16,12 +17,12 @@ MS_TO_KPH = 3.6
|
|||||||
KPH_TO_MS = 1 / MS_TO_KPH
|
KPH_TO_MS = 1 / MS_TO_KPH
|
||||||
|
|
||||||
|
|
||||||
def heading_sum(h, a) -> int:
|
def heading_sum(h: int, a: int) -> int:
|
||||||
h += a
|
h += a
|
||||||
return h % 360
|
return h % 360
|
||||||
|
|
||||||
|
|
||||||
def opposite_heading(h):
|
def opposite_heading(h: int) -> int:
|
||||||
return heading_sum(h, 180)
|
return heading_sum(h, 180)
|
||||||
|
|
||||||
|
|
||||||
@ -180,7 +181,7 @@ def mach(value: float, altitude: Distance) -> Speed:
|
|||||||
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
||||||
|
|
||||||
|
|
||||||
def pairwise(iterable):
|
def pairwise(iterable: Iterable[Any]) -> Iterable[tuple[Any, Any]]:
|
||||||
"""
|
"""
|
||||||
itertools recipe
|
itertools recipe
|
||||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MAJOR_VERSION = 5
|
||||||
|
MINOR_VERSION = 0
|
||||||
|
MICRO_VERSION = 0
|
||||||
|
|
||||||
|
|
||||||
def _build_version_string() -> str:
|
def _build_version_string() -> str:
|
||||||
components = ["5.0.0"]
|
components = [
|
||||||
|
".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
|
||||||
|
]
|
||||||
build_number_path = Path("resources/buildnumber")
|
build_number_path = Path("resources/buildnumber")
|
||||||
if build_number_path.exists():
|
if build_number_path.exists():
|
||||||
with build_number_path.open("r") as build_number_file:
|
with build_number_path.open("r") as build_number_file:
|
||||||
@ -96,4 +103,7 @@ VERSION = _build_version_string()
|
|||||||
#: mission using map buildings as strike targets must check and potentially recreate
|
#: mission using map buildings as strike targets must check and potentially recreate
|
||||||
#: all those objectives. This definitely affects all Syria campaigns, other maps are
|
#: all those objectives. This definitely affects all Syria campaigns, other maps are
|
||||||
#: not yet verified.
|
#: not yet verified.
|
||||||
CAMPAIGN_FORMAT_VERSION = (7, 0)
|
#:
|
||||||
|
#: Version 7.1
|
||||||
|
#: * Support for Mariana Islands terrain
|
||||||
|
CAMPAIGN_FORMAT_VERSION = (7, 1)
|
||||||
|
|||||||
@ -24,6 +24,14 @@ class TimeOfDay(Enum):
|
|||||||
Night = "night"
|
Night = "night"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AtmosphericConditions:
|
||||||
|
#: Pressure at sea level in inches of mercury.
|
||||||
|
qnh_inches_mercury: float
|
||||||
|
#: Temperature at sea level in Celcius.
|
||||||
|
temperature_celsius: float
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class WindConditions:
|
class WindConditions:
|
||||||
at_0m: Wind
|
at_0m: Wind
|
||||||
@ -64,10 +72,16 @@ class Fog:
|
|||||||
|
|
||||||
class Weather:
|
class Weather:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
# Future improvement: Use theater, day and time of day
|
||||||
|
# to get a more realistic conditions
|
||||||
|
self.atmospheric = self.generate_atmospheric()
|
||||||
self.clouds = self.generate_clouds()
|
self.clouds = self.generate_clouds()
|
||||||
self.fog = self.generate_fog()
|
self.fog = self.generate_fog()
|
||||||
self.wind = self.generate_wind()
|
self.wind = self.generate_wind()
|
||||||
|
|
||||||
|
def generate_atmospheric(self) -> AtmosphericConditions:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def generate_clouds(self) -> Optional[Clouds]:
|
def generate_clouds(self) -> Optional[Clouds]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@ -83,7 +97,7 @@ class Weather:
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def random_wind(minimum: int, maximum) -> WindConditions:
|
def random_wind(minimum: int, maximum: int) -> WindConditions:
|
||||||
wind_direction = random.randint(0, 360)
|
wind_direction = random.randint(0, 360)
|
||||||
at_0m_factor = 1
|
at_0m_factor = 1
|
||||||
at_2000m_factor = 2
|
at_2000m_factor = 2
|
||||||
@ -105,8 +119,35 @@ class Weather:
|
|||||||
def random_cloud_thickness() -> int:
|
def random_cloud_thickness() -> int:
|
||||||
return random.randint(100, 400)
|
return random.randint(100, 400)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def random_pressure(average_pressure: float) -> float:
|
||||||
|
# "Safe" constants based roughly on ME and viper altimeter.
|
||||||
|
# Units are inches of mercury.
|
||||||
|
SAFE_MIN = 28.4
|
||||||
|
SAFE_MAX = 30.9
|
||||||
|
# Use normalvariate to get normal distribution, more realistic than uniform
|
||||||
|
pressure = random.normalvariate(average_pressure, 0.2)
|
||||||
|
return max(SAFE_MIN, min(SAFE_MAX, pressure))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def random_temperature(average_temperature: float) -> float:
|
||||||
|
# "Safe" constants based roughly on ME.
|
||||||
|
# Temperatures are in Celcius.
|
||||||
|
SAFE_MIN = -12
|
||||||
|
SAFE_MAX = 49
|
||||||
|
# Use normalvariate to get normal distribution, more realistic than uniform
|
||||||
|
temperature = random.normalvariate(average_temperature, 4)
|
||||||
|
temperature = round(temperature)
|
||||||
|
return max(SAFE_MIN, min(SAFE_MAX, temperature))
|
||||||
|
|
||||||
|
|
||||||
class ClearSkies(Weather):
|
class ClearSkies(Weather):
|
||||||
|
def generate_atmospheric(self) -> AtmosphericConditions:
|
||||||
|
return AtmosphericConditions(
|
||||||
|
qnh_inches_mercury=self.random_pressure(29.96),
|
||||||
|
temperature_celsius=self.random_temperature(22),
|
||||||
|
)
|
||||||
|
|
||||||
def generate_clouds(self) -> Optional[Clouds]:
|
def generate_clouds(self) -> Optional[Clouds]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -118,6 +159,12 @@ class ClearSkies(Weather):
|
|||||||
|
|
||||||
|
|
||||||
class Cloudy(Weather):
|
class Cloudy(Weather):
|
||||||
|
def generate_atmospheric(self) -> AtmosphericConditions:
|
||||||
|
return AtmosphericConditions(
|
||||||
|
qnh_inches_mercury=self.random_pressure(29.90),
|
||||||
|
temperature_celsius=self.random_temperature(20),
|
||||||
|
)
|
||||||
|
|
||||||
def generate_clouds(self) -> Optional[Clouds]:
|
def generate_clouds(self) -> Optional[Clouds]:
|
||||||
return Clouds.random_preset(rain=False)
|
return Clouds.random_preset(rain=False)
|
||||||
|
|
||||||
@ -130,6 +177,12 @@ class Cloudy(Weather):
|
|||||||
|
|
||||||
|
|
||||||
class Raining(Weather):
|
class Raining(Weather):
|
||||||
|
def generate_atmospheric(self) -> AtmosphericConditions:
|
||||||
|
return AtmosphericConditions(
|
||||||
|
qnh_inches_mercury=self.random_pressure(29.70),
|
||||||
|
temperature_celsius=self.random_temperature(16),
|
||||||
|
)
|
||||||
|
|
||||||
def generate_clouds(self) -> Optional[Clouds]:
|
def generate_clouds(self) -> Optional[Clouds]:
|
||||||
return Clouds.random_preset(rain=True)
|
return Clouds.random_preset(rain=True)
|
||||||
|
|
||||||
@ -142,6 +195,12 @@ class Raining(Weather):
|
|||||||
|
|
||||||
|
|
||||||
class Thunderstorm(Weather):
|
class Thunderstorm(Weather):
|
||||||
|
def generate_atmospheric(self) -> AtmosphericConditions:
|
||||||
|
return AtmosphericConditions(
|
||||||
|
qnh_inches_mercury=self.random_pressure(29.60),
|
||||||
|
temperature_celsius=self.random_temperature(15),
|
||||||
|
)
|
||||||
|
|
||||||
def generate_clouds(self) -> Optional[Clouds]:
|
def generate_clouds(self) -> Optional[Clouds]:
|
||||||
return Clouds(
|
return Clouds(
|
||||||
base=self.random_cloud_base(),
|
base=self.random_cloud_base(),
|
||||||
@ -168,11 +227,12 @@ class Conditions:
|
|||||||
time_of_day: TimeOfDay,
|
time_of_day: TimeOfDay,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
) -> Conditions:
|
) -> Conditions:
|
||||||
|
_start_time = cls.generate_start_time(
|
||||||
|
theater, day, time_of_day, settings.night_disabled
|
||||||
|
)
|
||||||
return cls(
|
return cls(
|
||||||
time_of_day=time_of_day,
|
time_of_day=time_of_day,
|
||||||
start_time=cls.generate_start_time(
|
start_time=_start_time,
|
||||||
theater, day, time_of_day, settings.night_disabled
|
|
||||||
),
|
|
||||||
weather=cls.generate_weather(),
|
weather=cls.generate_weather(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
133
gen/aircraft.py
133
gen/aircraft.py
@ -5,7 +5,7 @@ import random
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable
|
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable, Any
|
||||||
|
|
||||||
from dcs import helicopters
|
from dcs import helicopters
|
||||||
from dcs.action import AITaskPush, ActivateGroup
|
from dcs.action import AITaskPush, ActivateGroup
|
||||||
@ -22,7 +22,6 @@ from dcs.planes import (
|
|||||||
C_101EB,
|
C_101EB,
|
||||||
F_14B,
|
F_14B,
|
||||||
JF_17,
|
JF_17,
|
||||||
PlaneType,
|
|
||||||
Su_33,
|
Su_33,
|
||||||
Tu_22M3,
|
Tu_22M3,
|
||||||
)
|
)
|
||||||
@ -262,8 +261,8 @@ class AircraftConflictGenerator:
|
|||||||
@cached_property
|
@cached_property
|
||||||
def use_client(self) -> bool:
|
def use_client(self) -> bool:
|
||||||
"""True if Client should be used instead of Player."""
|
"""True if Client should be used instead of Player."""
|
||||||
blue_clients = self.client_slots_in_ato(self.game.blue_ato)
|
blue_clients = self.client_slots_in_ato(self.game.blue.ato)
|
||||||
red_clients = self.client_slots_in_ato(self.game.red_ato)
|
red_clients = self.client_slots_in_ato(self.game.red.ato)
|
||||||
return blue_clients + red_clients > 1
|
return blue_clients + red_clients > 1
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -321,7 +320,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def livery_from_db(flight: Flight) -> Optional[str]:
|
def livery_from_db(flight: Flight) -> Optional[str]:
|
||||||
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type)
|
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type.dcs_unit_type)
|
||||||
|
|
||||||
def livery_from_faction(self, flight: Flight) -> Optional[str]:
|
def livery_from_faction(self, flight: Flight) -> Optional[str]:
|
||||||
faction = self.game.faction_for(player=flight.departure.captured)
|
faction = self.game.faction_for(player=flight.departure.captured)
|
||||||
@ -342,7 +341,7 @@ class AircraftConflictGenerator:
|
|||||||
return livery
|
return livery
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _setup_livery(self, flight: Flight, group: FlyingGroup) -> None:
|
def _setup_livery(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
||||||
livery = self.livery_for(flight)
|
livery = self.livery_for(flight)
|
||||||
if livery is None:
|
if livery is None:
|
||||||
return
|
return
|
||||||
@ -351,7 +350,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def _setup_group(
|
def _setup_group(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -458,8 +457,8 @@ class AircraftConflictGenerator:
|
|||||||
unit_type: Type[FlyingType],
|
unit_type: Type[FlyingType],
|
||||||
count: int,
|
count: int,
|
||||||
start_type: str,
|
start_type: str,
|
||||||
airport: Optional[Airport] = None,
|
airport: Airport,
|
||||||
) -> FlyingGroup:
|
) -> FlyingGroup[Any]:
|
||||||
assert count > 0
|
assert count > 0
|
||||||
|
|
||||||
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
||||||
@ -476,7 +475,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def _generate_inflight(
|
def _generate_inflight(
|
||||||
self, name: str, side: Country, flight: Flight, origin: ControlPoint
|
self, name: str, side: Country, flight: Flight, origin: ControlPoint
|
||||||
) -> FlyingGroup:
|
) -> FlyingGroup[Any]:
|
||||||
assert flight.count > 0
|
assert flight.count > 0
|
||||||
at = origin.position
|
at = origin.position
|
||||||
|
|
||||||
@ -521,7 +520,7 @@ class AircraftConflictGenerator:
|
|||||||
count: int,
|
count: int,
|
||||||
start_type: str,
|
start_type: str,
|
||||||
at: Union[ShipGroup, StaticGroup],
|
at: Union[ShipGroup, StaticGroup],
|
||||||
) -> FlyingGroup:
|
) -> FlyingGroup[Any]:
|
||||||
assert count > 0
|
assert count > 0
|
||||||
|
|
||||||
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
||||||
@ -536,34 +535,18 @@ class AircraftConflictGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _add_radio_waypoint(
|
def _add_radio_waypoint(
|
||||||
self, group: FlyingGroup, position, altitude: Distance, airspeed: int = 600
|
self,
|
||||||
|
group: FlyingGroup[Any],
|
||||||
|
position: Point,
|
||||||
|
altitude: Distance,
|
||||||
|
airspeed: int = 600,
|
||||||
) -> MovingPoint:
|
) -> MovingPoint:
|
||||||
point = group.add_waypoint(position, altitude.meters, airspeed)
|
point = group.add_waypoint(position, altitude.meters, airspeed)
|
||||||
point.alt_type = "RADIO"
|
point.alt_type = "RADIO"
|
||||||
return point
|
return point
|
||||||
|
|
||||||
def _rtb_for(
|
@staticmethod
|
||||||
self,
|
def _at_position(at: Union[Point, ShipGroup, Type[Airport]]) -> Point:
|
||||||
group: FlyingGroup,
|
|
||||||
cp: ControlPoint,
|
|
||||||
at: Optional[db.StartingPosition] = None,
|
|
||||||
):
|
|
||||||
if at is None:
|
|
||||||
at = cp.at
|
|
||||||
position = at if isinstance(at, Point) else at.position
|
|
||||||
|
|
||||||
last_waypoint = group.points[-1]
|
|
||||||
if last_waypoint is not None:
|
|
||||||
heading = position.heading_between_point(last_waypoint.position)
|
|
||||||
tod_location = position.point_from_heading(heading, RTB_DISTANCE)
|
|
||||||
self._add_radio_waypoint(group, tod_location, last_waypoint.alt)
|
|
||||||
|
|
||||||
destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE)
|
|
||||||
if isinstance(at, Airport):
|
|
||||||
group.land_at(at)
|
|
||||||
return destination_waypoint
|
|
||||||
|
|
||||||
def _at_position(self, at) -> Point:
|
|
||||||
if isinstance(at, Point):
|
if isinstance(at, Point):
|
||||||
return at
|
return at
|
||||||
elif isinstance(at, ShipGroup):
|
elif isinstance(at, ShipGroup):
|
||||||
@ -573,7 +556,7 @@ class AircraftConflictGenerator:
|
|||||||
else:
|
else:
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
def _setup_payload(self, flight: Flight, group: FlyingGroup) -> None:
|
def _setup_payload(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
||||||
for p in group.units:
|
for p in group.units:
|
||||||
p.pylons.clear()
|
p.pylons.clear()
|
||||||
|
|
||||||
@ -593,7 +576,10 @@ class AircraftConflictGenerator:
|
|||||||
parking_slot.unit_id = None
|
parking_slot.unit_id = None
|
||||||
|
|
||||||
def generate_flights(
|
def generate_flights(
|
||||||
self, country, ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData]
|
self,
|
||||||
|
country: Country,
|
||||||
|
ato: AirTaskingOrder,
|
||||||
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
for package in ato.packages:
|
for package in ato.packages:
|
||||||
@ -614,12 +600,11 @@ class AircraftConflictGenerator:
|
|||||||
if not isinstance(control_point, Airfield):
|
if not isinstance(control_point, Airfield):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
faction = self.game.coalition_for(control_point.captured).faction
|
||||||
if control_point.captured:
|
if control_point.captured:
|
||||||
country = player_country
|
country = player_country
|
||||||
faction = self.game.player_faction
|
|
||||||
else:
|
else:
|
||||||
country = enemy_country
|
country = enemy_country
|
||||||
faction = self.game.enemy_faction
|
|
||||||
|
|
||||||
for aircraft, available in inventory.all_aircraft:
|
for aircraft, available in inventory.all_aircraft:
|
||||||
try:
|
try:
|
||||||
@ -672,7 +657,7 @@ class AircraftConflictGenerator:
|
|||||||
self.unit_map.add_aircraft(group, flight)
|
self.unit_map.add_aircraft(group, flight)
|
||||||
|
|
||||||
def set_activation_time(
|
def set_activation_time(
|
||||||
self, flight: Flight, group: FlyingGroup, delay: timedelta
|
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
|
||||||
) -> None:
|
) -> None:
|
||||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||||
# mission editor. Waypoint times will be relative to the group
|
# mission editor. Waypoint times will be relative to the group
|
||||||
@ -691,7 +676,7 @@ class AircraftConflictGenerator:
|
|||||||
self.m.triggerrules.triggers.append(activation_trigger)
|
self.m.triggerrules.triggers.append(activation_trigger)
|
||||||
|
|
||||||
def set_startup_time(
|
def set_startup_time(
|
||||||
self, flight: Flight, group: FlyingGroup, delay: timedelta
|
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
|
||||||
) -> None:
|
) -> None:
|
||||||
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
||||||
group.uncontrolled = True
|
group.uncontrolled = True
|
||||||
@ -712,14 +697,12 @@ class AircraftConflictGenerator:
|
|||||||
if flight.from_cp.cptype != ControlPointType.AIRBASE:
|
if flight.from_cp.cptype != ControlPointType.AIRBASE:
|
||||||
return
|
return
|
||||||
|
|
||||||
if flight.from_cp.captured:
|
coalition = self.game.coalition_for(flight.departure.captured).coalition_id
|
||||||
coalition = self.game.get_player_coalition_id()
|
|
||||||
else:
|
|
||||||
coalition = self.game.get_enemy_coalition_id()
|
|
||||||
|
|
||||||
trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
||||||
|
|
||||||
def generate_planned_flight(self, cp, country, flight: Flight):
|
def generate_planned_flight(
|
||||||
|
self, cp: ControlPoint, country: Country, flight: Flight
|
||||||
|
) -> FlyingGroup[Any]:
|
||||||
name = namegen.next_aircraft_name(country, cp.id, flight)
|
name = namegen.next_aircraft_name(country, cp.id, flight)
|
||||||
try:
|
try:
|
||||||
if flight.start_type == "In Flight":
|
if flight.start_type == "In Flight":
|
||||||
@ -728,13 +711,19 @@ class AircraftConflictGenerator:
|
|||||||
)
|
)
|
||||||
elif isinstance(cp, NavalControlPoint):
|
elif isinstance(cp, NavalControlPoint):
|
||||||
group_name = cp.get_carrier_group_name()
|
group_name = cp.get_carrier_group_name()
|
||||||
|
carrier_group = self.m.find_group(group_name)
|
||||||
|
if not isinstance(carrier_group, ShipGroup):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Carrier group {carrier_group} is a "
|
||||||
|
"{carrier_group.__class__.__name__}, expected a ShipGroup"
|
||||||
|
)
|
||||||
group = self._generate_at_group(
|
group = self._generate_at_group(
|
||||||
name=name,
|
name=name,
|
||||||
side=country,
|
side=country,
|
||||||
unit_type=flight.unit_type.dcs_unit_type,
|
unit_type=flight.unit_type.dcs_unit_type,
|
||||||
count=flight.count,
|
count=flight.count,
|
||||||
start_type=flight.start_type,
|
start_type=flight.start_type,
|
||||||
at=self.m.find_group(group_name),
|
at=carrier_group,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if not isinstance(cp, Airfield):
|
if not isinstance(cp, Airfield):
|
||||||
@ -765,7 +754,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_reduced_fuel(
|
def set_reduced_fuel(
|
||||||
flight: Flight, group: FlyingGroup, unit_type: Type[PlaneType]
|
flight: Flight, group: FlyingGroup[Any], unit_type: Type[FlyingType]
|
||||||
) -> None:
|
) -> None:
|
||||||
if unit_type is Su_33:
|
if unit_type is Su_33:
|
||||||
for unit in group.units:
|
for unit in group.units:
|
||||||
@ -791,9 +780,9 @@ class AircraftConflictGenerator:
|
|||||||
def configure_behavior(
|
def configure_behavior(
|
||||||
self,
|
self,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
||||||
roe: Optional[OptROE.Values] = None,
|
roe: Optional[int] = None,
|
||||||
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
||||||
restrict_jettison: Optional[bool] = None,
|
restrict_jettison: Optional[bool] = None,
|
||||||
mission_uses_gun: bool = True,
|
mission_uses_gun: bool = True,
|
||||||
@ -824,13 +813,13 @@ class AircraftConflictGenerator:
|
|||||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
|
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
|
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
|
||||||
if flight.unit_type.eplrs_capable:
|
if flight.unit_type.eplrs_capable:
|
||||||
group.points[0].tasks.append(EPLRS(group.id))
|
group.points[0].tasks.append(EPLRS(group.id))
|
||||||
|
|
||||||
def configure_cap(
|
def configure_cap(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -847,7 +836,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_sweep(
|
def configure_sweep(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -864,7 +853,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_cas(
|
def configure_cas(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -882,7 +871,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_dead(
|
def configure_dead(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -907,7 +896,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_sead(
|
def configure_sead(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -931,7 +920,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_strike(
|
def configure_strike(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -949,7 +938,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_anti_ship(
|
def configure_anti_ship(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -967,7 +956,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_runway_attack(
|
def configure_runway_attack(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -985,7 +974,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_oca_strike(
|
def configure_oca_strike(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -1002,7 +991,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_awacs(
|
def configure_awacs(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -1030,7 +1019,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_refueling(
|
def configure_refueling(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -1056,7 +1045,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_escort(
|
def configure_escort(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -1072,7 +1061,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_sead_escort(
|
def configure_sead_escort(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -1095,7 +1084,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_transport(
|
def configure_transport(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -1110,13 +1099,13 @@ class AircraftConflictGenerator:
|
|||||||
restrict_jettison=True,
|
restrict_jettison=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
|
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||||
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||||
self.configure_behavior(flight, group)
|
self.configure_behavior(flight, group)
|
||||||
|
|
||||||
def setup_flight_group(
|
def setup_flight_group(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@ -1160,7 +1149,7 @@ class AircraftConflictGenerator:
|
|||||||
self.configure_eplrs(group, flight)
|
self.configure_eplrs(group, flight)
|
||||||
|
|
||||||
def create_waypoints(
|
def create_waypoints(
|
||||||
self, group: FlyingGroup, package: Package, flight: Flight
|
self, group: FlyingGroup[Any], package: Package, flight: Flight
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
for waypoint in flight.points:
|
for waypoint in flight.points:
|
||||||
@ -1228,7 +1217,7 @@ class AircraftConflictGenerator:
|
|||||||
waypoint: FlightWaypoint,
|
waypoint: FlightWaypoint,
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
estimator = TotEstimator(package)
|
estimator = TotEstimator(package)
|
||||||
start_time = estimator.mission_start_time(flight)
|
start_time = estimator.mission_start_time(flight)
|
||||||
@ -1271,7 +1260,7 @@ class PydcsWaypointBuilder:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
waypoint: FlightWaypoint,
|
waypoint: FlightWaypoint,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
@ -1314,7 +1303,7 @@ class PydcsWaypointBuilder:
|
|||||||
def for_waypoint(
|
def for_waypoint(
|
||||||
cls,
|
cls,
|
||||||
waypoint: FlightWaypoint,
|
waypoint: FlightWaypoint,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
@ -1428,7 +1417,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
|||||||
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||||
waypoint.add_task(
|
waypoint.add_task(
|
||||||
EngageTargetsInZone(
|
EngageTargetsInZone(
|
||||||
position=self.flight.flight_plan.target,
|
position=self.flight.flight_plan.target.position,
|
||||||
radius=int(self.flight.flight_plan.engagement_distance.meters),
|
radius=int(self.flight.flight_plan.engagement_distance.meters),
|
||||||
targets=[
|
targets=[
|
||||||
Targets.All.GroundUnits.GroundVehicles,
|
Targets.All.GroundUnits.GroundVehicles,
|
||||||
|
|||||||
@ -1521,4 +1521,47 @@ AIRFIELD_DATA = {
|
|||||||
runway_length=3953,
|
runway_length=3953,
|
||||||
atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
|
atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
|
||||||
),
|
),
|
||||||
|
"Antonio B. Won Pat Intl": AirfieldData(
|
||||||
|
theater="MarianaIslands",
|
||||||
|
icao="PGUM",
|
||||||
|
elevation=255,
|
||||||
|
runway_length=9359,
|
||||||
|
atc=AtcData(MHz(3, 825), MHz(118, 100), MHz(38, 550), MHz(340, 200)),
|
||||||
|
ils={
|
||||||
|
"06": ("IGUM", MHz(110, 30)),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"Andersen AFB": AirfieldData(
|
||||||
|
theater="MarianaIslands",
|
||||||
|
icao="PGUA",
|
||||||
|
elevation=545,
|
||||||
|
runway_length=10490,
|
||||||
|
tacan=TacanChannel(54, TacanBand.X),
|
||||||
|
tacan_callsign="UAM",
|
||||||
|
atc=AtcData(MHz(3, 850), MHz(126, 200), MHz(38, 600), MHz(250, 100)),
|
||||||
|
),
|
||||||
|
"Rota Intl": AirfieldData(
|
||||||
|
theater="MarianaIslands",
|
||||||
|
icao="PGRO",
|
||||||
|
elevation=568,
|
||||||
|
runway_length=6105,
|
||||||
|
atc=AtcData(MHz(3, 750), MHz(123, 600), MHz(38, 400), MHz(250, 0)),
|
||||||
|
),
|
||||||
|
"Tinian Intl": AirfieldData(
|
||||||
|
theater="MarianaIslands",
|
||||||
|
icao="PGWT",
|
||||||
|
elevation=240,
|
||||||
|
runway_length=7777,
|
||||||
|
atc=AtcData(MHz(3, 800), MHz(123, 650), MHz(38, 500), MHz(250, 50)),
|
||||||
|
),
|
||||||
|
"Saipan Intl": AirfieldData(
|
||||||
|
theater="MarianaIslands",
|
||||||
|
icao="PGSN",
|
||||||
|
elevation=213,
|
||||||
|
runway_length=7790,
|
||||||
|
atc=AtcData(MHz(3, 775), MHz(125, 700), MHz(38, 450), MHz(256, 900)),
|
||||||
|
ils={
|
||||||
|
"07": ("IGSN", MHz(109, 90)),
|
||||||
|
},
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import List, Type, Tuple, Optional
|
from typing import List, Type, Tuple, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs.mission import Mission, StartType
|
from dcs.mission import Mission, StartType
|
||||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType
|
||||||
from dcs.unittype import UnitType
|
|
||||||
from dcs.task import (
|
from dcs.task import (
|
||||||
AWACS,
|
AWACS,
|
||||||
ActivateBeaconCommand,
|
ActivateBeaconCommand,
|
||||||
@ -14,15 +15,17 @@ from dcs.task import (
|
|||||||
SetImmortalCommand,
|
SetImmortalCommand,
|
||||||
SetInvisibleCommand,
|
SetInvisibleCommand,
|
||||||
)
|
)
|
||||||
|
from dcs.unittype import UnitType
|
||||||
|
|
||||||
from game import db
|
|
||||||
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
|
||||||
from .naming import namegen
|
|
||||||
from .callsigns import callsign_for_support_unit
|
from .callsigns import callsign_for_support_unit
|
||||||
from .conflictgen import Conflict
|
from .conflictgen import Conflict
|
||||||
|
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||||
|
from .naming import namegen
|
||||||
from .radios import RadioFrequency, RadioRegistry
|
from .radios import RadioFrequency, RadioRegistry
|
||||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
|
||||||
TANKER_DISTANCE = 15000
|
TANKER_DISTANCE = 15000
|
||||||
TANKER_ALT = 4572
|
TANKER_ALT = 4572
|
||||||
@ -70,7 +73,7 @@ class AirSupportConflictGenerator:
|
|||||||
self,
|
self,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
conflict: Conflict,
|
conflict: Conflict,
|
||||||
game,
|
game: Game,
|
||||||
radio_registry: RadioRegistry,
|
radio_registry: RadioRegistry,
|
||||||
tacan_registry: TacanRegistry,
|
tacan_registry: TacanRegistry,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -95,19 +98,26 @@ class AirSupportConflictGenerator:
|
|||||||
return (TANKER_ALT + 500, 596)
|
return (TANKER_ALT + 500, 596)
|
||||||
return (TANKER_ALT, 574)
|
return (TANKER_ALT, 574)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
player_cp = (
|
player_cp = (
|
||||||
self.conflict.blue_cp
|
self.conflict.blue_cp
|
||||||
if self.conflict.blue_cp.captured
|
if self.conflict.blue_cp.captured
|
||||||
else self.conflict.red_cp
|
else self.conflict.red_cp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
country = self.mission.country(self.game.blue.country_name)
|
||||||
|
|
||||||
if not self.game.settings.disable_legacy_tanker:
|
if not self.game.settings.disable_legacy_tanker:
|
||||||
fallback_tanker_number = 0
|
fallback_tanker_number = 0
|
||||||
|
|
||||||
for i, tanker_unit_type in enumerate(
|
for i, tanker_unit_type in enumerate(
|
||||||
self.game.faction_for(player=True).tankers
|
self.game.faction_for(player=True).tankers
|
||||||
):
|
):
|
||||||
|
unit_type = tanker_unit_type.dcs_unit_type
|
||||||
|
if not issubclass(unit_type, PlaneType):
|
||||||
|
logging.warning(f"Refueling aircraft {unit_type} must be a plane")
|
||||||
|
continue
|
||||||
|
|
||||||
# TODO: Make loiter altitude a property of the unit type.
|
# TODO: Make loiter altitude a property of the unit type.
|
||||||
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
||||||
freq = self.radio_registry.alloc_uhf()
|
freq = self.radio_registry.alloc_uhf()
|
||||||
@ -122,12 +132,10 @@ class AirSupportConflictGenerator:
|
|||||||
tanker_heading, TANKER_DISTANCE
|
tanker_heading, TANKER_DISTANCE
|
||||||
)
|
)
|
||||||
tanker_group = self.mission.refuel_flight(
|
tanker_group = self.mission.refuel_flight(
|
||||||
country=self.mission.country(self.game.player_country),
|
country=country,
|
||||||
name=namegen.next_tanker_name(
|
name=namegen.next_tanker_name(country, tanker_unit_type),
|
||||||
self.mission.country(self.game.player_country), tanker_unit_type
|
|
||||||
),
|
|
||||||
airport=None,
|
airport=None,
|
||||||
plane_type=tanker_unit_type,
|
plane_type=unit_type,
|
||||||
position=tanker_position,
|
position=tanker_position,
|
||||||
altitude=alt,
|
altitude=alt,
|
||||||
race_distance=58000,
|
race_distance=58000,
|
||||||
@ -177,6 +185,8 @@ class AirSupportConflictGenerator:
|
|||||||
tanker_unit_type.name,
|
tanker_unit_type.name,
|
||||||
freq,
|
freq,
|
||||||
tacan,
|
tacan,
|
||||||
|
start_time=None,
|
||||||
|
end_time=None,
|
||||||
blue=True,
|
blue=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -195,12 +205,15 @@ class AirSupportConflictGenerator:
|
|||||||
awacs_unit = possible_awacs[0]
|
awacs_unit = possible_awacs[0]
|
||||||
freq = self.radio_registry.alloc_uhf()
|
freq = self.radio_registry.alloc_uhf()
|
||||||
|
|
||||||
|
unit_type = awacs_unit.dcs_unit_type
|
||||||
|
if not issubclass(unit_type, PlaneType):
|
||||||
|
logging.warning(f"AWACS aircraft {unit_type} must be a plane")
|
||||||
|
return
|
||||||
|
|
||||||
awacs_flight = self.mission.awacs_flight(
|
awacs_flight = self.mission.awacs_flight(
|
||||||
country=self.mission.country(self.game.player_country),
|
country=country,
|
||||||
name=namegen.next_awacs_name(
|
name=namegen.next_awacs_name(country),
|
||||||
self.mission.country(self.game.player_country)
|
plane_type=unit_type,
|
||||||
),
|
|
||||||
plane_type=awacs_unit,
|
|
||||||
altitude=AWACS_ALT,
|
altitude=AWACS_ALT,
|
||||||
airport=None,
|
airport=None,
|
||||||
position=self.conflict.position.random_point_within(
|
position=self.conflict.position.random_point_within(
|
||||||
|
|||||||
84
gen/armor.py
84
gen/armor.py
@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||||
@ -23,7 +24,7 @@ from dcs.task import (
|
|||||||
SetInvisibleCommand,
|
SetInvisibleCommand,
|
||||||
)
|
)
|
||||||
from dcs.triggers import Event, TriggerOnce
|
from dcs.triggers import Event, TriggerOnce
|
||||||
from dcs.unit import Vehicle
|
from dcs.unit import Vehicle, Skill
|
||||||
from dcs.unitgroup import VehicleGroup
|
from dcs.unitgroup import VehicleGroup
|
||||||
|
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.groundunitclass import GroundUnitClass
|
||||||
@ -85,57 +86,24 @@ class GroundConflictGenerator:
|
|||||||
player_planned_combat_groups: List[CombatGroup],
|
player_planned_combat_groups: List[CombatGroup],
|
||||||
enemy_planned_combat_groups: List[CombatGroup],
|
enemy_planned_combat_groups: List[CombatGroup],
|
||||||
player_stance: CombatStance,
|
player_stance: CombatStance,
|
||||||
|
enemy_stance: CombatStance,
|
||||||
unit_map: UnitMap,
|
unit_map: UnitMap,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.mission = mission
|
self.mission = mission
|
||||||
self.conflict = conflict
|
self.conflict = conflict
|
||||||
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
self.enemy_planned_combat_groups = enemy_planned_combat_groups
|
||||||
self.player_planned_combat_groups = player_planned_combat_groups
|
self.player_planned_combat_groups = player_planned_combat_groups
|
||||||
self.player_stance = CombatStance(player_stance)
|
self.player_stance = player_stance
|
||||||
self.enemy_stance = self._enemy_stance()
|
self.enemy_stance = enemy_stance
|
||||||
self.game = game
|
self.game = game
|
||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
self.jtacs: List[JtacInfo] = []
|
self.jtacs: List[JtacInfo] = []
|
||||||
|
|
||||||
def _enemy_stance(self):
|
def generate(self) -> None:
|
||||||
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
|
|
||||||
if len(self.enemy_planned_combat_groups) > len(
|
|
||||||
self.player_planned_combat_groups
|
|
||||||
):
|
|
||||||
return random.choice(
|
|
||||||
[
|
|
||||||
CombatStance.AGGRESSIVE,
|
|
||||||
CombatStance.AGGRESSIVE,
|
|
||||||
CombatStance.AGGRESSIVE,
|
|
||||||
CombatStance.ELIMINATION,
|
|
||||||
CombatStance.BREAKTHROUGH,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return random.choice(
|
|
||||||
[
|
|
||||||
CombatStance.DEFENSIVE,
|
|
||||||
CombatStance.DEFENSIVE,
|
|
||||||
CombatStance.DEFENSIVE,
|
|
||||||
CombatStance.AMBUSH,
|
|
||||||
CombatStance.AGGRESSIVE,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _group_point(point: Point, base_distance) -> Point:
|
|
||||||
distance = random.randint(
|
|
||||||
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
|
|
||||||
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
|
|
||||||
)
|
|
||||||
return point.random_point_within(
|
|
||||||
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
position = Conflict.frontline_position(
|
position = Conflict.frontline_position(
|
||||||
self.conflict.front_line, self.game.theater
|
self.conflict.front_line, self.game.theater
|
||||||
)
|
)
|
||||||
|
|
||||||
frontline_vector = Conflict.frontline_vector(
|
frontline_vector = Conflict.frontline_vector(
|
||||||
self.conflict.front_line, self.game.theater
|
self.conflict.front_line, self.game.theater
|
||||||
)
|
)
|
||||||
@ -150,6 +118,13 @@ class GroundConflictGenerator:
|
|||||||
self.enemy_planned_combat_groups, frontline_vector, False
|
self.enemy_planned_combat_groups, frontline_vector, False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: Differentiate AirConflict and GroundConflict classes.
|
||||||
|
if self.conflict.heading is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Cannot generate ground units for non-ground conflict. Ground unit "
|
||||||
|
"conflicts cannot have the heading `None`."
|
||||||
|
)
|
||||||
|
|
||||||
# Plan combat actions for groups
|
# Plan combat actions for groups
|
||||||
self.plan_action_for_groups(
|
self.plan_action_for_groups(
|
||||||
self.player_stance,
|
self.player_stance,
|
||||||
@ -169,16 +144,16 @@ class GroundConflictGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add JTAC
|
# Add JTAC
|
||||||
if self.game.player_faction.has_jtac:
|
if self.game.blue.faction.has_jtac:
|
||||||
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
|
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
|
||||||
code = 1688 - len(self.jtacs)
|
code = 1688 - len(self.jtacs)
|
||||||
|
|
||||||
utype = self.game.player_faction.jtac_unit
|
utype = self.game.blue.faction.jtac_unit
|
||||||
if self.game.player_faction.jtac_unit is None:
|
if utype is None:
|
||||||
utype = AircraftType.named("MQ-9 Reaper")
|
utype = AircraftType.named("MQ-9 Reaper")
|
||||||
|
|
||||||
jtac = self.mission.flight_group(
|
jtac = self.mission.flight_group(
|
||||||
country=self.mission.country(self.game.player_country),
|
country=self.mission.country(self.game.blue.country_name),
|
||||||
name=n,
|
name=n,
|
||||||
aircraft_type=utype.dcs_unit_type,
|
aircraft_type=utype.dcs_unit_type,
|
||||||
position=position[0],
|
position=position[0],
|
||||||
@ -361,7 +336,6 @@ class GroundConflictGenerator:
|
|||||||
self.mission.triggerrules.triggers.append(artillery_fallback)
|
self.mission.triggerrules.triggers.append(artillery_fallback)
|
||||||
|
|
||||||
for u in dcs_group.units:
|
for u in dcs_group.units:
|
||||||
u.initial = True
|
|
||||||
u.heading = forward_heading + random.randint(-5, 5)
|
u.heading = forward_heading + random.randint(-5, 5)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -570,10 +544,10 @@ class GroundConflictGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Fallback task
|
# Fallback task
|
||||||
fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
|
task = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
|
||||||
fallback.enabled = False
|
task.enabled = False
|
||||||
dcs_group.add_trigger_action(Hold())
|
dcs_group.add_trigger_action(Hold())
|
||||||
dcs_group.add_trigger_action(fallback)
|
dcs_group.add_trigger_action(task)
|
||||||
|
|
||||||
# Create trigger
|
# Create trigger
|
||||||
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
|
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
|
||||||
@ -634,7 +608,7 @@ class GroundConflictGenerator:
|
|||||||
@param enemy_groups Potential enemy groups
|
@param enemy_groups Potential enemy groups
|
||||||
@param n number of nearby groups to take
|
@param n number of nearby groups to take
|
||||||
"""
|
"""
|
||||||
targets = [] # type: List[Optional[VehicleGroup]]
|
targets = [] # type: List[VehicleGroup]
|
||||||
sorted_list = sorted(
|
sorted_list = sorted(
|
||||||
enemy_groups,
|
enemy_groups,
|
||||||
key=lambda group: player_group.points[0].position.distance_to_point(
|
key=lambda group: player_group.points[0].position.distance_to_point(
|
||||||
@ -658,7 +632,7 @@ class GroundConflictGenerator:
|
|||||||
@param group Group for which we should find the nearest ennemy
|
@param group Group for which we should find the nearest ennemy
|
||||||
@param enemy_groups Potential enemy groups
|
@param enemy_groups Potential enemy groups
|
||||||
"""
|
"""
|
||||||
min_distance = 99999999
|
min_distance = math.inf
|
||||||
target = None
|
target = None
|
||||||
for dcs_group, _ in enemy_groups:
|
for dcs_group, _ in enemy_groups:
|
||||||
dist = player_group.points[0].position.distance_to_point(
|
dist = player_group.points[0].position.distance_to_point(
|
||||||
@ -696,7 +670,7 @@ class GroundConflictGenerator:
|
|||||||
"""
|
"""
|
||||||
For artilery group, decide the distance from frontline with the range of the unit
|
For artilery group, decide the distance from frontline with the range of the unit
|
||||||
"""
|
"""
|
||||||
rg = getattr(group.unit_type.dcs_unit_type, "threat_range", 0) - 7500
|
rg = group.unit_type.dcs_unit_type.threat_range - 7500
|
||||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||||
rg = random.randint(
|
rg = random.randint(
|
||||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
||||||
@ -716,7 +690,7 @@ class GroundConflictGenerator:
|
|||||||
distance_from_frontline: int,
|
distance_from_frontline: int,
|
||||||
heading: int,
|
heading: int,
|
||||||
spawn_heading: int,
|
spawn_heading: int,
|
||||||
):
|
) -> Optional[Point]:
|
||||||
shifted = conflict_position.point_from_heading(
|
shifted = conflict_position.point_from_heading(
|
||||||
heading, random.randint(0, combat_width)
|
heading, random.randint(0, combat_width)
|
||||||
)
|
)
|
||||||
@ -741,7 +715,7 @@ class GroundConflictGenerator:
|
|||||||
if is_player
|
if is_player
|
||||||
else int(heading_sum(heading, 90))
|
else int(heading_sum(heading, 90))
|
||||||
)
|
)
|
||||||
country = self.game.player_country if is_player else self.game.enemy_country
|
country = self.game.coalition_for(is_player).country_name
|
||||||
for group in groups:
|
for group in groups:
|
||||||
if group.role == CombatGroupRole.ARTILLERY:
|
if group.role == CombatGroupRole.ARTILLERY:
|
||||||
distance_from_frontline = (
|
distance_from_frontline = (
|
||||||
@ -766,9 +740,9 @@ class GroundConflictGenerator:
|
|||||||
heading=opposite_heading(spawn_heading),
|
heading=opposite_heading(spawn_heading),
|
||||||
)
|
)
|
||||||
if is_player:
|
if is_player:
|
||||||
g.set_skill(self.game.settings.player_skill)
|
g.set_skill(Skill(self.game.settings.player_skill))
|
||||||
else:
|
else:
|
||||||
g.set_skill(self.game.settings.enemy_vehicle_skill)
|
g.set_skill(Skill(self.game.settings.enemy_vehicle_skill))
|
||||||
positioned_groups.append((g, group))
|
positioned_groups.append((g, group))
|
||||||
|
|
||||||
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
|
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
|
||||||
@ -790,7 +764,7 @@ class GroundConflictGenerator:
|
|||||||
count: int,
|
count: int,
|
||||||
at: Point,
|
at: Point,
|
||||||
move_formation: PointAction = PointAction.OffRoad,
|
move_formation: PointAction = PointAction.OffRoad,
|
||||||
heading=0,
|
heading: int = 0,
|
||||||
) -> VehicleGroup:
|
) -> VehicleGroup:
|
||||||
|
|
||||||
if side == self.conflict.attackers_country:
|
if side == self.conflict.attackers_country:
|
||||||
|
|||||||
@ -40,7 +40,6 @@ class Task:
|
|||||||
class PackageWaypoints:
|
class PackageWaypoints:
|
||||||
join: Point
|
join: Point
|
||||||
ingress: Point
|
ingress: Point
|
||||||
egress: Point
|
|
||||||
split: Point
|
split: Point
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
"""Support for working with DCS group callsigns."""
|
"""Support for working with DCS group callsigns."""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from dcs.unitgroup import FlyingGroup
|
from dcs.unitgroup import FlyingGroup
|
||||||
from dcs.flyingunit import FlyingUnit
|
from dcs.flyingunit import FlyingUnit
|
||||||
|
|
||||||
|
|
||||||
def callsign_for_support_unit(group: FlyingGroup) -> str:
|
def callsign_for_support_unit(group: FlyingGroup[Any]) -> str:
|
||||||
# Either something like Overlord11 for Western AWACS, or else just a number.
|
# Either something like Overlord11 for Western AWACS, or else just a number.
|
||||||
# Convert to either "Overlord" or "Flight 123".
|
# Convert to either "Overlord" or "Flight 123".
|
||||||
lead = group.units[0]
|
lead = group.units[0]
|
||||||
|
|||||||
@ -24,12 +24,13 @@ class CargoShipGenerator:
|
|||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
# Reset the count to make generation deterministic.
|
# Reset the count to make generation deterministic.
|
||||||
for ship in self.game.transfers.cargo_ships:
|
for coalition in self.game.coalitions:
|
||||||
self.generate_cargo_ship(ship)
|
for ship in coalition.transfers.cargo_ships:
|
||||||
|
self.generate_cargo_ship(ship)
|
||||||
|
|
||||||
def generate_cargo_ship(self, ship: CargoShip) -> ShipGroup:
|
def generate_cargo_ship(self, ship: CargoShip) -> ShipGroup:
|
||||||
country = self.mission.country(
|
country = self.mission.country(
|
||||||
self.game.player_country if ship.player_owned else self.game.enemy_country
|
self.game.coalition_for(ship.player_owned).country_name
|
||||||
)
|
)
|
||||||
waypoints = ship.route
|
waypoints = ship.route
|
||||||
group = self.mission.ship_group(
|
group = self.mission.ship_group(
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from game import db
|
from typing import Optional
|
||||||
|
|
||||||
|
from dcs.unitgroup import VehicleGroup
|
||||||
|
|
||||||
|
from game import db, Game
|
||||||
|
from game.theater.theatergroundobject import CoastalSiteGroundObject
|
||||||
from gen.coastal.silkworm import SilkwormGenerator
|
from gen.coastal.silkworm import SilkwormGenerator
|
||||||
|
|
||||||
COASTAL_MAP = {
|
COASTAL_MAP = {
|
||||||
@ -8,10 +13,13 @@ COASTAL_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def generate_coastal_group(game, ground_object, faction_name: str):
|
def generate_coastal_group(
|
||||||
|
game: Game, ground_object: CoastalSiteGroundObject, faction_name: str
|
||||||
|
) -> Optional[VehicleGroup]:
|
||||||
"""
|
"""
|
||||||
This generate a coastal defenses group
|
This generate a coastal defenses group
|
||||||
:return: Nothing, but put the group reference inside the ground object
|
:return: The generated group, or None if this faction does not support coastal
|
||||||
|
defenses.
|
||||||
"""
|
"""
|
||||||
faction = db.FACTIONS[faction_name]
|
faction = db.FACTIONS[faction_name]
|
||||||
if len(faction.coastal_defenses) > 0:
|
if len(faction.coastal_defenses) > 0:
|
||||||
|
|||||||
@ -1,14 +1,19 @@
|
|||||||
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
|
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
|
||||||
|
|
||||||
from gen.sam.group_generator import GroupGenerator
|
from game import Game
|
||||||
|
from game.factions.faction import Faction
|
||||||
|
from game.theater.theatergroundobject import CoastalSiteGroundObject
|
||||||
|
from gen.sam.group_generator import VehicleGroupGenerator
|
||||||
|
|
||||||
|
|
||||||
class SilkwormGenerator(GroupGenerator):
|
class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
|
||||||
def __init__(self, game, ground_object, faction):
|
def __init__(
|
||||||
|
self, game: Game, ground_object: CoastalSiteGroundObject, faction: Faction
|
||||||
|
) -> None:
|
||||||
super(SilkwormGenerator, self).__init__(game, ground_object)
|
super(SilkwormGenerator, self).__init__(game, ground_object)
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
|
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user