mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge remote-tracking branch 'khopa/develop' into helipads
# Conflicts: # changelog.md
This commit is contained in:
@@ -35,10 +35,10 @@ class DefaultSquadronAssigner:
|
||||
pass
|
||||
|
||||
def assign(self) -> None:
|
||||
for control_point, squadron_configs in self.config.by_location.items():
|
||||
if not control_point.is_friendly(self.coalition.player):
|
||||
continue
|
||||
for squadron_config in squadron_configs:
|
||||
for control_point in self.game.theater.control_points_for(
|
||||
self.coalition.player
|
||||
):
|
||||
for squadron_config in self.config.by_location[control_point]:
|
||||
squadron_def = self.find_squadron_for(squadron_config, control_point)
|
||||
if squadron_def is None:
|
||||
logging.info(
|
||||
|
||||
@@ -105,8 +105,7 @@ class MizCampaignLoader:
|
||||
|
||||
@staticmethod
|
||||
def control_point_from_airport(airport: Airport) -> ControlPoint:
|
||||
cp = Airfield(airport)
|
||||
cp.captured = airport.is_blue()
|
||||
cp = Airfield(airport, starts_blue=airport.is_blue())
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
@@ -249,30 +248,38 @@ class MizCampaignLoader:
|
||||
for blue in (False, True):
|
||||
for group in self.off_map_spawns(blue):
|
||||
control_point = OffMapSpawn(
|
||||
next(self.control_point_id), str(group.name), group.position
|
||||
next(self.control_point_id),
|
||||
str(group.name),
|
||||
group.position,
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for ship in self.carriers(blue):
|
||||
control_point = Carrier(
|
||||
ship.name, ship.position, next(self.control_point_id)
|
||||
ship.name,
|
||||
ship.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for ship in self.lhas(blue):
|
||||
control_point = Lha(
|
||||
ship.name, ship.position, next(self.control_point_id)
|
||||
ship.name,
|
||||
ship.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for fob in self.fobs(blue):
|
||||
control_point = Fob(
|
||||
str(fob.name), fob.position, next(self.control_point_id)
|
||||
str(fob.name),
|
||||
fob.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
|
||||
@@ -61,13 +61,256 @@ class SquadronDefGenerator:
|
||||
adjective = random.choice(
|
||||
(
|
||||
None,
|
||||
"Red",
|
||||
"Blue",
|
||||
"Green",
|
||||
"Golden",
|
||||
"Aggressive",
|
||||
"Alpha",
|
||||
"Ancient",
|
||||
"Angelic",
|
||||
"Angry",
|
||||
"Apoplectic",
|
||||
"Aquamarine",
|
||||
"Astral",
|
||||
"Avenging",
|
||||
"Azure",
|
||||
"Badass",
|
||||
"Barbaric",
|
||||
"Battle",
|
||||
"Battling",
|
||||
"Bellicose",
|
||||
"Belligerent",
|
||||
"Big",
|
||||
"Bionic",
|
||||
"Black",
|
||||
"Bladed",
|
||||
"Blazoned",
|
||||
"Blood",
|
||||
"Bloody",
|
||||
"Blue",
|
||||
"Bold",
|
||||
"Boxing",
|
||||
"Brash",
|
||||
"Brass",
|
||||
"Brave",
|
||||
"Brazen",
|
||||
"Brown",
|
||||
"Brutal",
|
||||
"Brzone",
|
||||
"Burning",
|
||||
"Buzzing",
|
||||
"Celestial",
|
||||
"Clever",
|
||||
"Cloud",
|
||||
"Cobalt",
|
||||
"Copper",
|
||||
"Coral",
|
||||
"Crazy",
|
||||
"Crimson",
|
||||
"Crouching",
|
||||
"Cursed",
|
||||
"Cyan",
|
||||
"Danger",
|
||||
"Dangerous",
|
||||
"Dapper",
|
||||
"Daring",
|
||||
"Dark",
|
||||
"Dawn",
|
||||
"Day",
|
||||
"Deadly",
|
||||
"Death",
|
||||
"Defiant",
|
||||
"Demon",
|
||||
"Desert",
|
||||
"Devil",
|
||||
"Devil's",
|
||||
"Diabolical",
|
||||
"Diamond",
|
||||
"Dire",
|
||||
"Dirty",
|
||||
"Doom",
|
||||
"Doomed",
|
||||
"Double",
|
||||
"Drunken",
|
||||
"Dusk",
|
||||
"Dusty",
|
||||
"Eager",
|
||||
"Ebony",
|
||||
"Electric",
|
||||
"Emerald",
|
||||
"Eternal",
|
||||
"Evil",
|
||||
"Faithful",
|
||||
"Famous",
|
||||
"Fanged",
|
||||
"Fearless",
|
||||
"Feisty",
|
||||
"Ferocious",
|
||||
"Fierce",
|
||||
"Fiery",
|
||||
"Fighting",
|
||||
"Fire",
|
||||
"First",
|
||||
"Flame",
|
||||
"Flaming",
|
||||
"Flying",
|
||||
"Forest",
|
||||
"Frenzied",
|
||||
"Frosty",
|
||||
"Frozen",
|
||||
"Furious",
|
||||
"Gallant",
|
||||
"Ghost",
|
||||
"Giant",
|
||||
"Gigantic",
|
||||
"Glaring",
|
||||
"Global",
|
||||
"Gold",
|
||||
"Golden",
|
||||
"Green",
|
||||
"Grey",
|
||||
"Grim",
|
||||
"Grizzly",
|
||||
"Growling",
|
||||
"Grumpy",
|
||||
"Hammer",
|
||||
"Hard",
|
||||
"Hardy",
|
||||
"Heavy",
|
||||
"Hell",
|
||||
"Hell's",
|
||||
"Hidden",
|
||||
"Homicidal",
|
||||
"Hostile",
|
||||
"Howling",
|
||||
"Hyper",
|
||||
"Ice",
|
||||
"Icy",
|
||||
"Immortal",
|
||||
"Indignant",
|
||||
"Infamous",
|
||||
"Invincible",
|
||||
"Iron",
|
||||
"Jolly",
|
||||
"Laser",
|
||||
"Lava",
|
||||
"Lavender",
|
||||
"Lethal",
|
||||
"Light",
|
||||
"Lightning",
|
||||
"Livid",
|
||||
"Lucky",
|
||||
"Mad",
|
||||
"Magenta",
|
||||
"Magma",
|
||||
"Maroon",
|
||||
"Menacing",
|
||||
"Merciless",
|
||||
"Metal",
|
||||
"Midnight",
|
||||
"Mighty",
|
||||
"Mithril",
|
||||
"Mocking",
|
||||
"Moon",
|
||||
"Mountain",
|
||||
"Muddy",
|
||||
"Nasty",
|
||||
"Naughty",
|
||||
"Night",
|
||||
"Nova",
|
||||
"Nutty",
|
||||
"Obsidian",
|
||||
"Ocean",
|
||||
"Oddball",
|
||||
"Old",
|
||||
"Omega",
|
||||
"Onyx",
|
||||
"Orange",
|
||||
"Perky",
|
||||
"Pink",
|
||||
"Power",
|
||||
"Prickly",
|
||||
"Proud",
|
||||
"Puckered",
|
||||
"Pugnacious",
|
||||
"Puking",
|
||||
"Purple",
|
||||
"Ragged",
|
||||
"Raging",
|
||||
"Rainbow",
|
||||
"Rampant",
|
||||
"Razor",
|
||||
"Ready",
|
||||
"Reaper",
|
||||
"Reckless",
|
||||
"Red",
|
||||
"Roaring",
|
||||
"Rocky",
|
||||
"Rolling",
|
||||
"Royal",
|
||||
"Rusty",
|
||||
"Sable",
|
||||
"Salty",
|
||||
"Sand",
|
||||
"Sarcastic",
|
||||
"Saucy",
|
||||
"Scarlet",
|
||||
"Scarred",
|
||||
"Scary",
|
||||
"Screaming",
|
||||
"Scythed",
|
||||
"Shadow",
|
||||
"Shiny",
|
||||
"Shocking",
|
||||
"Silver",
|
||||
"Sky",
|
||||
"Smoke",
|
||||
"Smokin'",
|
||||
"Snapping",
|
||||
"Snappy",
|
||||
"Snarling",
|
||||
"Snow",
|
||||
"Soaring",
|
||||
"Space",
|
||||
"Spiky",
|
||||
"Spiny",
|
||||
"Star",
|
||||
"Steady",
|
||||
"Steel",
|
||||
"Stone",
|
||||
"Storm",
|
||||
"Striking",
|
||||
"Strong",
|
||||
"Stubborn",
|
||||
"Sun",
|
||||
"Super",
|
||||
"Terrible",
|
||||
"Thorny",
|
||||
"Thunder",
|
||||
"Top",
|
||||
"Tough",
|
||||
"Toxic",
|
||||
"Tricky",
|
||||
"Turquoise",
|
||||
"Typhoon",
|
||||
"Ultimate",
|
||||
"Ultra",
|
||||
"Ultramarine",
|
||||
"Vengeful",
|
||||
"Venom",
|
||||
"Vermillion",
|
||||
"Vicious",
|
||||
"Victorious",
|
||||
"Vigilant",
|
||||
"Violent",
|
||||
"Violet",
|
||||
"War",
|
||||
"Water",
|
||||
"Whistling",
|
||||
"White",
|
||||
"Wicked",
|
||||
"Wild",
|
||||
"Wizard",
|
||||
"Wrathful",
|
||||
"Yellow",
|
||||
"Young",
|
||||
)
|
||||
)
|
||||
if adjective is None:
|
||||
|
||||
@@ -40,7 +40,7 @@ class Coalition:
|
||||
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
|
||||
self.bullseye = Bullseye(Point(0, 0))
|
||||
self.faker = Faker(self.faction.locales)
|
||||
self.air_wing = AirWing(game)
|
||||
self.air_wing = AirWing(player)
|
||||
self.transfers = PendingTransfers(game, player)
|
||||
|
||||
# Late initialized because the two coalitions in the game are mutually
|
||||
@@ -139,7 +139,7 @@ class Coalition:
|
||||
For more information on turn finalization in general, see the documentation for
|
||||
`Game.finish_turn`.
|
||||
"""
|
||||
self.air_wing.replenish()
|
||||
self.air_wing.end_turn()
|
||||
self.budget += Income(self.game, self.player).total
|
||||
|
||||
# Need to recompute before transfers and deliveries to account for captures.
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from game.commander.missionproposals import ProposedFlight
|
||||
from game.squadrons.airwing import AirWing
|
||||
from game.squadrons.squadron import Squadron
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import meters
|
||||
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, is_player: bool
|
||||
) -> None:
|
||||
self.air_wing = air_wing
|
||||
self.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
|
||||
def find_squadron_for_flight(
|
||||
self, target: MissionTarget, 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(target, flight, flight.task)
|
||||
|
||||
def find_aircraft_for_task(
|
||||
self, target: MissionTarget, flight: ProposedFlight, task: FlightType
|
||||
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||
types = aircraft_for_task(task)
|
||||
for airfield in self.closest_airfields.operational_airfields:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
continue
|
||||
for aircraft in types:
|
||||
if not airfield.can_operate(aircraft):
|
||||
continue
|
||||
distance_to_target = meters(target.distance_to(airfield))
|
||||
if distance_to_target > aircraft.max_mission_range:
|
||||
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, airfield
|
||||
)
|
||||
for squadron in squadrons:
|
||||
if squadron.operates_from(airfield) and squadron.can_fulfill_flight(
|
||||
flight.num_aircraft
|
||||
):
|
||||
return airfield, squadron
|
||||
return None
|
||||
@@ -157,10 +157,7 @@ class ObjectiveFinder:
|
||||
for control_point in self.enemy_control_points():
|
||||
if not isinstance(control_point, Airfield):
|
||||
continue
|
||||
if (
|
||||
control_point.allocated_aircraft(self.game).total_present
|
||||
>= min_aircraft
|
||||
):
|
||||
if control_point.allocated_aircraft().total_present >= min_aircraft:
|
||||
airfields.append(control_point)
|
||||
return self._targets_by_range(airfields)
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.commander.aircraftallocator import AircraftAllocator
|
||||
from game.commander.missionproposals import ProposedFlight
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.airwing import AirWing
|
||||
from game.theater import MissionTarget, OffMapSpawn, ControlPoint
|
||||
from game.utils import nautical_miles
|
||||
from gen.ato import Package
|
||||
from gen.flights.closestairfields import ClosestAirfields
|
||||
from game.theater import MissionTarget, OffMapSpawn, ControlPoint
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.airwing import AirWing
|
||||
from gen.flights.closestairfields import ClosestAirfields
|
||||
from .missionproposals import ProposedFlight
|
||||
|
||||
|
||||
class PackageBuilder:
|
||||
"""Builds a Package for the flights it receives."""
|
||||
|
||||
@@ -28,7 +32,7 @@ class PackageBuilder:
|
||||
self.is_player = is_player
|
||||
self.package_country = package_country
|
||||
self.package = Package(location, auto_asap=asap)
|
||||
self.allocator = AircraftAllocator(air_wing, closest_airfields, is_player)
|
||||
self.air_wing = air_wing
|
||||
self.start_type = start_type
|
||||
|
||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||
@@ -39,13 +43,13 @@ class PackageBuilder:
|
||||
caller should return any previously planned flights to the inventory
|
||||
using release_planned_aircraft.
|
||||
"""
|
||||
assignment = self.allocator.find_squadron_for_flight(self.package.target, plan)
|
||||
if assignment is None:
|
||||
squadron = self.air_wing.best_squadron_for(
|
||||
self.package.target, plan.task, plan.num_aircraft, this_turn=True
|
||||
)
|
||||
if squadron is None:
|
||||
return False
|
||||
airfield, squadron = assignment
|
||||
if isinstance(airfield, OffMapSpawn):
|
||||
start_type = "In Flight"
|
||||
else:
|
||||
start_type = squadron.location.required_aircraft_start_type
|
||||
if start_type is None:
|
||||
start_type = self.start_type
|
||||
|
||||
flight = Flight(
|
||||
@@ -55,9 +59,7 @@ class PackageBuilder:
|
||||
plan.num_aircraft,
|
||||
plan.task,
|
||||
start_type,
|
||||
departure=airfield,
|
||||
arrival=airfield,
|
||||
divert=self.find_divert_field(squadron.aircraft, airfield),
|
||||
divert=self.find_divert_field(squadron.aircraft, squadron.location),
|
||||
)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
@@ -8,7 +8,6 @@ from dcs.task import Task
|
||||
|
||||
from game import persistency
|
||||
from game.debriefing import Debriefing
|
||||
from game.infos.information import Information
|
||||
from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen.ato import AirTaskingOrder
|
||||
@@ -173,13 +172,10 @@ class Event:
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.kill()
|
||||
self.game.informations.append(
|
||||
Information(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.message(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -191,19 +187,16 @@ class Event:
|
||||
for captured in debriefing.base_captures:
|
||||
try:
|
||||
if captured.captured_by_player:
|
||||
info = Information(
|
||||
self.game.message(
|
||||
f"{captured.control_point} captured!",
|
||||
f"We took control of {captured.control_point}.",
|
||||
self.game.turn,
|
||||
)
|
||||
else:
|
||||
info = Information(
|
||||
self.game.message(
|
||||
f"{captured.control_point} lost!",
|
||||
f"The enemy took control of {captured.control_point}.",
|
||||
self.game.turn,
|
||||
)
|
||||
|
||||
self.game.informations.append(info)
|
||||
captured.control_point.capture(self.game, captured.captured_by_player)
|
||||
logging.info(f"Will run redeploy for {captured.control_point}")
|
||||
self.redeploy_units(captured.control_point)
|
||||
@@ -330,34 +323,28 @@ class Event:
|
||||
# Handle the case where there are no casualties at all on either side but both sides still have units
|
||||
if delta == 0.0:
|
||||
print(status_msg)
|
||||
info = Information(
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
else:
|
||||
if player_won:
|
||||
print(status_msg)
|
||||
cp.base.affect_strength(delta)
|
||||
enemy_cp.base.affect_strength(-delta)
|
||||
info = Information(
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
|
||||
self.game.turn,
|
||||
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
else:
|
||||
print(status_msg)
|
||||
enemy_cp.base.affect_strength(delta)
|
||||
cp.base.affect_strength(-delta)
|
||||
info = Information(
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} are losing ground against the enemy forces from "
|
||||
f"{enemy_cp.name}. {status_msg}",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
|
||||
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||
""" "
|
||||
@@ -410,10 +397,8 @@ class Event:
|
||||
total_units_redeployed += move_count
|
||||
|
||||
if total_units_redeployed > 0:
|
||||
text = (
|
||||
self.game.message(
|
||||
"Units redeployed",
|
||||
f"{total_units_redeployed} units have been redeployed from "
|
||||
f"{source.name} to {destination.name}"
|
||||
f"{source.name} to {destination.name}",
|
||||
)
|
||||
info = Information("Units redeployed", text, self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
logging.info(text)
|
||||
|
||||
19
game/game.py
19
game/game.py
@@ -107,8 +107,8 @@ class Game:
|
||||
self.game_stats = GameStats()
|
||||
self.notes = ""
|
||||
self.ground_planners: dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
self.informations: list[Information] = []
|
||||
self.message("Game Start", "-" * 40)
|
||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||
self.__culling_zones: List[Point] = []
|
||||
self.__destroyed_units: list[dict[str, Union[float, str]]] = []
|
||||
@@ -125,6 +125,9 @@ class Game:
|
||||
self.blue.set_opponent(self.red)
|
||||
self.red.set_opponent(self.blue)
|
||||
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.finish_init(self)
|
||||
|
||||
self.blue.configure_default_air_wing(air_wing_config)
|
||||
self.red.configure_default_air_wing(air_wing_config)
|
||||
|
||||
@@ -279,9 +282,7 @@ class Game:
|
||||
Args:
|
||||
skipped: True if the turn was skipped.
|
||||
"""
|
||||
self.informations.append(
|
||||
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
||||
)
|
||||
self.message("End of turn #" + str(self.turn), "-" * 40)
|
||||
self.turn += 1
|
||||
|
||||
# The coalition-specific turn finalization *must* happen before unit deliveries,
|
||||
@@ -297,10 +298,6 @@ class Game:
|
||||
if not skipped:
|
||||
for cp in self.theater.player_points():
|
||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
elif self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
if not cp.is_carrier and not cp.is_lha:
|
||||
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
@@ -414,8 +411,8 @@ class Game:
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
def message(self, title: str, text: str = "") -> None:
|
||||
self.informations.append(Information(title, text, turn=self.turn))
|
||||
|
||||
@property
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
|
||||
@@ -34,7 +34,7 @@ from gen.kneeboard import KneeboardGenerator
|
||||
from gen.lasercoderegistry import LaserCodeRegistry
|
||||
from gen.naming import namegen
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
from gen.tacan import TacanRegistry, TacanUsage
|
||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
from gen.visualgen import VisualGenerator
|
||||
from .. import db
|
||||
@@ -242,7 +242,7 @@ class Operation:
|
||||
if beacon.channel is None:
|
||||
logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
cls.tacan_registry.reserve(beacon.tacan_channel)
|
||||
cls.tacan_registry.mark_unavailable(beacon.tacan_channel)
|
||||
|
||||
@classmethod
|
||||
def _create_radio_registry(
|
||||
|
||||
@@ -74,7 +74,7 @@ class ProcurementAi:
|
||||
self.game.coalition_for(self.is_player).transfers
|
||||
)
|
||||
armor_investment += cp_ground_units.total_value
|
||||
cp_aircraft = cp.allocated_aircraft(self.game)
|
||||
cp_aircraft = cp.allocated_aircraft()
|
||||
aircraft_investment += cp_aircraft.total_value
|
||||
|
||||
total_investment = aircraft_investment + armor_investment
|
||||
@@ -221,45 +221,22 @@ class ProcurementAi:
|
||||
else:
|
||||
return self.game.theater.enemy_points()
|
||||
|
||||
@staticmethod
|
||||
def squadron_rank_for_task(squadron: Squadron, task: FlightType) -> int:
|
||||
return aircraft_for_task(task).index(squadron.aircraft)
|
||||
|
||||
def compatible_squadrons_at_airbase(
|
||||
self, airbase: ControlPoint, request: AircraftProcurementRequest
|
||||
) -> Iterator[Squadron]:
|
||||
compatible: list[Squadron] = []
|
||||
for squadron in airbase.squadrons:
|
||||
if not squadron.can_auto_assign(request.task_capability):
|
||||
continue
|
||||
if not squadron.can_provide_pilots(request.number):
|
||||
continue
|
||||
|
||||
distance_to_target = meters(request.near.distance_to(airbase))
|
||||
if distance_to_target > squadron.aircraft.max_mission_range:
|
||||
continue
|
||||
compatible.append(squadron)
|
||||
yield from sorted(
|
||||
compatible,
|
||||
key=lambda s: self.squadron_rank_for_task(s, request.task_capability),
|
||||
)
|
||||
|
||||
def best_squadrons_for(
|
||||
self, request: AircraftProcurementRequest
|
||||
) -> Iterator[Squadron]:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
|
||||
threatened = []
|
||||
for cp in distance_cache.operational_airfields:
|
||||
if not cp.is_friendly(self.is_player):
|
||||
for squadron in self.air_wing.best_squadrons_for(
|
||||
request.near, request.task_capability, request.number, this_turn=False
|
||||
):
|
||||
if not squadron.can_provide_pilots(request.number):
|
||||
continue
|
||||
if cp.unclaimed_parking(self.game) < request.number:
|
||||
if squadron.location.unclaimed_parking() < request.number:
|
||||
continue
|
||||
if self.threat_zones.threatened(cp.position):
|
||||
threatened.append(cp)
|
||||
if self.threat_zones.threatened(squadron.location.position):
|
||||
threatened.append(squadron)
|
||||
continue
|
||||
yield from self.compatible_squadrons_at_airbase(cp, request)
|
||||
for threatened_base in threatened:
|
||||
yield from self.compatible_squadrons_at_airbase(threatened_base, request)
|
||||
yield squadron
|
||||
yield from threatened
|
||||
|
||||
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
|
||||
worst_supply = math.inf
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from abc import abstractmethod
|
||||
from typing import TypeVar, Generic
|
||||
from typing import TypeVar, Generic, Any
|
||||
|
||||
from game import Game
|
||||
from game.coalition import Coalition
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ControlPoint
|
||||
|
||||
@@ -90,14 +92,15 @@ class PurchaseAdapter(Generic[ItemType]):
|
||||
def name_of(self, item: ItemType, multiline: bool = False) -> str:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def unit_type_of(self, item: ItemType) -> UnitType[Any]:
|
||||
...
|
||||
|
||||
|
||||
class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
|
||||
def __init__(
|
||||
self, control_point: ControlPoint, coalition: Coalition, game: Game
|
||||
) -> None:
|
||||
super().__init__(coalition)
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
super().__init__(control_point.coalition)
|
||||
self.control_point = control_point
|
||||
self.game = game
|
||||
|
||||
def pending_delivery_quantity(self, item: Squadron) -> int:
|
||||
return item.pending_deliveries
|
||||
@@ -106,10 +109,7 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
|
||||
return item.owned_aircraft
|
||||
|
||||
def can_buy(self, item: Squadron) -> bool:
|
||||
return (
|
||||
super().can_buy(item)
|
||||
and self.control_point.unclaimed_parking(self.game) > 0
|
||||
)
|
||||
return super().can_buy(item) and self.control_point.unclaimed_parking() > 0
|
||||
|
||||
def can_sell(self, item: Squadron) -> bool:
|
||||
return item.untasked_aircraft > 0
|
||||
@@ -138,6 +138,9 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
|
||||
separator = " "
|
||||
return separator.join([item.aircraft.name, str(item)])
|
||||
|
||||
def unit_type_of(self, item: Squadron) -> AircraftType:
|
||||
return item.aircraft
|
||||
|
||||
|
||||
class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]):
|
||||
def __init__(
|
||||
@@ -178,3 +181,6 @@ class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]):
|
||||
|
||||
def name_of(self, item: GroundUnitType, multiline: bool = False) -> str:
|
||||
return f"{item}"
|
||||
|
||||
def unit_type_of(self, item: GroundUnitType) -> GroundUnitType:
|
||||
return item
|
||||
|
||||
@@ -2,24 +2,24 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from typing import Sequence, Iterator, TYPE_CHECKING
|
||||
from typing import Sequence, Iterator, TYPE_CHECKING, Optional
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from .squadron import Squadron
|
||||
from ..theater import ControlPoint
|
||||
from ..theater import ControlPoint, MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class AirWing:
|
||||
def __init__(self, game: Game) -> None:
|
||||
self.game = game
|
||||
def __init__(self, player: bool) -> None:
|
||||
self.player = player
|
||||
self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
|
||||
|
||||
def add_squadron(self, squadron: Squadron) -> None:
|
||||
squadron.location.squadrons.append(squadron)
|
||||
self.squadrons[squadron.aircraft].append(squadron)
|
||||
|
||||
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
|
||||
@@ -32,6 +32,35 @@ class AirWing:
|
||||
except StopIteration:
|
||||
return False
|
||||
|
||||
def best_squadrons_for(
|
||||
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool
|
||||
) -> list[Squadron]:
|
||||
airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
best_aircraft = aircraft_for_task(task)
|
||||
ordered: list[Squadron] = []
|
||||
for control_point in airfield_cache.operational_airfields:
|
||||
if control_point.captured != self.player:
|
||||
continue
|
||||
capable_at_base = []
|
||||
for squadron in control_point.squadrons:
|
||||
if squadron.can_auto_assign_mission(location, task, size, this_turn):
|
||||
capable_at_base.append(squadron)
|
||||
|
||||
ordered.extend(
|
||||
sorted(
|
||||
capable_at_base,
|
||||
key=lambda s: best_aircraft.index(s.aircraft),
|
||||
)
|
||||
)
|
||||
return ordered
|
||||
|
||||
def best_squadron_for(
|
||||
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool
|
||||
) -> Optional[Squadron]:
|
||||
for squadron in self.best_squadrons_for(location, task, size, this_turn):
|
||||
return squadron
|
||||
return None
|
||||
|
||||
@property
|
||||
def available_aircraft_types(self) -> Iterator[AircraftType]:
|
||||
for aircraft, squadrons in self.squadrons.items():
|
||||
@@ -52,17 +81,6 @@ class AirWing:
|
||||
if squadron.can_auto_assign(task) and squadron.location == base:
|
||||
yield squadron
|
||||
|
||||
def auto_assignable_for_task_with_type(
|
||||
self, aircraft: AircraftType, task: FlightType, base: ControlPoint
|
||||
) -> Iterator[Squadron]:
|
||||
for squadron in self.squadrons_for(aircraft):
|
||||
if (
|
||||
squadron.location == base
|
||||
and squadron.can_auto_assign(task)
|
||||
and squadron.has_available_pilots
|
||||
):
|
||||
yield squadron
|
||||
|
||||
def squadron_for(self, aircraft: AircraftType) -> Squadron:
|
||||
return self.squadrons_for(aircraft)[0]
|
||||
|
||||
@@ -76,9 +94,9 @@ class AirWing:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.populate_for_turn_0()
|
||||
|
||||
def replenish(self) -> None:
|
||||
def end_turn(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.replenish_lost_pilots()
|
||||
squadron.end_turn()
|
||||
|
||||
def reset(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
|
||||
@@ -11,17 +11,20 @@ from typing import (
|
||||
|
||||
from faker import Faker
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.settings import AutoAtoBehavior, Settings
|
||||
from game.squadrons.operatingbases import OperatingBases
|
||||
from game.squadrons.pilot import Pilot, PilotStatus
|
||||
from game.squadrons.squadrondef import SquadronDef
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import FlightType, Flight
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from .pilot import Pilot, PilotStatus
|
||||
from ..utils import meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.coalition import Coalition
|
||||
from gen.flights.flight import FlightType
|
||||
from game.theater import ControlPoint
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.theater import ControlPoint, ConflictTheater, MissionTarget
|
||||
from .operatingbases import OperatingBases
|
||||
from .squadrondef import SquadronDef
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -53,6 +56,9 @@ class Squadron:
|
||||
settings: Settings = field(hash=False, compare=False)
|
||||
|
||||
location: ControlPoint
|
||||
destination: Optional[ControlPoint] = field(
|
||||
init=False, hash=False, compare=False, default=None
|
||||
)
|
||||
|
||||
owned_aircraft: int = field(init=False, hash=False, compare=False, default=0)
|
||||
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
|
||||
@@ -82,9 +88,7 @@ class Squadron:
|
||||
return self.coalition.player
|
||||
|
||||
def assign_to_base(self, base: ControlPoint) -> None:
|
||||
self.location.squadrons.remove(self)
|
||||
self.location = base
|
||||
self.location.squadrons.append(self)
|
||||
logging.debug(f"Assigned {self} to {base}")
|
||||
|
||||
@property
|
||||
@@ -169,6 +173,12 @@ class Squadron:
|
||||
raise ValueError("Squadrons can only be created with active pilots.")
|
||||
self._recruit_pilots(self.settings.squadron_pilot_limit)
|
||||
|
||||
def end_turn(self) -> None:
|
||||
if self.destination is not None:
|
||||
self.relocate_to(self.destination)
|
||||
self.replenish_lost_pilots()
|
||||
self.deliver_orders()
|
||||
|
||||
def replenish_lost_pilots(self) -> None:
|
||||
if not self.pilot_limits_enabled:
|
||||
return
|
||||
@@ -243,6 +253,17 @@ class Squadron:
|
||||
def can_auto_assign(self, task: FlightType) -> bool:
|
||||
return task in self.auto_assignable_mission_types
|
||||
|
||||
def can_auto_assign_mission(
|
||||
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool
|
||||
) -> bool:
|
||||
if not self.can_auto_assign(task):
|
||||
return False
|
||||
if this_turn and not self.can_fulfill_flight(size):
|
||||
return False
|
||||
|
||||
distance_to_target = meters(location.distance_to(self.location))
|
||||
return distance_to_target <= self.aircraft.max_mission_range
|
||||
|
||||
def operates_from(self, control_point: ControlPoint) -> bool:
|
||||
if control_point.is_carrier:
|
||||
return self.operating_bases.carrier
|
||||
@@ -265,18 +286,133 @@ class Squadron:
|
||||
def can_fulfill_flight(self, count: int) -> bool:
|
||||
return self.can_provide_pilots(count) and self.untasked_aircraft >= count
|
||||
|
||||
def refund_orders(self) -> None:
|
||||
self.coalition.adjust_budget(self.aircraft.price * self.pending_deliveries)
|
||||
self.pending_deliveries = 0
|
||||
def refund_orders(self, count: Optional[int] = None) -> None:
|
||||
if count is None:
|
||||
count = self.pending_deliveries
|
||||
self.coalition.adjust_budget(self.aircraft.price * count)
|
||||
self.pending_deliveries -= count
|
||||
|
||||
def deliver_orders(self) -> None:
|
||||
self.cancel_overflow_orders()
|
||||
self.owned_aircraft += self.pending_deliveries
|
||||
self.pending_deliveries = 0
|
||||
|
||||
def relocate_to(self, destination: ControlPoint) -> None:
|
||||
self.location = destination
|
||||
if self.location == self.destination:
|
||||
self.destination = None
|
||||
|
||||
def cancel_overflow_orders(self) -> None:
|
||||
if self.pending_deliveries <= 0:
|
||||
return
|
||||
overflow = -self.location.unclaimed_parking()
|
||||
if overflow > 0:
|
||||
sell_count = min(overflow, self.pending_deliveries)
|
||||
logging.debug(
|
||||
f"{self.location} is overfull by {overflow} aircraft. Cancelling "
|
||||
f"orders for {sell_count} aircraft to make room."
|
||||
)
|
||||
self.refund_orders(sell_count)
|
||||
|
||||
@property
|
||||
def max_fulfillable_aircraft(self) -> int:
|
||||
return max(self.number_of_available_pilots, self.untasked_aircraft)
|
||||
|
||||
@property
|
||||
def expected_size_next_turn(self) -> int:
|
||||
return self.owned_aircraft + self.pending_deliveries
|
||||
|
||||
@property
|
||||
def arrival(self) -> ControlPoint:
|
||||
return self.location if self.destination is None else self.destination
|
||||
|
||||
def plan_relocation(
|
||||
self, destination: ControlPoint, theater: ConflictTheater
|
||||
) -> None:
|
||||
if destination == self.location:
|
||||
logging.warning(
|
||||
f"Attempted to plan relocation of {self} to current location "
|
||||
f"{destination}. Ignoring."
|
||||
)
|
||||
return
|
||||
if destination == self.destination:
|
||||
logging.warning(
|
||||
f"Attempted to plan relocation of {self} to current destination "
|
||||
f"{destination}. Ignoring."
|
||||
)
|
||||
return
|
||||
|
||||
if self.expected_size_next_turn >= destination.unclaimed_parking():
|
||||
raise RuntimeError(f"Not enough parking for {self} at {destination}.")
|
||||
if not destination.can_operate(self.aircraft):
|
||||
raise RuntimeError(f"{self} cannot operate at {destination}.")
|
||||
self.destination = destination
|
||||
self.replan_ferry_flights(theater)
|
||||
|
||||
def cancel_relocation(self) -> None:
|
||||
if self.destination is None:
|
||||
logging.warning(
|
||||
f"Attempted to cancel relocation of squadron with no transfer order. "
|
||||
"Ignoring."
|
||||
)
|
||||
return
|
||||
|
||||
if self.expected_size_next_turn >= self.location.unclaimed_parking():
|
||||
raise RuntimeError(f"Not enough parking for {self} at {self.location}.")
|
||||
self.destination = None
|
||||
self.cancel_ferry_flights()
|
||||
|
||||
def replan_ferry_flights(self, theater: ConflictTheater) -> None:
|
||||
self.cancel_ferry_flights()
|
||||
self.plan_ferry_flights(theater)
|
||||
|
||||
def cancel_ferry_flights(self) -> None:
|
||||
for package in self.coalition.ato.packages:
|
||||
# Copy the list so our iterator remains consistent throughout the removal.
|
||||
for flight in list(package.flights):
|
||||
if flight.squadron == self and flight.flight_type is FlightType.FERRY:
|
||||
package.remove_flight(flight)
|
||||
flight.return_pilots_and_aircraft()
|
||||
if not package.flights:
|
||||
self.coalition.ato.remove_package(package)
|
||||
|
||||
def plan_ferry_flights(self, theater: ConflictTheater) -> None:
|
||||
if self.destination is None:
|
||||
raise RuntimeError(
|
||||
f"Cannot plan ferry flights for {self} because there is no destination."
|
||||
)
|
||||
remaining = self.untasked_aircraft
|
||||
if not remaining:
|
||||
return
|
||||
|
||||
package = Package(self.destination)
|
||||
builder = FlightPlanBuilder(package, self.coalition, theater)
|
||||
while remaining:
|
||||
size = min(remaining, self.aircraft.max_group_size)
|
||||
self.plan_ferry_flight(builder, package, size)
|
||||
remaining -= size
|
||||
package.set_tot_asap()
|
||||
self.coalition.ato.add_package(package)
|
||||
|
||||
def plan_ferry_flight(
|
||||
self, builder: FlightPlanBuilder, package: Package, size: int
|
||||
) -> None:
|
||||
start_type = self.location.required_aircraft_start_type
|
||||
if start_type is None:
|
||||
start_type = self.settings.default_start_type
|
||||
|
||||
flight = Flight(
|
||||
package,
|
||||
self.coalition.country_name,
|
||||
self,
|
||||
size,
|
||||
FlightType.FERRY,
|
||||
start_type,
|
||||
divert=None,
|
||||
)
|
||||
package.add_flight(flight)
|
||||
builder.populate_flight_plan(flight)
|
||||
|
||||
@classmethod
|
||||
def create_from(
|
||||
cls,
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
@@ -56,6 +57,7 @@ if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
from game.squadrons.squadron import Squadron
|
||||
from ..coalition import Coalition
|
||||
from ..transfers import PendingTransfers
|
||||
|
||||
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
|
||||
@@ -281,7 +283,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
position = None # type: Point
|
||||
name = None # type: str
|
||||
|
||||
captured = False
|
||||
has_frontline = True
|
||||
|
||||
alt = 0
|
||||
@@ -295,6 +296,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
name: str,
|
||||
position: Point,
|
||||
at: db.StartingPosition,
|
||||
starts_blue: bool,
|
||||
has_frontline: bool = True,
|
||||
cptype: ControlPointType = ControlPointType.AIRBASE,
|
||||
) -> None:
|
||||
@@ -303,11 +305,12 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.id = cp_id
|
||||
self.full_name = name
|
||||
self.at = at
|
||||
self.starts_blue = starts_blue
|
||||
self.connected_objectives: List[TheaterGroundObject[Any]] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
self.helipads: List[PointWithHeading] = []
|
||||
|
||||
self.captured = False
|
||||
self._coalition: Optional[Coalition] = None
|
||||
self.captured_invert = False
|
||||
# TODO: Should be Airbase specific.
|
||||
self.has_frontline = has_frontline
|
||||
@@ -324,15 +327,33 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
self.target_position: Optional[Point] = None
|
||||
|
||||
self.squadrons: list[Squadron] = []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__}: {self.name}>"
|
||||
|
||||
@property
|
||||
def coalition(self) -> Coalition:
|
||||
if self._coalition is None:
|
||||
raise RuntimeError("ControlPoint not fully initialized: coalition not set")
|
||||
return self._coalition
|
||||
|
||||
def finish_init(self, game: Game) -> None:
|
||||
assert self._coalition is None
|
||||
self._coalition = game.coalition_for(self.starts_blue)
|
||||
|
||||
@property
|
||||
def captured(self) -> bool:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> List[TheaterGroundObject[Any]]:
|
||||
return list(self.connected_objectives)
|
||||
|
||||
@property
|
||||
def squadrons(self) -> Iterator[Squadron]:
|
||||
for squadron in self.coalition.air_wing.iter_squadrons():
|
||||
if squadron.location == self:
|
||||
yield squadron
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def heading(self) -> Heading:
|
||||
@@ -564,36 +585,82 @@ class ControlPoint(MissionTarget, ABC):
|
||||
value = airframe.price * count
|
||||
game.adjust_budget(value, player=not self.captured)
|
||||
game.message(
|
||||
f"No valid retreat destination in range of {self.name} for {airframe}"
|
||||
f"No valid retreat destination in range of {self.name} for {airframe} "
|
||||
f"{count} aircraft have been captured and sold for ${value}M."
|
||||
)
|
||||
|
||||
def aircraft_retreat_destination(
|
||||
self, game: Game, airframe: AircraftType
|
||||
self, squadron: Squadron
|
||||
) -> Optional[ControlPoint]:
|
||||
closest = ObjectiveDistanceCache.get_closest_airfields(self)
|
||||
# TODO: Should be airframe dependent.
|
||||
max_retreat_distance = nautical_miles(200)
|
||||
max_retreat_distance = squadron.aircraft.max_mission_range
|
||||
# Skip the first airbase because that's the airbase we're retreating
|
||||
# from.
|
||||
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
|
||||
not_preferred: Optional[ControlPoint] = None
|
||||
overfull: list[ControlPoint] = []
|
||||
for airbase in airfields:
|
||||
if not airbase.can_operate(airframe):
|
||||
continue
|
||||
if airbase.captured != self.captured:
|
||||
continue
|
||||
if airbase.unclaimed_parking(game) > 0:
|
||||
return airbase
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _retreat_squadron(squadron: Squadron) -> None:
|
||||
logging.error("Air unit retreat not currently implemented")
|
||||
if airbase.unclaimed_parking() < squadron.owned_aircraft:
|
||||
if airbase.can_operate(squadron.aircraft):
|
||||
overfull.append(airbase)
|
||||
continue
|
||||
|
||||
if squadron.operates_from(airbase):
|
||||
# Has room, is a preferred base type for this squadron, and is the
|
||||
# closest choice. No need to keep looking.
|
||||
return airbase
|
||||
|
||||
if not_preferred is None and airbase.can_operate(squadron.aircraft):
|
||||
# Has room and is capable of operating from this base, but it isn't
|
||||
# preferred. Remember this option and use it if we can't find a
|
||||
# preferred base type with room.
|
||||
not_preferred = airbase
|
||||
if not_preferred is not None:
|
||||
# It's not our best choice but the other choices don't have room for the
|
||||
# squadron and would lead to aircraft being captured.
|
||||
return not_preferred
|
||||
|
||||
# No base was available with enough room. Find whichever base has the most room
|
||||
# available so we lose as little as possible. The overfull list is already
|
||||
# sorted by distance, and filtered for appropriate destinations.
|
||||
base_for_fewest_losses: Optional[ControlPoint] = None
|
||||
loss_count = math.inf
|
||||
for airbase in overfull:
|
||||
overflow = -(
|
||||
airbase.unclaimed_parking()
|
||||
- squadron.owned_aircraft
|
||||
- squadron.pending_deliveries
|
||||
)
|
||||
if overflow < loss_count:
|
||||
loss_count = overflow
|
||||
base_for_fewest_losses = airbase
|
||||
return base_for_fewest_losses
|
||||
|
||||
def _retreat_squadron(self, game: Game, squadron: Squadron) -> None:
|
||||
destination = self.aircraft_retreat_destination(squadron)
|
||||
if destination is None:
|
||||
squadron.refund_orders()
|
||||
self.capture_aircraft(game, squadron.aircraft, squadron.owned_aircraft)
|
||||
return
|
||||
logging.debug(f"{squadron} retreating to {destination} from {self}")
|
||||
squadron.relocate_to(destination)
|
||||
squadron.cancel_overflow_orders()
|
||||
overflow = -destination.unclaimed_parking()
|
||||
if overflow > 0:
|
||||
logging.debug(
|
||||
f"Not enough room for {squadron} at {destination}. Capturing "
|
||||
f"{overflow} aircraft."
|
||||
)
|
||||
self.capture_aircraft(game, squadron.aircraft, overflow)
|
||||
squadron.owned_aircraft -= overflow
|
||||
|
||||
def retreat_air_units(self, game: Game) -> None:
|
||||
# TODO: Capture in order of price to retain maximum value?
|
||||
for squadron in self.squadrons:
|
||||
self._retreat_squadron(squadron)
|
||||
self._retreat_squadron(game, squadron)
|
||||
|
||||
def depopulate_uncapturable_tgos(self) -> None:
|
||||
for tgo in self.connected_objectives:
|
||||
@@ -602,27 +669,25 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
coalition = game.coalition_for(for_player)
|
||||
self.ground_unit_orders.refund_all(coalition)
|
||||
for squadron in self.squadrons:
|
||||
squadron.refund_orders()
|
||||
new_coalition = game.coalition_for(for_player)
|
||||
self.ground_unit_orders.refund_all(self.coalition)
|
||||
self.retreat_ground_units(game)
|
||||
self.retreat_air_units(game)
|
||||
self.depopulate_uncapturable_tgos()
|
||||
|
||||
if for_player:
|
||||
self.captured = True
|
||||
else:
|
||||
self.captured = False
|
||||
|
||||
self._coalition = new_coalition
|
||||
self.base.set_strength_to_minimum()
|
||||
|
||||
@property
|
||||
def required_aircraft_start_type(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
...
|
||||
|
||||
def unclaimed_parking(self, game: Game) -> int:
|
||||
return self.total_aircraft_parking - self.allocated_aircraft(game).total
|
||||
def unclaimed_parking(self) -> int:
|
||||
return self.total_aircraft_parking - self.allocated_aircraft().total
|
||||
|
||||
@abstractmethod
|
||||
def active_runway(
|
||||
@@ -630,6 +695,10 @@ class ControlPoint(MissionTarget, ABC):
|
||||
) -> RunwayData:
|
||||
...
|
||||
|
||||
@property
|
||||
def airdrome_id_for_landing(self) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@property
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from []
|
||||
@@ -651,8 +720,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
def process_turn(self, game: Game) -> None:
|
||||
self.ground_unit_orders.process(game)
|
||||
for squadron in self.squadrons:
|
||||
squadron.deliver_orders()
|
||||
|
||||
runway_status = self.runway_status
|
||||
if runway_status is not None:
|
||||
@@ -674,16 +741,22 @@ class ControlPoint(MissionTarget, ABC):
|
||||
u.position.x = u.position.x + delta.x
|
||||
u.position.y = u.position.y + delta.y
|
||||
|
||||
def allocated_aircraft(self, _game: Game) -> AircraftAllocations:
|
||||
def allocated_aircraft(self) -> AircraftAllocations:
|
||||
present: dict[AircraftType, int] = defaultdict(int)
|
||||
on_order: dict[AircraftType, int] = defaultdict(int)
|
||||
transferring: dict[AircraftType, int] = defaultdict(int)
|
||||
for squadron in self.squadrons:
|
||||
present[squadron.aircraft] += squadron.owned_aircraft
|
||||
# TODO: Only if this is the squadron destination, not location.
|
||||
on_order[squadron.aircraft] += squadron.pending_deliveries
|
||||
if squadron.destination is None:
|
||||
on_order[squadron.aircraft] += squadron.pending_deliveries
|
||||
else:
|
||||
transferring[squadron.aircraft] -= squadron.owned_aircraft
|
||||
for squadron in self.coalition.air_wing.iter_squadrons():
|
||||
if squadron.destination == self:
|
||||
on_order[squadron.aircraft] += squadron.pending_deliveries
|
||||
transferring[squadron.aircraft] += squadron.owned_aircraft
|
||||
|
||||
# TODO: Implement squadron transfers.
|
||||
return AircraftAllocations(present, on_order, transferring={})
|
||||
return AircraftAllocations(present, on_order, transferring)
|
||||
|
||||
def allocated_ground_units(
|
||||
self, transfers: PendingTransfers
|
||||
@@ -795,13 +868,14 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
def __init__(self, airport: Airport, has_frontline: bool = True) -> None:
|
||||
def __init__(self, airport: Airport, starts_blue: bool) -> None:
|
||||
super().__init__(
|
||||
airport.id,
|
||||
airport.name,
|
||||
airport.position,
|
||||
airport,
|
||||
has_frontline,
|
||||
starts_blue,
|
||||
has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE,
|
||||
)
|
||||
self.airport = airport
|
||||
@@ -863,6 +937,10 @@ class Airfield(ControlPoint):
|
||||
assigner = RunwayAssigner(conditions)
|
||||
return assigner.get_preferred_runway(self.airport)
|
||||
|
||||
@property
|
||||
def airdrome_id_for_landing(self) -> Optional[int]:
|
||||
return self.airport.id
|
||||
|
||||
@property
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from self.airport.parking_slots
|
||||
@@ -970,12 +1048,13 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
|
||||
class Carrier(NavalControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool):
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
starts_blue,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
)
|
||||
@@ -1010,12 +1089,13 @@ class Carrier(NavalControlPoint):
|
||||
|
||||
|
||||
class Lha(NavalControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool):
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
starts_blue,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.LHA_GROUP,
|
||||
)
|
||||
@@ -1043,12 +1123,13 @@ class OffMapSpawn(ControlPoint):
|
||||
def runway_is_operational(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, cp_id: int, name: str, position: Point):
|
||||
def __init__(self, cp_id: int, name: str, position: Point, starts_blue: bool):
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
position,
|
||||
at=position,
|
||||
position,
|
||||
starts_blue,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.OFF_MAP,
|
||||
)
|
||||
@@ -1066,6 +1147,10 @@ class OffMapSpawn(ControlPoint):
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def required_aircraft_start_type(self) -> Optional[str]:
|
||||
return "In Flight"
|
||||
|
||||
@property
|
||||
def heading(self) -> Heading:
|
||||
return Heading.from_degrees(0)
|
||||
@@ -1096,12 +1181,13 @@ class OffMapSpawn(ControlPoint):
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool):
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
starts_blue,
|
||||
has_frontline=True,
|
||||
cptype=ControlPointType.FOB,
|
||||
)
|
||||
|
||||
@@ -8,13 +8,11 @@ from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, List, Set
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater import Carrier, Lha, PointWithHeading
|
||||
from game.theater import PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
@@ -30,7 +28,6 @@ from game.theater.theatergroundobject import (
|
||||
)
|
||||
from game.utils import Heading
|
||||
from game.version import VERSION
|
||||
from gen.naming import namegen
|
||||
from gen.coastal.coastal_group_generator import generate_coastal_group
|
||||
from gen.defenses.armor_group_generator import generate_armor_group
|
||||
from gen.fleet.ship_group_generator import (
|
||||
@@ -39,6 +36,7 @@ from gen.fleet.ship_group_generator import (
|
||||
generate_ship_group,
|
||||
)
|
||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.naming import namegen
|
||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||
from gen.sam.ewr_group_generator import generate_ewr_group
|
||||
from gen.sam.sam_group_generator import generate_anti_air_group
|
||||
@@ -61,7 +59,6 @@ class GeneratorSettings:
|
||||
start_date: datetime
|
||||
player_budget: int
|
||||
enemy_budget: int
|
||||
midgame: bool
|
||||
inverted: bool
|
||||
no_carrier: bool
|
||||
no_lha: bool
|
||||
@@ -91,13 +88,12 @@ class GameGenerator:
|
||||
generator_settings: GeneratorSettings,
|
||||
mod_settings: ModSettings,
|
||||
) -> None:
|
||||
self.player = player
|
||||
self.enemy = enemy
|
||||
self.player = player.apply_mod_settings(mod_settings)
|
||||
self.enemy = enemy.apply_mod_settings(mod_settings)
|
||||
self.theater = theater
|
||||
self.air_wing_config = air_wing_config
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
self.mod_settings = mod_settings
|
||||
|
||||
def generate(self) -> Game:
|
||||
with logged_duration("TGO population"):
|
||||
@@ -105,8 +101,8 @@ class GameGenerator:
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
game = Game(
|
||||
player_faction=self.player.apply_mod_settings(self.mod_settings),
|
||||
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings),
|
||||
player_faction=self.player,
|
||||
enemy_faction=self.enemy,
|
||||
theater=self.theater,
|
||||
air_wing_config=self.air_wing_config,
|
||||
start_date=self.generator_settings.start_date,
|
||||
@@ -119,37 +115,31 @@ class GameGenerator:
|
||||
game.settings.version = VERSION
|
||||
return game
|
||||
|
||||
def should_remove_carrier(self, player: bool) -> bool:
|
||||
faction = self.player if player else self.enemy
|
||||
return self.generator_settings.no_carrier or not faction.carrier_names
|
||||
|
||||
def should_remove_lha(self, player: bool) -> bool:
|
||||
faction = self.player if player else self.enemy
|
||||
return self.generator_settings.no_lha or not faction.helicopter_carrier_names
|
||||
|
||||
def prepare_theater(self) -> None:
|
||||
to_remove: List[ControlPoint] = []
|
||||
# Auto-capture half the bases if midgame.
|
||||
if self.generator_settings.midgame:
|
||||
control_points = self.theater.controlpoints
|
||||
for control_point in control_points[: len(control_points) // 2]:
|
||||
control_point.captured = True
|
||||
|
||||
# Remove carrier and lha, invert situation if needed
|
||||
for cp in self.theater.controlpoints:
|
||||
if isinstance(cp, Carrier) and self.generator_settings.no_carrier:
|
||||
to_remove.append(cp)
|
||||
elif isinstance(cp, Lha) and self.generator_settings.no_lha:
|
||||
to_remove.append(cp)
|
||||
|
||||
if self.generator_settings.inverted:
|
||||
cp.captured = cp.captured_invert
|
||||
cp.starts_blue = cp.captured_invert
|
||||
|
||||
if cp.is_carrier and self.should_remove_carrier(cp.starts_blue):
|
||||
to_remove.append(cp)
|
||||
elif cp.is_lha and self.should_remove_lha(cp.starts_blue):
|
||||
to_remove.append(cp)
|
||||
|
||||
# do remove
|
||||
for cp in to_remove:
|
||||
self.theater.controlpoints.remove(cp)
|
||||
|
||||
# TODO: Fix this. This captures all bases for blue.
|
||||
# reapply midgame inverted if needed
|
||||
if self.generator_settings.midgame and self.generator_settings.inverted:
|
||||
for i, cp in enumerate(reversed(self.theater.controlpoints)):
|
||||
if i > len(self.theater.controlpoints):
|
||||
break
|
||||
else:
|
||||
cp.captured = True
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(
|
||||
|
||||
@@ -349,15 +349,17 @@ class AirliftPlanner:
|
||||
else:
|
||||
transfer = self.transfer
|
||||
|
||||
start_type = squadron.location.required_aircraft_start_type
|
||||
if start_type is None:
|
||||
start_type = self.game.settings.default_start_type
|
||||
|
||||
flight = Flight(
|
||||
self.package,
|
||||
self.game.country_for(squadron.player),
|
||||
squadron,
|
||||
flight_size,
|
||||
FlightType.TRANSPORT,
|
||||
self.game.settings.default_start_type,
|
||||
departure=squadron.location,
|
||||
arrival=squadron.location,
|
||||
start_type,
|
||||
divert=None,
|
||||
cargo=transfer,
|
||||
)
|
||||
@@ -752,7 +754,7 @@ class PendingTransfers:
|
||||
)
|
||||
|
||||
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
||||
unclaimed_parking = control_point.unclaimed_parking(self.game)
|
||||
unclaimed_parking = control_point.unclaimed_parking()
|
||||
# Buy a maximum of unclaimed_parking only to prevent that aircraft procurement
|
||||
# take place at another base
|
||||
gap = min(
|
||||
|
||||
Reference in New Issue
Block a user