Merge remote-tracking branch 'khopa/develop' into helipads

# Conflicts:
#	changelog.md
This commit is contained in:
Khopa 2021-09-08 21:56:45 +02:00
commit e84e36fd22
132 changed files with 2856 additions and 648 deletions

View File

@ -36,6 +36,11 @@ jobs:
run: | run: |
./venv/scripts/activate ./venv/scripts/activate
mypy gen mypy gen
- name: mypy tests
run: |
./venv/scripts/activate
mypy tests
- name: update build number - name: update build number
run: | run: |

33
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Test
on: [push, pull_request]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install environment
run: |
python -m venv ./venv
- name: Install dependencies
run: |
./venv/scripts/activate
python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
- name: run tests
run: |
./venv/scripts/activate
pytest tests

View File

@ -8,26 +8,32 @@ Saves from 4.x are not compatible with 5.0.
* **[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]** 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]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts. * **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts.
* **[Campaign]** FOBs control point can have FARP/helipad slot and host helicopters. To enable this feature on a FOB, add "Invisible FARP" statics objects near the FOB location in the campaign definition file. * **[Campaign]** FOBs control point can have FARP/helipad slot and host helicopters. To enable this feature on a FOB, add "Invisible FARP" statics objects near the FOB location in the campaign definition file.
* **[Campaign]** (WIP) Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status. * **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status.
* **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers. * **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers.
* **[Campaign]** Skipped turns are no longer counted as defeats on front lines.
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions. * **[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. * **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
* **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points. * **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points.
* **[Campaign AI]** Auto-planning mission range limits are now specified per-aircraft. On average this means that longer range missions will now be plannable. The limit only accounts for the direct distance to the target, not the path taken. * **[Campaign AI]** Auto-planning mission range limits are now specified per-aircraft. On average this means that longer range missions will now be plannable. The limit only accounts for the direct distance to the target, not the path taken.
* **[Campaign AI]** Transport aircraft will now be bought only if necessary at control points which can produce ground units and are capable to operate transport aircraft. * **[Campaign AI]** Transport aircraft will now be bought only if necessary at control points which can produce ground units and are capable to operate transport aircraft.
* **[Campaign AI]** Aircraft will now only be automatically purchased or assigned at appropriate bases. Naval aircraft will default to only operating from carriers, Harriers will default to LHAs and shore bases, helicopters will operate from anywhere. This can be customized per-squadron. * **[Campaign AI]** Aircraft will now only be automatically purchased or assigned at appropriate bases. Naval aircraft will default to only operating from carriers, Harriers will default to LHAs and shore bases, helicopters will operate from anywhere. This can be customized per-squadron.
* **[Engine]** Support for DCS 2.7.5.10869 and newer, including support for F-16 CBU-105s.
* **[Mission Generation]** EWRs are now also headed towards the center of the conflict * **[Mission Generation]** EWRs are now also headed towards the center of the conflict
* **[Mission Generation]** FACs can now use FC3 compatible laser codes. Note that this setting is global, not per FAC.
* **[Modding]** Campaigns now specify the squadrons that are present in the campaign, their roles, and their starting bases. Players can customize this at game start but the campaign will choose the defaults. * **[Modding]** Campaigns now specify the squadrons that are present in the campaign, their roles, and their starting bases. Players can customize this at game start but the campaign will choose the defaults.
* **[Kneeboard]** Minimum required fuel estimates have been added to the kneeboard for aircraft with supporting data (currently only the Hornet). * **[Kneeboard]** Minimum required fuel estimates have been added to the kneeboard for aircraft with supporting data (currently only the Hornet).
* **[Kneeboard]** QNH (pressure MSL) and temperature have been added to the kneeboard. * **[Kneeboard]** QNH (pressure MSL) and temperature have been added to the kneeboard.
* **[New Game Wizard]** Can now customize the player's air wing before campaign start to disable, relocate, or rename squadrons. * **[New Game Wizard]** Can now customize the player's air wing before campaign start to disable, relocate, or rename squadrons.
* **[UI]** Sell Button for aircraft will be disabled if there are no units available to be sold or all are already assigned to a mission * **[UI]** Sell Button for aircraft will be disabled if there are no units available to be sold or all are already assigned to a mission
* **[UI]** Enemy aircraft inventory now viewable in the air wing menu.
## Fixes ## Fixes
* **[Campaign]** Naval control points will no longer claim ground objectives during campaign generation and prevent them from spawning. * **[Campaign]** Naval control points will no longer claim ground objectives during campaign generation and prevent them from spawning.
* **[Mission Generation]** Mission results and other files will now be opened with enforced utf-8 encoding to prevent an issue where destroyed ground units were untracked because of special characters in their names. * **[Mission Generation]** Mission results and other files will now be opened with enforced utf-8 encoding to prevent an issue where destroyed ground units were untracked because of special characters in their names.
* **[Mission Generation]** Fixed generation of landing waypoints so that the AI obeys them.
* **[UI]** Selling of Units is now visible again in the UI dialog and shows the correct amount of sold units * **[UI]** Selling of Units is now visible again in the UI dialog and shows the correct amount of sold units
* **[UI]** Fixed bug where an incompatible campaign could be generated if no action is taken on the campaign selection screen.
# 4.1.1 # 4.1.1

View File

@ -35,10 +35,10 @@ class DefaultSquadronAssigner:
pass pass
def assign(self) -> None: def assign(self) -> None:
for control_point, squadron_configs in self.config.by_location.items(): for control_point in self.game.theater.control_points_for(
if not control_point.is_friendly(self.coalition.player): self.coalition.player
continue ):
for squadron_config in squadron_configs: for squadron_config in self.config.by_location[control_point]:
squadron_def = self.find_squadron_for(squadron_config, control_point) squadron_def = self.find_squadron_for(squadron_config, control_point)
if squadron_def is None: if squadron_def is None:
logging.info( logging.info(

View File

@ -105,8 +105,7 @@ class MizCampaignLoader:
@staticmethod @staticmethod
def control_point_from_airport(airport: Airport) -> ControlPoint: def control_point_from_airport(airport: Airport) -> ControlPoint:
cp = Airfield(airport) cp = Airfield(airport, starts_blue=airport.is_blue())
cp.captured = airport.is_blue()
# Use the unlimited aircraft option to determine if an airfield should # Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted". # be owned by the player when the campaign is "inverted".
@ -249,30 +248,38 @@ class MizCampaignLoader:
for blue in (False, True): for blue in (False, True):
for group in self.off_map_spawns(blue): for group in self.off_map_spawns(blue):
control_point = OffMapSpawn( 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_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for ship in self.carriers(blue): for ship in self.carriers(blue):
control_point = Carrier( 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_point.captured_invert = ship.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for ship in self.lhas(blue): for ship in self.lhas(blue):
control_point = Lha( 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_point.captured_invert = ship.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for fob in self.fobs(blue): for fob in self.fobs(blue):
control_point = Fob( 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_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point

View File

@ -61,13 +61,256 @@ class SquadronDefGenerator:
adjective = random.choice( adjective = random.choice(
( (
None, None,
"Red", "Aggressive",
"Blue", "Alpha",
"Green", "Ancient",
"Golden", "Angelic",
"Angry",
"Apoplectic",
"Aquamarine",
"Astral",
"Avenging",
"Azure",
"Badass",
"Barbaric",
"Battle",
"Battling",
"Bellicose",
"Belligerent",
"Big",
"Bionic",
"Black", "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", "Fighting",
"Fire",
"First",
"Flame",
"Flaming",
"Flying", "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: if adjective is None:

View File

@ -40,7 +40,7 @@ class Coalition:
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet() self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
self.bullseye = Bullseye(Point(0, 0)) self.bullseye = Bullseye(Point(0, 0))
self.faker = Faker(self.faction.locales) self.faker = Faker(self.faction.locales)
self.air_wing = AirWing(game) self.air_wing = AirWing(player)
self.transfers = PendingTransfers(game, player) self.transfers = PendingTransfers(game, player)
# Late initialized because the two coalitions in the game are mutually # 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 For more information on turn finalization in general, see the documentation for
`Game.finish_turn`. `Game.finish_turn`.
""" """
self.air_wing.replenish() self.air_wing.end_turn()
self.budget += Income(self.game, self.player).total self.budget += Income(self.game, self.player).total
# Need to recompute before transfers and deliveries to account for captures. # Need to recompute before transfers and deliveries to account for captures.

View File

@ -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

View File

@ -157,10 +157,7 @@ class ObjectiveFinder:
for control_point in self.enemy_control_points(): for control_point in self.enemy_control_points():
if not isinstance(control_point, Airfield): if not isinstance(control_point, Airfield):
continue continue
if ( if control_point.allocated_aircraft().total_present >= min_aircraft:
control_point.allocated_aircraft(self.game).total_present
>= min_aircraft
):
airfields.append(control_point) airfields.append(control_point)
return self._targets_by_range(airfields) return self._targets_by_range(airfields)

View File

@ -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 game.utils import nautical_miles
from gen.ato import Package from gen.ato import Package
from gen.flights.closestairfields import ClosestAirfields from game.theater import MissionTarget, OffMapSpawn, ControlPoint
from gen.flights.flight import Flight 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: class PackageBuilder:
"""Builds a Package for the flights it receives.""" """Builds a Package for the flights it receives."""
@ -28,7 +32,7 @@ class PackageBuilder:
self.is_player = is_player self.is_player = is_player
self.package_country = package_country self.package_country = package_country
self.package = Package(location, auto_asap=asap) 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 self.start_type = start_type
def plan_flight(self, plan: ProposedFlight) -> bool: def plan_flight(self, plan: ProposedFlight) -> bool:
@ -39,13 +43,13 @@ class PackageBuilder:
caller should return any previously planned flights to the inventory caller should return any previously planned flights to the inventory
using release_planned_aircraft. using release_planned_aircraft.
""" """
assignment = self.allocator.find_squadron_for_flight(self.package.target, plan) squadron = self.air_wing.best_squadron_for(
if assignment is None: self.package.target, plan.task, plan.num_aircraft, this_turn=True
)
if squadron is None:
return False return False
airfield, squadron = assignment start_type = squadron.location.required_aircraft_start_type
if isinstance(airfield, OffMapSpawn): if start_type is None:
start_type = "In Flight"
else:
start_type = self.start_type start_type = self.start_type
flight = Flight( flight = Flight(
@ -55,9 +59,7 @@ class PackageBuilder:
plan.num_aircraft, plan.num_aircraft,
plan.task, plan.task,
start_type, start_type,
departure=airfield, divert=self.find_divert_field(squadron.aircraft, squadron.location),
arrival=airfield,
divert=self.find_divert_field(squadron.aircraft, airfield),
) )
self.package.add_flight(flight) self.package.add_flight(flight)
return True return True

View File

@ -8,7 +8,6 @@ from dcs.task import Task
from game import persistency from game import persistency
from game.debriefing import Debriefing from game.debriefing import Debriefing
from game.infos.information import Information
from game.operation.operation import Operation from game.operation.operation import Operation
from game.theater import ControlPoint from game.theater import ControlPoint
from gen.ato import AirTaskingOrder from gen.ato import AirTaskingOrder
@ -173,13 +172,10 @@ class Event:
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:
loss.ground_object.kill() loss.ground_object.kill()
self.game.informations.append( self.game.message(
Information( "Building destroyed",
"Building destroyed", f"{loss.ground_object.dcs_identifier} has been destroyed at "
f"{loss.ground_object.dcs_identifier} has been destroyed at " f"location {loss.ground_object.obj_name}",
f"location {loss.ground_object.obj_name}",
self.game.turn,
)
) )
@staticmethod @staticmethod
@ -191,19 +187,16 @@ class Event:
for captured in debriefing.base_captures: for captured in debriefing.base_captures:
try: try:
if captured.captured_by_player: if captured.captured_by_player:
info = Information( self.game.message(
f"{captured.control_point} captured!", f"{captured.control_point} captured!",
f"We took control of {captured.control_point}.", f"We took control of {captured.control_point}.",
self.game.turn,
) )
else: else:
info = Information( self.game.message(
f"{captured.control_point} lost!", f"{captured.control_point} lost!",
f"The enemy took control of {captured.control_point}.", 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) captured.control_point.capture(self.game, captured.captured_by_player)
logging.info(f"Will run redeploy for {captured.control_point}") logging.info(f"Will run redeploy for {captured.control_point}")
self.redeploy_units(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 # Handle the case where there are no casualties at all on either side but both sides still have units
if delta == 0.0: if delta == 0.0:
print(status_msg) print(status_msg)
info = Information( self.game.message(
"Frontline Report", "Frontline Report",
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.", 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: else:
if player_won: if player_won:
print(status_msg) print(status_msg)
cp.base.affect_strength(delta) cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta) enemy_cp.base.affect_strength(-delta)
info = Information( self.game.message(
"Frontline Report", "Frontline Report",
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}", f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
self.game.turn,
) )
self.game.informations.append(info)
else: else:
print(status_msg) print(status_msg)
enemy_cp.base.affect_strength(delta) enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta) cp.base.affect_strength(-delta)
info = Information( self.game.message(
"Frontline Report", "Frontline Report",
f"Our ground forces from {cp.name} are losing ground against the enemy forces from " f"Our ground forces from {cp.name} are losing ground against the enemy forces from "
f"{enemy_cp.name}. {status_msg}", f"{enemy_cp.name}. {status_msg}",
self.game.turn,
) )
self.game.informations.append(info)
def redeploy_units(self, cp: ControlPoint) -> None: def redeploy_units(self, cp: ControlPoint) -> None:
""" " """ "
@ -410,10 +397,8 @@ class Event:
total_units_redeployed += move_count total_units_redeployed += move_count
if total_units_redeployed > 0: if total_units_redeployed > 0:
text = ( self.game.message(
"Units redeployed",
f"{total_units_redeployed} units have been redeployed from " 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)

View File

@ -107,8 +107,8 @@ class Game:
self.game_stats = GameStats() self.game_stats = GameStats()
self.notes = "" self.notes = ""
self.ground_planners: dict[int, GroundPlanner] = {} self.ground_planners: dict[int, GroundPlanner] = {}
self.informations = [] self.informations: list[Information] = []
self.informations.append(Information("Game Start", "-" * 40, 0)) self.message("Game Start", "-" * 40)
# 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[dict[str, Union[float, str]]] = [] self.__destroyed_units: list[dict[str, Union[float, str]]] = []
@ -125,6 +125,9 @@ class Game:
self.blue.set_opponent(self.red) self.blue.set_opponent(self.red)
self.red.set_opponent(self.blue) 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.blue.configure_default_air_wing(air_wing_config)
self.red.configure_default_air_wing(air_wing_config) self.red.configure_default_air_wing(air_wing_config)
@ -279,9 +282,7 @@ class Game:
Args: Args:
skipped: True if the turn was skipped. skipped: True if the turn was skipped.
""" """
self.informations.append( self.message("End of turn #" + str(self.turn), "-" * 40)
Information("End of turn #" + str(self.turn), "-" * 40, 0)
)
self.turn += 1 self.turn += 1
# The coalition-specific turn finalization *must* happen before unit deliveries, # The coalition-specific turn finalization *must* happen before unit deliveries,
@ -297,10 +298,6 @@ class Game:
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)
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() self.conditions = self.generate_conditions()
@ -414,8 +411,8 @@ class Game:
gplanner.plan_groundwar() gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner self.ground_planners[cp.id] = gplanner
def message(self, text: str) -> None: def message(self, title: str, text: str = "") -> None:
self.informations.append(Information(text, turn=self.turn)) self.informations.append(Information(title, text, turn=self.turn))
@property @property
def current_turn_time_of_day(self) -> TimeOfDay: def current_turn_time_of_day(self) -> TimeOfDay:

View File

@ -34,7 +34,7 @@ from gen.kneeboard import KneeboardGenerator
from gen.lasercoderegistry import LaserCodeRegistry from gen.lasercoderegistry import LaserCodeRegistry
from gen.naming import namegen from gen.naming import namegen
from gen.radios import RadioFrequency, RadioRegistry 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.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from gen.visualgen import VisualGenerator from gen.visualgen import VisualGenerator
from .. import db from .. import db
@ -242,7 +242,7 @@ class Operation:
if beacon.channel is None: if beacon.channel is None:
logging.error(f"TACAN beacon has no channel: {beacon.callsign}") logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
else: else:
cls.tacan_registry.reserve(beacon.tacan_channel) cls.tacan_registry.mark_unavailable(beacon.tacan_channel)
@classmethod @classmethod
def _create_radio_registry( def _create_radio_registry(

View File

@ -74,7 +74,7 @@ class ProcurementAi:
self.game.coalition_for(self.is_player).transfers 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()
aircraft_investment += cp_aircraft.total_value aircraft_investment += cp_aircraft.total_value
total_investment = aircraft_investment + armor_investment total_investment = aircraft_investment + armor_investment
@ -221,45 +221,22 @@ class ProcurementAi:
else: else:
return self.game.theater.enemy_points() 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( def best_squadrons_for(
self, request: AircraftProcurementRequest self, request: AircraftProcurementRequest
) -> Iterator[Squadron]: ) -> Iterator[Squadron]:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
threatened = [] threatened = []
for cp in distance_cache.operational_airfields: for squadron in self.air_wing.best_squadrons_for(
if not cp.is_friendly(self.is_player): request.near, request.task_capability, request.number, this_turn=False
):
if not squadron.can_provide_pilots(request.number):
continue continue
if cp.unclaimed_parking(self.game) < request.number: if squadron.location.unclaimed_parking() < request.number:
continue continue
if self.threat_zones.threatened(cp.position): if self.threat_zones.threatened(squadron.location.position):
threatened.append(cp) threatened.append(squadron)
continue continue
yield from self.compatible_squadrons_at_airbase(cp, request) yield squadron
for threatened_base in threatened: yield from threatened
yield from self.compatible_squadrons_at_airbase(threatened_base, request)
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]: def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
worst_supply = math.inf worst_supply = math.inf

View File

@ -1,9 +1,11 @@
from abc import abstractmethod from abc import abstractmethod
from typing import TypeVar, Generic from typing import TypeVar, Generic, Any
from game import Game from game import Game
from game.coalition import Coalition from game.coalition import Coalition
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.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ControlPoint from game.theater import ControlPoint
@ -90,14 +92,15 @@ class PurchaseAdapter(Generic[ItemType]):
def name_of(self, item: ItemType, multiline: bool = False) -> str: def name_of(self, item: ItemType, multiline: bool = False) -> str:
... ...
@abstractmethod
def unit_type_of(self, item: ItemType) -> UnitType[Any]:
...
class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]): class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
def __init__( def __init__(self, control_point: ControlPoint) -> None:
self, control_point: ControlPoint, coalition: Coalition, game: Game super().__init__(control_point.coalition)
) -> None:
super().__init__(coalition)
self.control_point = control_point self.control_point = control_point
self.game = game
def pending_delivery_quantity(self, item: Squadron) -> int: def pending_delivery_quantity(self, item: Squadron) -> int:
return item.pending_deliveries return item.pending_deliveries
@ -106,10 +109,7 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
return item.owned_aircraft return item.owned_aircraft
def can_buy(self, item: Squadron) -> bool: def can_buy(self, item: Squadron) -> bool:
return ( return super().can_buy(item) and self.control_point.unclaimed_parking() > 0
super().can_buy(item)
and self.control_point.unclaimed_parking(self.game) > 0
)
def can_sell(self, item: Squadron) -> bool: def can_sell(self, item: Squadron) -> bool:
return item.untasked_aircraft > 0 return item.untasked_aircraft > 0
@ -138,6 +138,9 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
separator = " " separator = " "
return separator.join([item.aircraft.name, str(item)]) return separator.join([item.aircraft.name, str(item)])
def unit_type_of(self, item: Squadron) -> AircraftType:
return item.aircraft
class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]): class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]):
def __init__( def __init__(
@ -178,3 +181,6 @@ class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]):
def name_of(self, item: GroundUnitType, multiline: bool = False) -> str: def name_of(self, item: GroundUnitType, multiline: bool = False) -> str:
return f"{item}" return f"{item}"
def unit_type_of(self, item: GroundUnitType) -> GroundUnitType:
return item

View File

@ -2,24 +2,24 @@ from __future__ import annotations
import itertools import itertools
from collections import defaultdict 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 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 .squadron import Squadron
from ..theater import ControlPoint from ..theater import ControlPoint, MissionTarget
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
class AirWing: class AirWing:
def __init__(self, game: Game) -> None: def __init__(self, player: bool) -> None:
self.game = game self.player = player
self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
def add_squadron(self, squadron: Squadron) -> None: def add_squadron(self, squadron: Squadron) -> None:
squadron.location.squadrons.append(squadron)
self.squadrons[squadron.aircraft].append(squadron) self.squadrons[squadron.aircraft].append(squadron)
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]: def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
@ -32,6 +32,35 @@ class AirWing:
except StopIteration: except StopIteration:
return False 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 @property
def available_aircraft_types(self) -> Iterator[AircraftType]: def available_aircraft_types(self) -> Iterator[AircraftType]:
for aircraft, squadrons in self.squadrons.items(): for aircraft, squadrons in self.squadrons.items():
@ -52,17 +81,6 @@ class AirWing:
if squadron.can_auto_assign(task) and squadron.location == base: if squadron.can_auto_assign(task) and squadron.location == base:
yield squadron 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: def squadron_for(self, aircraft: AircraftType) -> Squadron:
return self.squadrons_for(aircraft)[0] return self.squadrons_for(aircraft)[0]
@ -76,9 +94,9 @@ class AirWing:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():
squadron.populate_for_turn_0() squadron.populate_for_turn_0()
def replenish(self) -> None: def end_turn(self) -> None:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():
squadron.replenish_lost_pilots() squadron.end_turn()
def reset(self) -> None: def reset(self) -> None:
for squadron in self.iter_squadrons(): for squadron in self.iter_squadrons():

View File

@ -11,17 +11,20 @@ from typing import (
from faker import Faker from faker import Faker
from game.dcs.aircrafttype import AircraftType
from game.settings import AutoAtoBehavior, Settings from game.settings import AutoAtoBehavior, Settings
from game.squadrons.operatingbases import OperatingBases from gen.ato import Package
from game.squadrons.pilot import Pilot, PilotStatus from gen.flights.flight import FlightType, Flight
from game.squadrons.squadrondef import SquadronDef from gen.flights.flightplan import FlightPlanBuilder
from .pilot import Pilot, PilotStatus
from ..utils import meters
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.coalition import Coalition from game.coalition import Coalition
from gen.flights.flight import FlightType from game.dcs.aircrafttype import AircraftType
from game.theater import ControlPoint from game.theater import ControlPoint, ConflictTheater, MissionTarget
from .operatingbases import OperatingBases
from .squadrondef import SquadronDef
@dataclass @dataclass
@ -53,6 +56,9 @@ class Squadron:
settings: Settings = field(hash=False, compare=False) settings: Settings = field(hash=False, compare=False)
location: ControlPoint 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) owned_aircraft: int = field(init=False, hash=False, compare=False, default=0)
untasked_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 return self.coalition.player
def assign_to_base(self, base: ControlPoint) -> None: def assign_to_base(self, base: ControlPoint) -> None:
self.location.squadrons.remove(self)
self.location = base self.location = base
self.location.squadrons.append(self)
logging.debug(f"Assigned {self} to {base}") logging.debug(f"Assigned {self} to {base}")
@property @property
@ -169,6 +173,12 @@ class Squadron:
raise ValueError("Squadrons can only be created with active pilots.") raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.settings.squadron_pilot_limit) 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: def replenish_lost_pilots(self) -> None:
if not self.pilot_limits_enabled: if not self.pilot_limits_enabled:
return return
@ -243,6 +253,17 @@ class Squadron:
def can_auto_assign(self, task: FlightType) -> bool: def can_auto_assign(self, task: FlightType) -> bool:
return task in self.auto_assignable_mission_types 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: def operates_from(self, control_point: ControlPoint) -> bool:
if control_point.is_carrier: if control_point.is_carrier:
return self.operating_bases.carrier return self.operating_bases.carrier
@ -265,18 +286,133 @@ class Squadron:
def can_fulfill_flight(self, count: int) -> bool: def can_fulfill_flight(self, count: int) -> bool:
return self.can_provide_pilots(count) and self.untasked_aircraft >= count return self.can_provide_pilots(count) and self.untasked_aircraft >= count
def refund_orders(self) -> None: def refund_orders(self, count: Optional[int] = None) -> None:
self.coalition.adjust_budget(self.aircraft.price * self.pending_deliveries) if count is None:
self.pending_deliveries = 0 count = self.pending_deliveries
self.coalition.adjust_budget(self.aircraft.price * count)
self.pending_deliveries -= count
def deliver_orders(self) -> None: def deliver_orders(self) -> None:
self.cancel_overflow_orders()
self.owned_aircraft += self.pending_deliveries self.owned_aircraft += self.pending_deliveries
self.pending_deliveries = 0 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 @property
def max_fulfillable_aircraft(self) -> int: def max_fulfillable_aircraft(self) -> int:
return max(self.number_of_available_pilots, self.untasked_aircraft) 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 @classmethod
def create_from( def create_from(
cls, cls,

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import heapq import heapq
import itertools import itertools
import logging import logging
import math
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -56,6 +57,7 @@ if TYPE_CHECKING:
from game import Game from game import Game
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from game.squadrons.squadron import Squadron from game.squadrons.squadron import Squadron
from ..coalition import Coalition
from ..transfers import PendingTransfers from ..transfers import PendingTransfers
FREE_FRONTLINE_UNIT_SUPPLY: int = 15 FREE_FRONTLINE_UNIT_SUPPLY: int = 15
@ -281,7 +283,6 @@ class ControlPoint(MissionTarget, ABC):
position = None # type: Point position = None # type: Point
name = None # type: str name = None # type: str
captured = False
has_frontline = True has_frontline = True
alt = 0 alt = 0
@ -295,6 +296,7 @@ class ControlPoint(MissionTarget, ABC):
name: str, name: str,
position: Point, position: Point,
at: db.StartingPosition, at: db.StartingPosition,
starts_blue: bool,
has_frontline: bool = True, has_frontline: bool = True,
cptype: ControlPointType = ControlPointType.AIRBASE, cptype: ControlPointType = ControlPointType.AIRBASE,
) -> None: ) -> None:
@ -303,11 +305,12 @@ class ControlPoint(MissionTarget, ABC):
self.id = cp_id self.id = cp_id
self.full_name = name self.full_name = name
self.at = at self.at = at
self.starts_blue = starts_blue
self.connected_objectives: List[TheaterGroundObject[Any]] = [] self.connected_objectives: List[TheaterGroundObject[Any]] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = [] self.helipads: List[PointWithHeading] = []
self.captured = False self._coalition: Optional[Coalition] = None
self.captured_invert = False self.captured_invert = False
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.has_frontline = has_frontline self.has_frontline = has_frontline
@ -324,15 +327,33 @@ class ControlPoint(MissionTarget, ABC):
self.target_position: Optional[Point] = None self.target_position: Optional[Point] = None
self.squadrons: list[Squadron] = []
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.__class__}: {self.name}>" 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 @property
def ground_objects(self) -> List[TheaterGroundObject[Any]]: def ground_objects(self) -> List[TheaterGroundObject[Any]]:
return list(self.connected_objectives) 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 @property
@abstractmethod @abstractmethod
def heading(self) -> Heading: def heading(self) -> Heading:
@ -564,36 +585,82 @@ class ControlPoint(MissionTarget, ABC):
value = airframe.price * count value = airframe.price * count
game.adjust_budget(value, player=not self.captured) game.adjust_budget(value, player=not self.captured)
game.message( 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." f"{count} aircraft have been captured and sold for ${value}M."
) )
def aircraft_retreat_destination( def aircraft_retreat_destination(
self, game: Game, airframe: AircraftType self, squadron: Squadron
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
closest = ObjectiveDistanceCache.get_closest_airfields(self) closest = ObjectiveDistanceCache.get_closest_airfields(self)
# TODO: Should be airframe dependent. max_retreat_distance = squadron.aircraft.max_mission_range
max_retreat_distance = nautical_miles(200)
# Skip the first airbase because that's the airbase we're retreating # Skip the first airbase because that's the airbase we're retreating
# from. # from.
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:] airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
not_preferred: Optional[ControlPoint] = None
overfull: list[ControlPoint] = []
for airbase in airfields: for airbase in airfields:
if not airbase.can_operate(airframe):
continue
if airbase.captured != self.captured: if airbase.captured != self.captured:
continue continue
if airbase.unclaimed_parking(game) > 0:
return airbase
return None
@staticmethod if airbase.unclaimed_parking() < squadron.owned_aircraft:
def _retreat_squadron(squadron: Squadron) -> None: if airbase.can_operate(squadron.aircraft):
logging.error("Air unit retreat not currently implemented") 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: def retreat_air_units(self, game: Game) -> None:
# TODO: Capture in order of price to retain maximum value? # TODO: Capture in order of price to retain maximum value?
for squadron in self.squadrons: for squadron in self.squadrons:
self._retreat_squadron(squadron) self._retreat_squadron(game, squadron)
def depopulate_uncapturable_tgos(self) -> None: def depopulate_uncapturable_tgos(self) -> None:
for tgo in self.connected_objectives: for tgo in self.connected_objectives:
@ -602,27 +669,25 @@ 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:
coalition = game.coalition_for(for_player) new_coalition = game.coalition_for(for_player)
self.ground_unit_orders.refund_all(coalition) self.ground_unit_orders.refund_all(self.coalition)
for squadron in self.squadrons:
squadron.refund_orders()
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()
if for_player: self._coalition = new_coalition
self.captured = True
else:
self.captured = False
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
@property
def required_aircraft_start_type(self) -> Optional[str]:
return None
@abstractmethod @abstractmethod
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
... ...
def unclaimed_parking(self, game: Game) -> int: def unclaimed_parking(self) -> int:
return self.total_aircraft_parking - self.allocated_aircraft(game).total return self.total_aircraft_parking - self.allocated_aircraft().total
@abstractmethod @abstractmethod
def active_runway( def active_runway(
@ -630,6 +695,10 @@ class ControlPoint(MissionTarget, ABC):
) -> RunwayData: ) -> RunwayData:
... ...
@property
def airdrome_id_for_landing(self) -> Optional[int]:
return None
@property @property
def parking_slots(self) -> Iterator[ParkingSlot]: def parking_slots(self) -> Iterator[ParkingSlot]:
yield from [] yield from []
@ -651,8 +720,6 @@ class ControlPoint(MissionTarget, ABC):
def process_turn(self, game: Game) -> None: def process_turn(self, game: Game) -> None:
self.ground_unit_orders.process(game) self.ground_unit_orders.process(game)
for squadron in self.squadrons:
squadron.deliver_orders()
runway_status = self.runway_status runway_status = self.runway_status
if runway_status is not None: if runway_status is not None:
@ -674,16 +741,22 @@ class ControlPoint(MissionTarget, ABC):
u.position.x = u.position.x + delta.x u.position.x = u.position.x + delta.x
u.position.y = u.position.y + delta.y 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) present: dict[AircraftType, int] = defaultdict(int)
on_order: dict[AircraftType, int] = defaultdict(int) on_order: dict[AircraftType, int] = defaultdict(int)
transferring: dict[AircraftType, int] = defaultdict(int)
for squadron in self.squadrons: for squadron in self.squadrons:
present[squadron.aircraft] += squadron.owned_aircraft present[squadron.aircraft] += squadron.owned_aircraft
# TODO: Only if this is the squadron destination, not location. if squadron.destination is None:
on_order[squadron.aircraft] += squadron.pending_deliveries 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( def allocated_ground_units(
self, transfers: PendingTransfers self, transfers: PendingTransfers
@ -795,13 +868,14 @@ class ControlPoint(MissionTarget, ABC):
class Airfield(ControlPoint): class Airfield(ControlPoint):
def __init__(self, airport: Airport, has_frontline: bool = True) -> None: def __init__(self, airport: Airport, starts_blue: bool) -> None:
super().__init__( super().__init__(
airport.id, airport.id,
airport.name, airport.name,
airport.position, airport.position,
airport, airport,
has_frontline, starts_blue,
has_frontline=True,
cptype=ControlPointType.AIRBASE, cptype=ControlPointType.AIRBASE,
) )
self.airport = airport self.airport = airport
@ -863,6 +937,10 @@ class Airfield(ControlPoint):
assigner = RunwayAssigner(conditions) assigner = RunwayAssigner(conditions)
return assigner.get_preferred_runway(self.airport) return assigner.get_preferred_runway(self.airport)
@property
def airdrome_id_for_landing(self) -> Optional[int]:
return self.airport.id
@property @property
def parking_slots(self) -> Iterator[ParkingSlot]: def parking_slots(self) -> Iterator[ParkingSlot]:
yield from self.airport.parking_slots yield from self.airport.parking_slots
@ -970,12 +1048,13 @@ class NavalControlPoint(ControlPoint, ABC):
class Carrier(NavalControlPoint): 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__( super().__init__(
cp_id, cp_id,
name, name,
at, at,
at, at,
starts_blue,
has_frontline=False, has_frontline=False,
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
) )
@ -1010,12 +1089,13 @@ class Carrier(NavalControlPoint):
class Lha(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__( super().__init__(
cp_id, cp_id,
name, name,
at, at,
at, at,
starts_blue,
has_frontline=False, has_frontline=False,
cptype=ControlPointType.LHA_GROUP, cptype=ControlPointType.LHA_GROUP,
) )
@ -1043,12 +1123,13 @@ class OffMapSpawn(ControlPoint):
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
return True 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__( super().__init__(
cp_id, cp_id,
name, name,
position, position,
at=position, position,
starts_blue,
has_frontline=False, has_frontline=False,
cptype=ControlPointType.OFF_MAP, cptype=ControlPointType.OFF_MAP,
) )
@ -1066,6 +1147,10 @@ class OffMapSpawn(ControlPoint):
def can_operate(self, aircraft: AircraftType) -> bool: def can_operate(self, aircraft: AircraftType) -> bool:
return True return True
@property
def required_aircraft_start_type(self) -> Optional[str]:
return "In Flight"
@property @property
def heading(self) -> Heading: def heading(self) -> Heading:
return Heading.from_degrees(0) return Heading.from_degrees(0)
@ -1096,12 +1181,13 @@ class OffMapSpawn(ControlPoint):
class Fob(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__( super().__init__(
cp_id, cp_id,
name, name,
at, at,
at, at,
starts_blue,
has_frontline=True, has_frontline=True,
cptype=ControlPointType.FOB, cptype=ControlPointType.FOB,
) )

View File

@ -8,13 +8,11 @@ from datetime import datetime
from typing import Any, Dict, Iterable, List, Set from typing import Any, Dict, Iterable, List, Set
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from game import Game 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 PointWithHeading
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
CarrierGroundObject, CarrierGroundObject,
@ -30,7 +28,6 @@ from game.theater.theatergroundobject import (
) )
from game.utils import Heading from game.utils import Heading
from game.version import VERSION from game.version import VERSION
from gen.naming import namegen
from gen.coastal.coastal_group_generator import generate_coastal_group from gen.coastal.coastal_group_generator import generate_coastal_group
from gen.defenses.armor_group_generator import generate_armor_group from gen.defenses.armor_group_generator import generate_armor_group
from gen.fleet.ship_group_generator import ( from gen.fleet.ship_group_generator import (
@ -39,6 +36,7 @@ from gen.fleet.ship_group_generator import (
generate_ship_group, generate_ship_group,
) )
from gen.missiles.missiles_group_generator import generate_missile_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.airdefensegroupgenerator import AirDefenseRange
from gen.sam.ewr_group_generator import generate_ewr_group from gen.sam.ewr_group_generator import generate_ewr_group
from gen.sam.sam_group_generator import generate_anti_air_group from gen.sam.sam_group_generator import generate_anti_air_group
@ -61,7 +59,6 @@ class GeneratorSettings:
start_date: datetime start_date: datetime
player_budget: int player_budget: int
enemy_budget: int enemy_budget: int
midgame: bool
inverted: bool inverted: bool
no_carrier: bool no_carrier: bool
no_lha: bool no_lha: bool
@ -91,13 +88,12 @@ class GameGenerator:
generator_settings: GeneratorSettings, generator_settings: GeneratorSettings,
mod_settings: ModSettings, mod_settings: ModSettings,
) -> None: ) -> None:
self.player = player self.player = player.apply_mod_settings(mod_settings)
self.enemy = enemy self.enemy = enemy.apply_mod_settings(mod_settings)
self.theater = theater self.theater = theater
self.air_wing_config = air_wing_config self.air_wing_config = air_wing_config
self.settings = settings self.settings = settings
self.generator_settings = generator_settings self.generator_settings = generator_settings
self.mod_settings = mod_settings
def generate(self) -> Game: def generate(self) -> Game:
with logged_duration("TGO population"): with logged_duration("TGO population"):
@ -105,8 +101,8 @@ class GameGenerator:
namegen.reset() namegen.reset()
self.prepare_theater() self.prepare_theater()
game = Game( game = Game(
player_faction=self.player.apply_mod_settings(self.mod_settings), player_faction=self.player,
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings), enemy_faction=self.enemy,
theater=self.theater, theater=self.theater,
air_wing_config=self.air_wing_config, air_wing_config=self.air_wing_config,
start_date=self.generator_settings.start_date, start_date=self.generator_settings.start_date,
@ -119,37 +115,31 @@ class GameGenerator:
game.settings.version = VERSION game.settings.version = VERSION
return game 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: def prepare_theater(self) -> None:
to_remove: List[ControlPoint] = [] 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 # Remove carrier and lha, invert situation if needed
for cp in self.theater.controlpoints: 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: 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 # do remove
for cp in to_remove: for cp in to_remove:
self.theater.controlpoints.remove(cp) 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: class ControlPointGroundObjectGenerator:
def __init__( def __init__(

View File

@ -349,15 +349,17 @@ class AirliftPlanner:
else: else:
transfer = self.transfer 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( flight = Flight(
self.package, self.package,
self.game.country_for(squadron.player), self.game.country_for(squadron.player),
squadron, squadron,
flight_size, flight_size,
FlightType.TRANSPORT, FlightType.TRANSPORT,
self.game.settings.default_start_type, start_type,
departure=squadron.location,
arrival=squadron.location,
divert=None, divert=None,
cargo=transfer, cargo=transfer,
) )
@ -752,7 +754,7 @@ class PendingTransfers:
) )
def order_airlift_assets_at(self, control_point: ControlPoint) -> None: 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 # Buy a maximum of unclaimed_parking only to prevent that aircraft procurement
# take place at another base # take place at another base
gap = min( gap = min(

View File

@ -57,6 +57,7 @@ from dcs.task import (
Transport, Transport,
WeaponType, WeaponType,
TargetType, TargetType,
Nothing,
) )
from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
@ -92,7 +93,7 @@ from gen.flights.flight import (
from gen.lasercoderegistry import LaserCodeRegistry from gen.lasercoderegistry import LaserCodeRegistry
from gen.radios import RadioFrequency, RadioRegistry from gen.radios import RadioFrequency, RadioRegistry
from gen.runways import RunwayData from gen.runways import RunwayData
from gen.tacan import TacanBand, TacanRegistry from gen.tacan import TacanBand, TacanRegistry, TacanUsage
from .airsupport import AirSupport, AwacsInfo, TankerInfo from .airsupport import AirSupport, AwacsInfo, TankerInfo
from .callsigns import callsign_for_support_unit from .callsigns import callsign_for_support_unit
from .flights.flightplan import ( from .flights.flightplan import (
@ -437,7 +438,7 @@ class AircraftConflictGenerator:
if isinstance(flight.flight_plan, RefuelingFlightPlan): if isinstance(flight.flight_plan, RefuelingFlightPlan):
callsign = callsign_for_support_unit(group) callsign = callsign_for_support_unit(group)
tacan = self.tacan_registy.alloc_for_band(TacanBand.Y) tacan = self.tacan_registy.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
self.air_support.tankers.append( self.air_support.tankers.append(
TankerInfo( TankerInfo(
group_name=str(group.name), group_name=str(group.name),
@ -656,41 +657,36 @@ class AircraftConflictGenerator:
for squadron in control_point.squadrons: for squadron in control_point.squadrons:
try: try:
self._spawn_unused_at(control_point, country, faction, squadron) self._spawn_unused_for(squadron, country, faction)
except NoParkingSlotError: except NoParkingSlotError:
# If we run out of parking, stop spawning aircraft. # If we run out of parking, stop spawning aircraft.
return return
def _spawn_unused_at( def _spawn_unused_for(
self, self, squadron: Squadron, country: Country, faction: Faction
control_point: Airfield,
country: Country,
faction: Faction,
squadron: Squadron,
) -> None: ) -> None:
assert isinstance(squadron.location, Airfield)
for _ in range(squadron.untasked_aircraft): for _ in range(squadron.untasked_aircraft):
# Creating a flight even those this isn't a fragged mission lets us # Creating a flight even those this isn't a fragged mission lets us
# reuse the existing debriefing code. # reuse the existing debriefing code.
# TODO: Special flight type? # TODO: Special flight type?
flight = Flight( flight = Flight(
Package(control_point), Package(squadron.location),
faction.country, faction.country,
squadron, squadron,
1, 1,
FlightType.BARCAP, FlightType.BARCAP,
"Cold", "Cold",
departure=control_point,
arrival=control_point,
divert=None, divert=None,
) )
group = self._generate_at_airport( group = self._generate_at_airport(
name=namegen.next_aircraft_name(country, control_point.id, flight), name=namegen.next_aircraft_name(country, flight.departure.id, flight),
side=country, side=country,
unit_type=squadron.aircraft.dcs_unit_type, unit_type=squadron.aircraft.dcs_unit_type,
count=1, count=1,
start_type="Cold", start_type="Cold",
airport=control_point.airport, airport=squadron.location.airport,
) )
self._setup_livery(flight, group) self._setup_livery(flight, group)
@ -1153,6 +1149,23 @@ class AircraftConflictGenerator:
restrict_jettison=True, restrict_jettison=True,
) )
def configure_ferry(
self,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
group.task = Nothing.name
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
)
def configure_unknown_task(self, group: FlyingGroup[Any], 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)
@ -1197,6 +1210,8 @@ class AircraftConflictGenerator:
self.configure_oca_strike(group, package, flight, dynamic_runways) self.configure_oca_strike(group, package, flight, dynamic_runways)
elif flight_type == FlightType.TRANSPORT: elif flight_type == FlightType.TRANSPORT:
self.configure_transport(group, package, flight, dynamic_runways) self.configure_transport(group, package, flight, dynamic_runways)
elif flight_type == FlightType.FERRY:
self.configure_ferry(group, package, flight, dynamic_runways)
else: else:
self.configure_unknown_task(group, flight) self.configure_unknown_task(group, flight)
@ -1783,6 +1798,8 @@ class LandingPointBuilder(PydcsWaypointBuilder):
waypoint = super().build() waypoint = super().build()
waypoint.type = "Land" waypoint.type = "Land"
waypoint.action = PointAction.Landing waypoint.action = PointAction.Landing
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
return waypoint return waypoint
@ -1792,6 +1809,8 @@ class CargoStopBuilder(PydcsWaypointBuilder):
waypoint.type = "LandingReFuAr" waypoint.type = "LandingReFuAr"
waypoint.action = PointAction.LandingReFuAr waypoint.action = PointAction.LandingReFuAr
waypoint.landing_refuel_rearm_time = 2 # Minutes. waypoint.landing_refuel_rearm_time = 2 # Minutes.
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
return waypoint return waypoint

View File

@ -22,7 +22,7 @@ from .conflictgen import Conflict
from .flights.ai_flight_planner_db import AEWC_CAPABLE from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen from .naming import namegen
from .radios import RadioRegistry from .radios import RadioRegistry
from .tacan import TacanBand, TacanRegistry from .tacan import TacanBand, TacanRegistry, TacanUsage
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -89,7 +89,9 @@ class AirSupportConflictGenerator:
# 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()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) tacan = self.tacan_registry.alloc_for_band(
TacanBand.Y, TacanUsage.AirToAir
)
tanker_heading = Heading.from_degrees( tanker_heading = Heading.from_degrees(
self.conflict.red_cp.position.heading_between_point( self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position self.conflict.blue_cp.position

View File

@ -143,9 +143,17 @@ class GroundConflictGenerator:
# Add JTAC # Add JTAC
if self.game.blue.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: int = self.laser_code_registry.get_next_laser_code() code: int
freq = self.radio_registry.alloc_uhf() freq = self.radio_registry.alloc_uhf()
# If the option fc3LaserCode is enabled, force all JTAC
# laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs.
# Otherwise use 1688 for the first JTAC, 1687 for the second etc.
if self.game.settings.plugins["plugins.jtacautolase.fc3LaserCode"]:
code = 1113
else:
code = self.laser_code_registry.get_next_laser_code()
utype = self.game.blue.faction.jtac_unit utype = self.game.blue.faction.jtac_unit
if utype is None: if utype is None:
utype = AircraftType.named("MQ-9 Reaper") utype = AircraftType.named("MQ-9 Reaper")

View File

@ -183,6 +183,7 @@ class Package:
FlightType.TARCAP, FlightType.TARCAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.AEWC, FlightType.AEWC,
FlightType.FERRY,
FlightType.REFUELING, FlightType.REFUELING,
FlightType.SWEEP, FlightType.SWEEP,
FlightType.ESCORT, FlightType.ESCORT,

View File

@ -70,6 +70,7 @@ class FlightType(Enum):
TRANSPORT = "Transport" TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort" SEAD_ESCORT = "SEAD Escort"
REFUELING = "Refueling" REFUELING = "Refueling"
FERRY = "Ferry"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@ -159,6 +160,7 @@ class FlightWaypoint:
x: float, x: float,
y: float, y: float,
alt: Distance = meters(0), alt: Distance = meters(0),
control_point: Optional[ControlPoint] = None,
) -> None: ) -> None:
"""Creates a flight waypoint. """Creates a flight waypoint.
@ -168,11 +170,14 @@ class FlightWaypoint:
y: Y coordinate of the waypoint. y: Y coordinate of the waypoint.
alt: Altitude of the waypoint. By default this is MSL, but it can be alt: Altitude of the waypoint. By default this is MSL, but it can be
changed to AGL by setting alt_type to "RADIO" changed to AGL by setting alt_type to "RADIO"
control_point: The control point to associate with this waypoint. Needed for
landing points.
""" """
self.waypoint_type = waypoint_type self.waypoint_type = waypoint_type
self.x = x self.x = x
self.y = y self.y = y
self.alt = alt self.alt = alt
self.control_point = control_point
self.alt_type = "BARO" self.alt_type = "BARO"
self.name = "" self.name = ""
# TODO: Merge with pretty_name. # TODO: Merge with pretty_name.
@ -282,8 +287,6 @@ class Flight:
count: int, count: int,
flight_type: FlightType, flight_type: FlightType,
start_type: str, start_type: str,
departure: ControlPoint,
arrival: ControlPoint,
divert: Optional[ControlPoint], divert: Optional[ControlPoint],
custom_name: Optional[str] = None, custom_name: Optional[str] = None,
cargo: Optional[TransferOrder] = None, cargo: Optional[TransferOrder] = None,
@ -297,8 +300,8 @@ class Flight:
self.roster = FlightRoster(self.squadron, initial_size=count) self.roster = FlightRoster(self.squadron, initial_size=count)
else: else:
self.roster = roster self.roster = roster
self.departure = departure self.departure = self.squadron.location
self.arrival = arrival self.arrival = self.squadron.arrival
self.divert = divert self.divert = divert
self.flight_type = flight_type self.flight_type = flight_type
# TODO: Replace with FlightPlan. # TODO: Replace with FlightPlan.

View File

@ -37,10 +37,8 @@ from game.theater.theatergroundobject import (
NavalGroundObject, NavalGroundObject,
BuildingGroundObject, BuildingGroundObject,
) )
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime from .traveltime import GroundSpeed, TravelTime
@ -836,6 +834,39 @@ class AirliftFlightPlan(FlightPlan):
return self.package.time_over_target return self.package.time_over_target
@dataclass(frozen=True)
class FerryFlightPlan(FlightPlan):
takeoff: FlightWaypoint
nav_to_destination: list[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to_destination
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.land
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
# TOT planning isn't really useful for ferries. They're behind the front
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
return None
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
@dataclass(frozen=True) @dataclass(frozen=True)
class CustomFlightPlan(FlightPlan): class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint] custom_waypoints: List[FlightWaypoint]
@ -958,6 +989,8 @@ class FlightPlanBuilder:
return self.generate_transport(flight) return self.generate_transport(flight)
elif task == FlightType.REFUELING: elif task == FlightType.REFUELING:
return self.generate_refueling_racetrack(flight) return self.generate_refueling_racetrack(flight)
elif task == FlightType.FERRY:
return self.generate_ferry(flight)
raise PlanningError(f"{task} flight plan generation not implemented") raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None: def regenerate_package_waypoints(self) -> None:
@ -1244,6 +1277,42 @@ class FlightPlanBuilder:
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
) )
def generate_ferry(self, flight: Flight) -> FerryFlightPlan:
"""Generate a ferry flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
if flight.departure == flight.arrival:
raise PlanningError(
f"Cannot plan ferry flight: departure and arrival are both "
f"{flight.departure}"
)
altitude_is_agl = flight.unit_type.dcs_unit_type.helicopter
altitude = (
feet(1500)
if altitude_is_agl
else flight.unit_type.preferred_patrol_altitude
)
builder = WaypointBuilder(flight, self.coalition)
return FerryFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to_destination=builder.nav_path(
flight.departure.position,
flight.arrival.position,
altitude,
altitude_is_agl,
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def cap_racetrack_for_objective( def cap_racetrack_for_objective(
self, location: MissionTarget, barcap: bool self, location: MissionTarget, barcap: bool
) -> Tuple[Point, Point]: ) -> Tuple[Point, Point]:

View File

@ -110,7 +110,11 @@ class WaypointBuilder:
waypoint.pretty_name = "Exit theater" waypoint.pretty_name = "Exit theater"
else: else:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0) FlightWaypointType.LANDING_POINT,
position.x,
position.y,
meters(0),
control_point=arrival,
) )
waypoint.name = "LANDING" waypoint.name = "LANDING"
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
@ -139,7 +143,11 @@ class WaypointBuilder:
altitude_type = "RADIO" altitude_type = "RADIO"
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.DIVERT, position.x, position.y, altitude FlightWaypointType.DIVERT,
position.x,
position.y,
altitude,
control_point=divert,
) )
waypoint.alt_type = altitude_type waypoint.alt_type = altitude_type
waypoint.name = "DIVERT" waypoint.name = "DIVERT"
@ -488,6 +496,7 @@ class WaypointBuilder:
control_point.position.x, control_point.position.x,
control_point.position.y, control_point.position.y,
meters(0), meters(0),
control_point=control_point,
) )
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
waypoint.name = "DROP OFF" waypoint.name = "DROP OFF"

View File

@ -59,7 +59,7 @@ from game.unitmap import UnitMap
from game.utils import Heading, feet, knots, mps from game.utils import Heading, feet, knots, mps
from .radios import RadioFrequency, RadioRegistry from .radios import RadioFrequency, RadioRegistry
from .runways import RunwayData from .runways import RunwayData
from .tacan import TacanBand, TacanChannel, TacanRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -378,7 +378,9 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
for unit in group.units[1:]: for unit in group.units[1:]:
ship_group.add_unit(self.create_ship(unit, atc)) ship_group.add_unit(self.create_ship(unit, atc))
tacan = self.tacan_registry.alloc_for_band(TacanBand.X) tacan = self.tacan_registry.alloc_for_band(
TacanBand.X, TacanUsage.TransmitReceive
)
tacan_callsign = self.tacan_callsign() tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc) icls = next(self.icls_alloc)

View File

@ -43,209 +43,406 @@ ALPHA_MILITARY = [
] ]
ANIMALS: tuple[str, ...] = ( ANIMALS: tuple[str, ...] = (
"SHARK", "AARDVARK",
"TORTOISE",
"BAT",
"PANGOLIN",
"AARDWOLF", "AARDWOLF",
"MONKEY", "ADDER",
"BUFFALO", "ALBACORE",
"DOG", "ALBATROSS",
"BOBCAT", "ALLIGATOR",
"LYNX", "ALPACA",
"PANTHER", "ANACONDA",
"TIGER", "ANOLE",
"LION", "ANTEATER",
"OWL", "ANTELOPE",
"BUTTERFLY", "ANTLION",
"BISON", "ARAPAIMA",
"DUCK", "ARCHERFISH",
"COBRA", "ARGALI",
"MAMBA",
"DOLPHIN",
"PHEASANT",
"ARMADILLO", "ARMADILLO",
"RACOON", "ASP",
"ZEBRA", "AUROCHS",
"AXOLOTL",
"BABIRUSA",
"BABOON",
"BADGER",
"BANDICOOT",
"BARRACUDA",
"BARRAMUNDI",
"BASILISK",
"BASS",
"BAT",
"BEAR",
"BEAVER",
"BEETLE",
"BELUGA",
"BETTONG",
"BINTURONG",
"BISON",
"BLOODHOUND",
"BOA",
"BOBCAT",
"BONGO",
"BONITO",
"BUFFALO",
"BULLDOG",
"BULLFROG",
"BULLSHARK",
"BUMBLEBEE",
"BUNNY",
"BUTTERFLY",
"CAIMAN",
"CAMEL",
"CANARY",
"CAPYBARA",
"CARACAL",
"CARP",
"CASTOR",
"CAT",
"CATERPILLAR",
"CATFISH",
"CENTIPEDE",
"CHAMELEON",
"CHEETAH",
"CHICKEN",
"CHIMAERA",
"CICADA",
"CICHLID",
"CIVET",
"COBIA",
"COBRA",
"COCKATOO",
"COD",
"COELACANTH",
"COLT",
"CONDOR",
"COPPERHEAD",
"CORAL",
"CORGI",
"COTTONMOUTH",
"COUGAR",
"COW", "COW",
"COYOTE", "COYOTE",
"FOX",
"LIGHTFOOT",
"COTTONMOUTH",
"TAURUS",
"VIPER",
"CASTOR",
"GIRAFFE",
"SNAKE",
"MONSTER",
"ALBATROSS",
"HAWK",
"DOVE",
"MOCKINGBIRD",
"GECKO",
"ORYX",
"GORILLA",
"HARAMBE",
"GOOSE",
"MAVERICK",
"HARE",
"JACKAL",
"LEOPARD",
"CAT",
"MUSK",
"ORCA",
"OCELOT",
"BEAR",
"PANDA",
"GULL",
"PENGUIN",
"PYTHON",
"RAVEN",
"DEER",
"MOOSE",
"REINDEER",
"SHEEP",
"GAZELLE",
"INSECT",
"VULTURE",
"WALLABY",
"KANGAROO",
"KOALA",
"KIWI",
"WHALE",
"FISH",
"RHINO",
"HIPPO",
"RAT",
"WOODPECKER",
"WORM",
"BABOON",
"YAK",
"SCORPIO",
"HORSE",
"POODLE",
"CENTIPEDE",
"CHICKEN",
"CHEETAH",
"CHAMELEON",
"CATFISH",
"CATERPILLAR",
"CARACAL",
"CAMEL",
"CAIMAN",
"BARRACUDA",
"BANDICOOT",
"ALLIGATOR",
"BONGO",
"CORAL",
"ELEPHANT",
"ANTELOPE",
"CRAB", "CRAB",
"CRANE",
"CRICKET",
"CROCODILE",
"CROW",
"CUTTLEFISH",
"DACHSHUND", "DACHSHUND",
"DEER",
"DINGO",
"DIREWOLF",
"DODO", "DODO",
"FLAMINGO", "DOG",
"FERRET", "DOLPHIN",
"FALCON",
"BULLDOG",
"DONKEY", "DONKEY",
"IGUANA", "DOVE",
"TAMARIN", "DRACO",
"HARRIER",
"GRIZZLY",
"GREYHOUND",
"GRASSHOPPER",
"JAGUAR",
"LADYBUG",
"KOMODO",
"DRAGON", "DRAGON",
"DRAGONFLY",
"DUCK",
"DUGONG",
"EAGLE",
"EARWIG",
"ECHIDNA",
"EEL",
"ELEPHANT",
"ELK",
"EMU",
"ERMINE",
"FALCON",
"FANGTOOTH",
"FAWN",
"FENNEC",
"FERRET",
"FINCH",
"FIREFLY",
"FISH",
"FLAMINGO",
"FLEA",
"FLOUNDER",
"FORGMOUTH",
"FOX",
"FRINGEHEAD",
"FROG",
"GAR",
"GAZELLE",
"GECKO",
"GENET",
"GERBIL",
"GHARIAL",
"GIBBON",
"GIRAFFE",
"GOOSE",
"GOPHER",
"GORILLA",
"GOSHAWK",
"GRASSHOPPER",
"GREYHOUND",
"GRIZZLY",
"GROUPER",
"GROUSE",
"GRYPHON",
"GUANACO",
"GULL",
"GUPPY",
"HADDOCK",
"HAGFISH",
"HALIBUT",
"HAMSTER",
"HARAMBE",
"HARE",
"HARRIER",
"HAWK",
"HEDGEHOG",
"HERMITCRAB",
"HERON",
"HERRING",
"HIPPO",
"HORNBILL",
"HORNET",
"HORSE",
"HUNTSMAN",
"HUSKY",
"HYENA",
"IBEX",
"IBIS",
"IGUANA",
"IMPALA",
"INSECT",
"IRUKANDJI",
"ISOPOD",
"JACKAL",
"JAGUAR",
"JELLYFISH",
"JERBOA",
"KAKAPO",
"KANGAROO",
"KATYDID",
"KEA",
"KINGFISHER",
"KITTEN",
"KIWI",
"KOALA",
"KOMODO",
"KRAIT",
"LADYBUG",
"LAMPREY",
"LEMUR",
"LEOPARD",
"LIGHTFOOT",
"LION",
"LIONFISH",
"LIZARD", "LIZARD",
"LLAMA", "LLAMA",
"LOACH",
"LOBSTER", "LOBSTER",
"OCTOPUS",
"MANATEE",
"MAGPIE",
"MACAW",
"OSTRICH",
"OYSTER",
"MOLE",
"MULE",
"MOTH",
"MONGOOSE",
"MOLLY",
"MEERKAT",
"MOUSE",
"PEACOCK",
"PIKE",
"ROBIN",
"RAGDOLL",
"PLATYPUS",
"PELICAN",
"PARROT",
"PORCUPINE",
"PIRANHA",
"PUMA",
"PUG",
"TAPIR",
"TERMITE",
"URCHIN",
"SHRIMP",
"TURKEY",
"TOUCAN",
"TETRA",
"HUSKY",
"STARFISH",
"SWAN",
"FROG",
"SQUIRREL",
"WALRUS",
"WARTHOG",
"CORGI",
"WEASEL",
"WOMBAT",
"WOLVERINE",
"MAMMOTH",
"TOAD",
"WOLF",
"ZEBU",
"SEAL",
"SKATE",
"JELLYFISH",
"MOSQUITO",
"LOCUST", "LOCUST",
"LORIKEET",
"LUNGFISH",
"LYNX",
"MACAW",
"MAGPIE",
"MALLARD",
"MAMBA",
"MAMMOTH",
"MANATEE",
"MANDRILL",
"MANTA",
"MANTIS",
"MARE",
"MARLIN",
"MARMOT",
"MARTEN",
"MASTIFF",
"MASTODON",
"MAVERICK",
"MAYFLY",
"MEERKAT",
"MILLIPEDE",
"MINK",
"MOA",
"MOCKINGBIRD",
"MOLE",
"MOLERAT",
"MOLLY",
"MONGOOSE",
"MONKEY",
"MONKFISH",
"MONSTER",
"MOOSE",
"MORAY",
"MOSQUITO",
"MOTH",
"MOUSE",
"MUDSKIPPER",
"MULE",
"MUSK",
"MYNA",
"NARWHAL",
"NAUTILUS",
"NEWT",
"NIGHTINGALE",
"NUMBAT",
"OCELOT",
"OCTOPUS",
"OKAPI",
"OLM",
"OPAH",
"OPOSSUM",
"ORCA",
"ORYX",
"OSPREY",
"OSTRICH",
"OTTER",
"OWL",
"OX",
"OYSTER",
"PADDLEFISH",
"PADEMELON",
"PANDA",
"PANGOLIN",
"PANTHER",
"PARAKEET",
"PARROT",
"PEACOCK",
"PELICAN",
"PENGUIN",
"PERCH",
"PEREGRINE",
"PETRAL",
"PHEASANT",
"PIG",
"PIGEON",
"PIGLET",
"PIKE",
"PIRANHA",
"PLATYPUS",
"POODLE",
"PORCUPINE",
"PORPOISE",
"POSSUM",
"POTOROO",
"PRONGHORN",
"PUFFERFISH",
"PUFFIN",
"PUG",
"PUMA",
"PYTHON",
"QUAGGA",
"QUAIL",
"QUOKKA",
"QUOLL",
"RABBIT",
"RACOON",
"RAGDOLL",
"RAT",
"RATTLESNAKE",
"RAVEN",
"REINDEER",
"RHINO",
"ROACH",
"ROBIN",
"SABERTOOTH",
"SAILFISH",
"SALAMANDER",
"SALMON",
"SANDFLY",
"SARDINE",
"SAWFISH",
"SCARAB",
"SCORPION",
"SEAHORSE",
"SEAL",
"SEALION",
"SERVAL",
"SHARK",
"SHEEP",
"SHOEBILL",
"SHRIKE",
"SHRIMP",
"SIDEWINDER",
"SILKWORM",
"SKATE",
"SKINK",
"SKUNK",
"SLOTH",
"SLUG", "SLUG",
"SNAIL", "SNAIL",
"HEDGEHOG", "SNAKE",
"PIGLET", "SNAPPER",
"FENNEC", "SNOOK",
"BADGER", "SPARROW",
"ALPACA", "SPIDER",
"DINGO", "SPRINGBOK",
"COLT", "SQUID",
"SKUNK", "SQUIRREL",
"BUNNY", "STAGHORN",
"IMPALA", "STARFISH",
"GUANACO", "STINGRAY",
"CAPYBARA", "STINKBUG",
"ELK", "STOUT",
"MINK", "STURGEON",
"PRONGHORN", "SUGARGLIDER",
"CROW", "SUNBEAR",
"BUMBLEBEE", "SWALLOW",
"FAWN", "SWAN",
"OTTER", "SWIFT",
"SWORDFISH",
"TAIPAN",
"TAKAHE",
"TAMARIN",
"TANG",
"TAPIR",
"TARANTULA",
"TARPON",
"TARSIER",
"TAURUS",
"TERMITE",
"TERRIER",
"TETRA",
"THRUSH",
"THYLACINE",
"TIGER",
"TOAD",
"TORTOISE",
"TOUCAN",
"TREADFIN",
"TREVALLY",
"TRIGGERFISH",
"TROUT",
"TUATARA",
"TUNA",
"TURKEY",
"TURTLE",
"URCHIN",
"VIPER",
"VULTURE",
"WALLABY",
"WALLAROO",
"WALLEYE",
"WALRUS",
"WARTHOG",
"WASP",
"WATERBUCK", "WATERBUCK",
"JERBOA", "WEASEL",
"KITTEN", "WEEVIL",
"ARGALI", "WEKA",
"OX", "WHALE",
"MARE", "WILDCAT",
"FINCH", "WILDEBEEST",
"BASILISK", "WOLF",
"GOPHER", "WOLFHOUND",
"HAMSTER", "WOLVERINE",
"CANARY", "WOMBAT",
"WOODCHUCK", "WOODCHUCK",
"ANACONDA", "WOODPECKER",
"WORM",
"WRASSE",
"WYVERN",
"YAK",
"ZEBRA",
"ZEBU",
) )

View File

@ -4,13 +4,37 @@ from enum import Enum
from typing import Dict, Iterator, Set from typing import Dict, Iterator, Set
class TacanUsage(Enum):
TransmitReceive = "transmit receive"
AirToAir = "air to air"
class TacanBand(Enum): class TacanBand(Enum):
X = "X" X = "X"
Y = "Y" Y = "Y"
def range(self) -> Iterator["TacanChannel"]: def range(self) -> Iterator["TacanChannel"]:
"""Returns an iterator over the channels in this band.""" """Returns an iterator over the channels in this band."""
return (TacanChannel(x, self) for x in range(1, 100)) return (TacanChannel(x, self) for x in range(1, 126 + 1))
def valid_channels(self, usage: TacanUsage) -> Iterator["TacanChannel"]:
for x in self.range():
if x.number not in UNAVAILABLE[usage][self]:
yield x
# Avoid certain TACAN channels for various reasons
# https://forums.eagle.ru/topic/276390-datalink-issue/
UNAVAILABLE = {
TacanUsage.TransmitReceive: {
TacanBand.X: set(range(2, 30 + 1)) | set(range(47, 63 + 1)),
TacanBand.Y: set(range(2, 30 + 1)) | set(range(64, 92 + 1)),
},
TacanUsage.AirToAir: {
TacanBand.X: set(range(1, 36 + 1)) | set(range(64, 99 + 1)),
TacanBand.Y: set(range(1, 36 + 1)) | set(range(64, 99 + 1)),
},
}
@dataclass(frozen=True) @dataclass(frozen=True)
@ -41,25 +65,30 @@ class TacanRegistry:
def __init__(self) -> None: def __init__(self) -> None:
self.allocated_channels: Set[TacanChannel] = set() self.allocated_channels: Set[TacanChannel] = set()
self.band_allocators: Dict[TacanBand, Iterator[TacanChannel]] = {} self.allocators: Dict[TacanBand, Dict[TacanUsage, Iterator[TacanChannel]]] = {}
for band in TacanBand: for band in TacanBand:
self.band_allocators[band] = band.range() self.allocators[band] = {}
for usage in TacanUsage:
self.allocators[band][usage] = band.valid_channels(usage)
def alloc_for_band(self, band: TacanBand) -> TacanChannel: def alloc_for_band(
self, band: TacanBand, intended_usage: TacanUsage
) -> TacanChannel:
"""Allocates a TACAN channel in the given band. """Allocates a TACAN channel in the given band.
Args: Args:
band: The TACAN band to allocate a channel for. band: The TACAN band to allocate a channel for.
intended_usage: What the caller intends to use the tacan channel for.
Returns: Returns:
A TACAN channel in the given band. A TACAN channel in the given band.
Raises: Raises:
OutOfChannelsError: All channels compatible with the given radio are OutOfTacanChannelsError: All channels compatible with the given radio are
already allocated. already allocated.
""" """
allocator = self.band_allocators[band] allocator = self.allocators[band][intended_usage]
try: try:
while (channel := next(allocator)) in self.allocated_channels: while (channel := next(allocator)) in self.allocated_channels:
pass pass
@ -67,7 +96,7 @@ class TacanRegistry:
except StopIteration: except StopIteration:
raise OutOfTacanChannelsError(band) raise OutOfTacanChannelsError(band)
def reserve(self, channel: TacanChannel) -> None: def mark_unavailable(self, channel: TacanChannel) -> None:
"""Reserves the given channel. """Reserves the given channel.
Reserving a channel ensures that it will not be allocated in the future. Reserving a channel ensures that it will not be allocated in the future.
@ -76,7 +105,7 @@ class TacanRegistry:
channel: The channel to reserve. channel: The channel to reserve.
Raises: Raises:
ChannelInUseError: The given frequency is already in use. TacanChannelInUseError: The given channel is already in use.
""" """
if channel in self.allocated_channels: if channel in self.allocated_channels:
raise TacanChannelInUseError(channel) raise TacanChannelInUseError(channel)

17
qt_ui/errorreporter.py Normal file
View File

@ -0,0 +1,17 @@
import logging
from collections import Iterator
from contextlib import contextmanager
from typing import Type
from PySide2.QtWidgets import QDialog, QMessageBox
@contextmanager
def report_errors(
title: str, parent: QDialog, error_type: Type[Exception] = Exception
) -> Iterator[None]:
try:
yield
except error_type as ex:
logging.exception(title)
QMessageBox().critical(parent, title, str(ex), QMessageBox.Ok)

View File

@ -13,6 +13,7 @@ from PySide2.QtWidgets import QApplication, QSplashScreen
from dcs.payloads import PayloadDirectories from dcs.payloads import PayloadDirectories
from game import Game, VERSION, persistency from game import Game, VERSION, persistency
from game.campaignloader.campaign import Campaign
from game.data.weapons import WeaponGroup, Pylon, Weapon from game.data.weapons import WeaponGroup, Pylon, Weapon
from game.db import FACTIONS from game.db import FACTIONS
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
@ -27,7 +28,6 @@ from qt_ui import (
) )
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QLiberationWindow import QLiberationWindow from qt_ui.windows.QLiberationWindow import QLiberationWindow
from game.campaignloader.campaign import Campaign
from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET
from qt_ui.windows.preferences.QLiberationFirstStartWindow import ( from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
QLiberationFirstStartWindow, QLiberationFirstStartWindow,
@ -252,7 +252,6 @@ def create_game(
start_date=start_date, start_date=start_date,
player_budget=DEFAULT_BUDGET, player_budget=DEFAULT_BUDGET,
enemy_budget=DEFAULT_BUDGET, enemy_budget=DEFAULT_BUDGET,
midgame=False,
inverted=inverted, inverted=inverted,
no_carrier=False, no_carrier=False,
no_lha=False, no_lha=False,

View File

@ -0,0 +1,42 @@
# From https://timlehr.com/python-exception-hooks-with-qt-message-box/
import logging
import sys
import traceback
from PySide2.QtCore import Signal, QObject
from PySide2.QtWidgets import QMessageBox, QApplication
class UncaughtExceptionHandler(QObject):
_exception_caught = Signal(str, str)
def __init__(self, parent: QObject):
super().__init__(parent)
sys.excepthook = self.exception_hook
# Use a signal so that the message box always comes from the main thread.
self._exception_caught.connect(self.show_exception_box)
def exception_hook(self, exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
# Ignore keyboard interrupt to support console applications.
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.exception(
"Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
)
self._exception_caught.emit(
str(exc_value),
"".join(traceback.format_exception(exc_type, exc_value, exc_traceback)),
)
def show_exception_box(self, message: str, exception: str) -> None:
if QApplication.instance() is not None:
QMessageBox().critical(
self.parent(),
"An unexpected error occurred",
"\n".join([message, "", exception]),
QMessageBox.Ok,
)
else:
logging.critical("No QApplication instance available.")

View File

@ -10,7 +10,6 @@ from PySide2.QtCore import (
) )
from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAbstractItemView,
QDialog, QDialog,
QListView, QListView,
QVBoxLayout, QVBoxLayout,
@ -32,38 +31,7 @@ from game.dcs.aircrafttype import AircraftType
from game.squadrons import AirWing, Pilot, Squadron from game.squadrons import AirWing, Pilot, Squadron
from game.theater import ControlPoint, ConflictTheater from game.theater import ControlPoint, ConflictTheater
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from qt_ui.models import AirWingModel, SquadronModel
from qt_ui.uiconstants import AIRCRAFT_ICONS from qt_ui.uiconstants import AIRCRAFT_ICONS
from qt_ui.windows.AirWingDialog import SquadronDelegate
from qt_ui.windows.SquadronDialog import SquadronDialog
class SquadronList(QListView):
"""List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel) -> None:
super().__init__()
self.air_wing_model = air_wing_model
self.dialog: Optional[SquadronDialog] = None
self.setIconSize(QSize(91, 24))
self.setItemDelegate(SquadronDelegate(self.air_wing_model))
self.setModel(self.air_wing_model)
self.selectionModel().setCurrentIndex(
self.air_wing_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
)
# self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.doubleClicked.connect(self.on_double_click)
def on_double_click(self, index: QModelIndex) -> None:
if not index.isValid():
return
self.dialog = SquadronDialog(
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
)
self.dialog.show()
class AllowedMissionTypeControls(QVBoxLayout): class AllowedMissionTypeControls(QVBoxLayout):

View File

@ -14,12 +14,14 @@ from PySide2.QtWidgets import (
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
QWidget, QWidget,
QHBoxLayout,
) )
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ConflictTheater
from gen.flights.flight import Flight from gen.flights.flight import Flight
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.models import GameModel, AirWingModel, SquadronModel from qt_ui.models import GameModel, AirWingModel, SquadronModel, AtoModel
from qt_ui.windows.SquadronDialog import SquadronDialog from qt_ui.windows.SquadronDialog import SquadronDialog
@ -56,9 +58,16 @@ class SquadronDelegate(TwoColumnRowDelegate):
class SquadronList(QListView): class SquadronList(QListView):
"""List view for displaying the air wing's squadrons.""" """List view for displaying the air wing's squadrons."""
def __init__(self, air_wing_model: AirWingModel) -> None: def __init__(
self,
ato_model: AtoModel,
air_wing_model: AirWingModel,
theater: ConflictTheater,
) -> None:
super().__init__() super().__init__()
self.ato_model = ato_model
self.air_wing_model = air_wing_model self.air_wing_model = air_wing_model
self.theater = theater
self.dialog: Optional[SquadronDialog] = None self.dialog: Optional[SquadronDialog] = None
self.setIconSize(QSize(91, 24)) self.setIconSize(QSize(91, 24))
@ -76,7 +85,10 @@ class SquadronList(QListView):
if not index.isValid(): if not index.isValid():
return return
self.dialog = SquadronDialog( self.dialog = SquadronDialog(
SquadronModel(self.air_wing_model.squadron_at_index(index)), self self.ato_model,
SquadronModel(self.air_wing_model.squadron_at_index(index)),
self.theater,
self,
) )
self.dialog.show() self.dialog.show()
@ -138,30 +150,47 @@ class AircraftInventoryData:
class AirInventoryView(QWidget): class AirInventoryView(QWidget):
def __init__(self, game_model: GameModel) -> None: def __init__(self, game_model: GameModel) -> None:
super().__init__() super().__init__()
self.game_model = game_model self.game_model = game_model
self.country = self.game_model.game.country_for(player=True)
self.only_unallocated = False
self.enemy_info = False
layout = QVBoxLayout() layout = QVBoxLayout()
self.setLayout(layout) self.setLayout(layout)
self.only_unallocated_cb = QCheckBox("Unallocated Only?") checkbox_row = QHBoxLayout()
self.only_unallocated_cb.toggled.connect(self.update_table) layout.addLayout(checkbox_row)
layout.addWidget(self.only_unallocated_cb) self.only_unallocated_cb = QCheckBox("Unallocated only")
self.only_unallocated_cb.toggled.connect(self.set_only_unallocated)
checkbox_row.addWidget(self.only_unallocated_cb)
self.enemy_info_cb = QCheckBox("Show enemy info")
self.enemy_info_cb.toggled.connect(self.set_enemy_info)
checkbox_row.addWidget(self.enemy_info_cb)
checkbox_row.addStretch()
self.table = QTableWidget() self.table = QTableWidget()
layout.addWidget(self.table) layout.addWidget(self.table)
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table.verticalHeader().setVisible(False) self.table.verticalHeader().setVisible(False)
self.update_table(False) self.set_only_unallocated(False)
def update_table(self, only_unallocated: bool) -> None: def set_only_unallocated(self, value: bool) -> None:
self.only_unallocated = value
self.update_table()
def set_enemy_info(self, value: bool) -> None:
self.enemy_info = value
self.update_table()
def update_table(self) -> None:
self.table.setSortingEnabled(False) self.table.setSortingEnabled(False)
self.table.clear() self.table.clear()
inventory_rows = list(self.get_data(only_unallocated)) inventory_rows = list(self.get_data())
self.table.setRowCount(len(inventory_rows)) self.table.setRowCount(len(inventory_rows))
headers = AircraftInventoryData.headers() headers = AircraftInventoryData.headers()
self.table.setColumnCount(len(headers)) self.table.setColumnCount(len(headers))
@ -175,18 +204,19 @@ class AirInventoryView(QWidget):
self.table.setSortingEnabled(True) self.table.setSortingEnabled(True)
def iter_allocated_aircraft(self) -> Iterator[AircraftInventoryData]: def iter_allocated_aircraft(self) -> Iterator[AircraftInventoryData]:
for package in self.game_model.game.blue.ato.packages: coalition = self.game_model.game.coalition_for(not self.enemy_info)
for package in coalition.ato.packages:
for flight in package.flights: for flight in package.flights:
yield from AircraftInventoryData.from_flight(flight) yield from AircraftInventoryData.from_flight(flight)
def iter_unallocated_aircraft(self) -> Iterator[AircraftInventoryData]: def iter_unallocated_aircraft(self) -> Iterator[AircraftInventoryData]:
game = self.game_model.game coalition = self.game_model.game.coalition_for(not self.enemy_info)
for squadron in game.blue.air_wing.iter_squadrons(): for squadron in coalition.air_wing.iter_squadrons():
yield from AircraftInventoryData.each_untasked_from_squadron(squadron) yield from AircraftInventoryData.each_untasked_from_squadron(squadron)
def get_data(self, only_unallocated: bool) -> Iterator[AircraftInventoryData]: def get_data(self) -> Iterator[AircraftInventoryData]:
yield from self.iter_unallocated_aircraft() yield from self.iter_unallocated_aircraft()
if not only_unallocated: if not self.only_unallocated:
yield from self.iter_allocated_aircraft() yield from self.iter_allocated_aircraft()
@ -194,7 +224,14 @@ class AirWingTabs(QTabWidget):
def __init__(self, game_model: GameModel) -> None: def __init__(self, game_model: GameModel) -> None:
super().__init__() super().__init__()
self.addTab(SquadronList(game_model.blue_air_wing_model), "Squadrons") self.addTab(
SquadronList(
game_model.ato_model,
game_model.blue_air_wing_model,
game_model.game.theater,
),
"Squadrons",
)
self.addTab(AirInventoryView(game_model), "Inventory") self.addTab(AirInventoryView(game_model), "Inventory")

View File

@ -24,6 +24,7 @@ from qt_ui import liberation_install
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.uiconstants import URLS from qt_ui.uiconstants import URLS
from qt_ui.uncaughtexceptionhandler import UncaughtExceptionHandler
from qt_ui.widgets.QTopPanel import QTopPanel from qt_ui.widgets.QTopPanel import QTopPanel
from qt_ui.widgets.ato import QAirTaskingOrderPanel from qt_ui.widgets.ato import QAirTaskingOrderPanel
from qt_ui.widgets.map.QLiberationMap import QLiberationMap from qt_ui.widgets.map.QLiberationMap import QLiberationMap
@ -42,7 +43,9 @@ from qt_ui.windows.logs.QLogsWindow import QLogsWindow
class QLiberationWindow(QMainWindow): class QLiberationWindow(QMainWindow):
def __init__(self, game: Optional[Game]) -> None: def __init__(self, game: Optional[Game]) -> None:
super(QLiberationWindow, self).__init__() super().__init__()
self._uncaught_exception_handler = UncaughtExceptionHandler(self)
self.game = game self.game = game
self.game_model = GameModel(game) self.game_model = GameModel(game)

View File

@ -1,5 +1,5 @@
import logging import logging
from typing import Callable from typing import Callable, Iterator, Optional
from PySide2.QtCore import ( from PySide2.QtCore import (
QItemSelectionModel, QItemSelectionModel,
@ -16,12 +16,15 @@ from PySide2.QtWidgets import (
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QCheckBox, QCheckBox,
QComboBox,
) )
from game.squadrons import Pilot from game.squadrons import Pilot, Squadron
from game.theater import ControlPoint, ConflictTheater
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.models import SquadronModel from qt_ui.errorreporter import report_errors
from qt_ui.models import SquadronModel, AtoModel
class PilotDelegate(TwoColumnRowDelegate): class PilotDelegate(TwoColumnRowDelegate):
@ -90,12 +93,58 @@ class AutoAssignedTaskControls(QVBoxLayout):
self.squadron_model.set_auto_assignable(task, checked) self.squadron_model.set_auto_assignable(task, checked)
class SquadronDestinationComboBox(QComboBox):
def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None:
super().__init__()
self.squadron = squadron
self.theater = theater
room = squadron.location.unclaimed_parking()
self.addItem(
f"Remain at {squadron.location} (room for {room} more aircraft)", None
)
selected_index: Optional[int] = None
for idx, destination in enumerate(sorted(self.iter_destinations(), key=str), 1):
if destination == squadron.destination:
selected_index = idx
room = destination.unclaimed_parking()
self.addItem(
f"Transfer to {destination} (room for {room} more aircraft)",
destination,
)
if squadron.destination is None:
selected_index = 0
if selected_index is not None:
self.setCurrentIndex(selected_index)
def iter_destinations(self) -> Iterator[ControlPoint]:
size = self.squadron.expected_size_next_turn
for control_point in self.theater.control_points_for(self.squadron.player):
if control_point == self:
continue
if not control_point.can_operate(self.squadron.aircraft):
continue
if control_point.unclaimed_parking() < size:
continue
yield control_point
class SquadronDialog(QDialog): class SquadronDialog(QDialog):
"""Dialog window showing a squadron.""" """Dialog window showing a squadron."""
def __init__(self, squadron_model: SquadronModel, parent) -> None: def __init__(
self,
ato_model: AtoModel,
squadron_model: SquadronModel,
theater: ConflictTheater,
parent,
) -> None:
super().__init__(parent) super().__init__(parent)
self.ato_model = ato_model
self.squadron_model = squadron_model self.squadron_model = squadron_model
self.theater = theater
self.setMinimumSize(1000, 440) self.setMinimumSize(1000, 440)
self.setWindowTitle(str(squadron_model.squadron)) self.setWindowTitle(str(squadron_model.squadron))
@ -117,6 +166,15 @@ class SquadronDialog(QDialog):
columns.addWidget(self.pilot_list) columns.addWidget(self.pilot_list)
button_panel = QHBoxLayout() button_panel = QHBoxLayout()
self.transfer_destination = SquadronDestinationComboBox(
squadron_model.squadron, theater
)
self.transfer_destination.currentIndexChanged.connect(
self.on_destination_changed
)
button_panel.addWidget(self.transfer_destination)
button_panel.addStretch() button_panel.addStretch()
layout.addLayout(button_panel) layout.addLayout(button_panel)
@ -132,6 +190,19 @@ class SquadronDialog(QDialog):
self.toggle_leave_button.clicked.connect(self.toggle_leave) self.toggle_leave_button.clicked.connect(self.toggle_leave)
button_panel.addWidget(self.toggle_leave_button, alignment=Qt.AlignRight) button_panel.addWidget(self.toggle_leave_button, alignment=Qt.AlignRight)
@property
def squadron(self) -> Squadron:
return self.squadron_model.squadron
def on_destination_changed(self, index: int) -> None:
with report_errors("Could not change squadron destination", self):
destination = self.transfer_destination.itemData(index)
if destination is None:
self.squadron.cancel_relocation()
else:
self.squadron.plan_relocation(destination, self.theater)
self.ato_model.replace_from_game(player=True)
def check_disabled_button_states( def check_disabled_button_states(
self, button: QPushButton, index: QModelIndex self, button: QPushButton, index: QModelIndex
) -> bool: ) -> bool:

View File

@ -190,7 +190,7 @@ class QBaseMenu2(QDialog):
self.repair_button.setDisabled(True) self.repair_button.setDisabled(True)
def update_intel_summary(self) -> None: def update_intel_summary(self) -> None:
aircraft = self.cp.allocated_aircraft(self.game_model.game).total_present aircraft = self.cp.allocated_aircraft().total_present
parking = self.cp.total_aircraft_parking parking = self.cp.total_aircraft_parking
ground_unit_limit = self.cp.frontline_unit_count_limit ground_unit_limit = self.cp.frontline_unit_count_limit
deployable_unit_info = "" deployable_unit_info = ""

View File

@ -1,6 +1,6 @@
from PySide2.QtWidgets import QTabWidget from PySide2.QtWidgets import QTabWidget
from game.theater import ControlPoint, OffMapSpawn, Fob from game.theater import ControlPoint, Fob
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.windows.basemenu.DepartingConvoysMenu import DepartingConvoysMenu from qt_ui.windows.basemenu.DepartingConvoysMenu import DepartingConvoysMenu
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
@ -13,7 +13,7 @@ class QBaseMenuTabs(QTabWidget):
super(QBaseMenuTabs, self).__init__() super(QBaseMenuTabs, self).__init__()
if not cp.captured: if not cp.captured:
self.intel = QIntelInfo(cp, game_model.game) self.intel = QIntelInfo(cp)
self.addTab(self.intel, "Intel") self.addTab(self.intel, "Intel")
self.departing_convoys = DepartingConvoysMenu(cp, game_model) self.departing_convoys = DepartingConvoysMenu(cp, game_model)

View File

@ -273,6 +273,8 @@ class UnitTransactionFrame(QFrame, Generic[TransactionItemType]):
else: else:
return "Unit can not be sold." return "Unit can not be sold."
def info(self, unit_type: UnitType) -> None: def info(self, item: TransactionItemType) -> None:
self.info_window = QUnitInfoWindow(self.game_model.game, unit_type) self.info_window = QUnitInfoWindow(
self.game_model.game, self.purchase_adapter.unit_type_of(item)
)
self.info_window.show() self.info_window.show()

View File

@ -21,12 +21,7 @@ from game.purchaseadapter import AircraftPurchaseAdapter
class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]): class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]):
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None: def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
super().__init__( super().__init__(game_model, AircraftPurchaseAdapter(cp))
game_model,
AircraftPurchaseAdapter(
cp, game_model.game.coalition_for(cp.captured), game_model.game
),
)
self.cp = cp self.cp = cp
self.game_model = game_model self.game_model = game_model
self.purchase_groups = {} self.purchase_groups = {}
@ -98,7 +93,7 @@ class QHangarStatus(QHBoxLayout):
self.setAlignment(Qt.AlignLeft) self.setAlignment(Qt.AlignLeft)
def update_label(self) -> None: def update_label(self) -> None:
next_turn = self.control_point.allocated_aircraft(self.game_model.game) next_turn = self.control_point.allocated_aircraft()
max_amount = self.control_point.total_aircraft_parking max_amount = self.control_point.total_aircraft_parking
components = [f"{next_turn.total_present} present"] components = [f"{next_turn.total_present} present"]

View File

@ -11,22 +11,20 @@ from PySide2.QtWidgets import (
QWidget, QWidget,
) )
from game import Game
from game.theater import ControlPoint from game.theater import ControlPoint
class QIntelInfo(QFrame): class QIntelInfo(QFrame):
def __init__(self, cp: ControlPoint, game: Game): def __init__(self, cp: ControlPoint):
super(QIntelInfo, self).__init__() super(QIntelInfo, self).__init__()
self.cp = cp self.cp = cp
self.game = game
layout = QVBoxLayout() layout = QVBoxLayout()
scroll_content = QWidget() scroll_content = QWidget()
intel_layout = QVBoxLayout() intel_layout = QVBoxLayout()
units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
for unit_type, count in self.cp.allocated_aircraft(game).present.items(): for unit_type, count in self.cp.allocated_aircraft().present.items():
if count: if count:
task_type = unit_type.dcs_unit_type.task_default.name task_type = unit_type.dcs_unit_type.task_default.name
units_by_task[task_type][unit_type.name] += count units_by_task[task_type][unit_type.name] += count

View File

@ -77,7 +77,7 @@ class AircraftIntelLayout(IntelTableLayout):
total = 0 total = 0
for control_point in game.theater.control_points_for(player): for control_point in game.theater.control_points_for(player):
allocation = control_point.allocated_aircraft(game) allocation = control_point.allocated_aircraft()
base_total = allocation.total_present base_total = allocation.total_present
total += base_total total += base_total
if not base_total: if not base_total:

View File

@ -85,7 +85,7 @@ class QFlightCreator(QDialog):
squadron, initial_size=self.flight_size_spinner.value() squadron, initial_size=self.flight_size_spinner.value()
) )
self.roster_editor = FlightRosterEditor(roster) self.roster_editor = FlightRosterEditor(roster)
self.flight_size_spinner.valueChanged.connect(self.resize_roster) self.flight_size_spinner.valueChanged.connect(self.roster_editor.resize)
self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed) self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed)
roster_layout = QHBoxLayout() roster_layout = QHBoxLayout()
layout.addLayout(roster_layout) layout.addLayout(roster_layout)
@ -136,10 +136,6 @@ class QFlightCreator(QDialog):
def set_custom_name_text(self, text: str): def set_custom_name_text(self, text: str):
self.custom_name_text = text self.custom_name_text = text
def resize_roster(self, new_size: int) -> None:
self.roster_editor.roster.resize(new_size)
self.roster_editor.resize(new_size)
def verify_form(self) -> Optional[str]: def verify_form(self) -> Optional[str]:
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData() aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
squadron: Optional[Squadron] = self.squadron_selector.currentData() squadron: Optional[Squadron] = self.squadron_selector.currentData()
@ -182,8 +178,6 @@ class QFlightCreator(QDialog):
roster.max_size, roster.max_size,
task, task,
self.start_type.currentText(), self.start_type.currentText(),
squadron.location,
squadron.location,
divert, divert,
custom_name=self.custom_name_text, custom_name=self.custom_name_text,
roster=roster, roster=roster,
@ -198,7 +192,6 @@ class QFlightCreator(QDialog):
self.squadron_selector.update_items( self.squadron_selector.update_items(
self.task_selector.currentData(), new_aircraft self.task_selector.currentData(), new_aircraft
) )
self.departure.change_aircraft(new_aircraft)
self.divert.change_aircraft(new_aircraft) self.divert.change_aircraft(new_aircraft)
def on_departure_changed(self, departure: ControlPoint) -> None: def on_departure_changed(self, departure: ControlPoint) -> None:
@ -223,6 +216,7 @@ class QFlightCreator(QDialog):
def on_squadron_changed(self, index: int) -> None: def on_squadron_changed(self, index: int) -> None:
squadron: Optional[Squadron] = self.squadron_selector.itemData(index) squadron: Optional[Squadron] = self.squadron_selector.itemData(index)
self.update_max_size(self.squadron_selector.aircraft_available)
# Clear the roster first so we return the pilots to the pool. This way if we end # Clear the roster first so we return the pilots to the pool. This way if we end
# up repopulating from the same squadron we'll get the same pilots back. # up repopulating from the same squadron we'll get the same pilots back.
self.roster_editor.replace(None) self.roster_editor.replace(None)
@ -230,7 +224,7 @@ class QFlightCreator(QDialog):
self.roster_editor.replace( self.roster_editor.replace(
FlightRoster(squadron, self.flight_size_spinner.value()) FlightRoster(squadron, self.flight_size_spinner.value())
) )
self.on_departure_changed(squadron.location) self.on_departure_changed(squadron.location)
def update_max_size(self, available: int) -> None: def update_max_size(self, available: int) -> None:
aircraft = self.aircraft_selector.currentData() aircraft = self.aircraft_selector.currentData()

View File

@ -176,6 +176,8 @@ class FlightRosterEditor(QVBoxLayout):
def resize(self, new_size: int) -> None: def resize(self, new_size: int) -> None:
if new_size > self.MAX_PILOTS: if new_size > self.MAX_PILOTS:
raise ValueError("A flight may not have more than four pilots.") raise ValueError("A flight may not have more than four pilots.")
if self.roster is not None:
self.roster.resize(new_size)
for controls in self.pilot_controls[:new_size]: for controls in self.pilot_controls[:new_size]:
controls.enable_and_reset() controls.enable_and_reset()
for controls in self.pilot_controls[new_size:]: for controls in self.pilot_controls[new_size:]:

View File

@ -65,6 +65,8 @@ class NewGameWizard(QtWidgets.QWizard):
logging.info("======================") logging.info("======================")
campaign = self.field("selectedCampaign") campaign = self.field("selectedCampaign")
if campaign is None:
campaign = self.theater_page.campaignList.selected_campaign
if campaign is None: if campaign is None:
campaign = self.campaigns[0] campaign = self.campaigns[0]
@ -94,7 +96,6 @@ class NewGameWizard(QtWidgets.QWizard):
enemy_budget=int(self.field("enemy_starting_money")), enemy_budget=int(self.field("enemy_starting_money")),
# QSlider forces integers, so we use 1 to 50 and divide by 10 to # QSlider forces integers, so we use 1 to 50 and divide by 10 to
# give 0.1 to 5.0. # give 0.1 to 5.0.
midgame=False,
inverted=self.field("invertMap"), inverted=self.field("invertMap"),
no_carrier=self.field("no_carrier"), no_carrier=self.field("no_carrier"),
no_lha=self.field("no_lha"), no_lha=self.field("no_lha"),
@ -300,13 +301,13 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
text="Show incompatible campaigns" text="Show incompatible campaigns"
) )
show_incompatible_campaigns_checkbox.setChecked(False) show_incompatible_campaigns_checkbox.setChecked(False)
campaignList = QCampaignList( self.campaignList = QCampaignList(
campaigns, show_incompatible_campaigns_checkbox.isChecked() campaigns, show_incompatible_campaigns_checkbox.isChecked()
) )
show_incompatible_campaigns_checkbox.toggled.connect( show_incompatible_campaigns_checkbox.toggled.connect(
lambda checked: campaignList.setup_content(show_incompatible=checked) lambda checked: self.campaignList.setup_content(show_incompatible=checked)
) )
self.registerField("selectedCampaign", campaignList) self.registerField("selectedCampaign", self.campaignList)
# Faction description # Faction description
self.campaignMapDescription = QTextEdit("") self.campaignMapDescription = QTextEdit("")
@ -366,7 +367,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
template_perf = jinja_env.get_template( template_perf = jinja_env.get_template(
"campaign_performance_template_EN.j2" "campaign_performance_template_EN.j2"
) )
campaign = campaignList.selected_campaign campaign = self.campaignList.selected_campaign
self.setField("selectedCampaign", campaign) self.setField("selectedCampaign", campaign)
if campaign is None: if campaign is None:
self.campaignMapDescription.setText("No campaign selected") self.campaignMapDescription.setText("No campaign selected")
@ -379,11 +380,13 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
template_perf.render({"performance": campaign.performance}) template_perf.render({"performance": campaign.performance})
) )
campaignList.selectionModel().setCurrentIndex( self.campaignList.selectionModel().setCurrentIndex(
campaignList.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows self.campaignList.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows
) )
campaignList.selectionModel().selectionChanged.connect(on_campaign_selected) self.campaignList.selectionModel().selectionChanged.connect(
on_campaign_selected
)
on_campaign_selected() on_campaign_selected()
docsText = QtWidgets.QLabel( docsText = QtWidgets.QLabel(
@ -410,7 +413,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
layout = QtWidgets.QGridLayout() layout = QtWidgets.QGridLayout()
layout.setColumnMinimumWidth(0, 20) layout.setColumnMinimumWidth(0, 20)
layout.addWidget(campaignList, 0, 0, 5, 1) layout.addWidget(self.campaignList, 0, 0, 5, 1)
layout.addWidget(show_incompatible_campaigns_checkbox, 5, 0, 1, 1) layout.addWidget(show_incompatible_campaigns_checkbox, 5, 0, 1, 1)
layout.addWidget(docsText, 6, 0, 1, 1) layout.addWidget(docsText, 6, 0, 1, 1)
layout.addWidget(self.campaignMapDescription, 0, 1, 1, 1) layout.addWidget(self.campaignMapDescription, 0, 1, 1, 1)

View File

@ -22,7 +22,6 @@ from dcs.forcedoptions import ForcedOptions
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from game.game import Game from game.game import Game
from game.infos.information import Information
from game.settings import Settings, AutoAtoBehavior from game.settings import Settings, AutoAtoBehavior
from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs
@ -894,18 +893,6 @@ class QSettingsWindow(QDialog):
def cheatMoney(self, amount): def cheatMoney(self, amount):
logging.info("CHEATING FOR AMOUNT : " + str(amount) + "M") logging.info("CHEATING FOR AMOUNT : " + str(amount) + "M")
self.game.blue.budget += amount self.game.blue.budget += amount
if amount > 0:
self.game.informations.append(
Information(
"CHEATER",
"You are a cheater and you should feel bad",
self.game.turn,
)
)
else:
self.game.informations.append(
Information("CHEATER", "You are still a cheater !", self.game.turn)
)
GameUpdateSignal.get_instance().updateGame(self.game) GameUpdateSignal.get_instance().updateGame(self.game)
def applySettings(self): def applySettings(self):

View File

@ -1,5 +1,6 @@
altgraph==0.17 altgraph==0.17
appdirs==1.4.4 appdirs==1.4.4
attrs==21.2.0
black==21.4b0 black==21.4b0
certifi==2020.12.5 certifi==2020.12.5
cfgv==3.2.0 cfgv==3.2.0
@ -9,7 +10,9 @@ Faker==8.2.1
filelock==3.0.12 filelock==3.0.12
future==0.18.2 future==0.18.2
identify==1.5.13 identify==1.5.13
iniconfig==1.1.1
Jinja2==2.11.3 Jinja2==2.11.3
macholib==1.14
MarkupSafe==1.1.1 MarkupSafe==1.1.1
mypy==0.812 mypy==0.812
mypy-extensions==0.4.3 mypy-extensions==0.4.3
@ -17,14 +20,17 @@ nodeenv==1.5.0
packaging==20.9 packaging==20.9
pathspec==0.8.1 pathspec==0.8.1
pefile==2019.4.18 pefile==2019.4.18
Pillow==8.2.0 Pillow==8.3.2
pluggy==0.13.1
pre-commit==2.10.1 pre-commit==2.10.1
-e git://github.com/pydcs/dcs@eb0b9f2de660393ccd6ba17b2d82371d44e0d27b#egg=pydcs py==1.10.0
-e git://github.com/pydcs/dcs@5ec61b22a174ad8ddc762958998868db3150c947#egg=pydcs
pyinstaller==4.3 pyinstaller==4.3
pyinstaller-hooks-contrib==2021.1 pyinstaller-hooks-contrib==2021.1
pyparsing==2.4.7 pyparsing==2.4.7
pyproj==3.0.1 pyproj==3.0.1
PySide2==5.15.2 PySide2==5.15.2
pytest==6.2.4
python-dateutil==2.8.1 python-dateutil==2.8.1
pywin32-ctypes==0.2.0 pywin32-ctypes==0.2.0
PyYAML==5.4.1 PyYAML==5.4.1

View File

@ -7,4 +7,208 @@ recommended_enemy_faction: United Arab Emirates 2015
description: <p>You have managed to establish a foothold near Ras Al Khaima. Continue pushing south.</p> description: <p>You have managed to establish a foothold near Ras Al Khaima. Continue pushing south.</p>
miz: battle_of_abu_dhabi.miz miz: battle_of_abu_dhabi.miz
performance: 2 performance: 2
version": "8.0" version: "9.0"
squadrons:
# Blue CPs:
# The default faction is Iran, but the F-14B is given higher precedence so
# that it is used if the faction is something US. The F-14A will be used if
# the player picks some Iran faction that for some reason has carriers.
# Bandar Abbas:
# This is the main transit hub for blue, so it contains all the logistics-type
# squadrons: airlift, refueling, and AEW&C. It also contains an air-to-air
# squadron for self defense, a bomber squadron, and some air-to-ground
# squadrons.
#
# Due to its location, this will be the primary airbase for the initial phase
# of the campaign.
2:
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F-14A Tomcat (Block 135-GR Late)
- primary: DEAD
secondary: air-to-ground
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F-14A Tomcat (Block 135-GR Late)
- primary: SEAD
secondary: air-to-ground
aircraft:
- F-16CM Fighting Falcon (Block 50)
- F-4E Phantom II
- primary: AEW&C
aircraft:
- E-3A
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- primary: Transport
aircraft:
- C-17A
- primary: Strike
secondary: air-to-ground
aircraft:
- B-1B Lancer
- Su-24MK Fencer-D
# Kish:
# This airbase has better access to the theater as the front-line moves south
# west. It contains combat squadrons only.
24:
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-16CM Fighting Falcon (Block 50)
- MiG-29A Fulcrum-A
- primary: CAS
secondary: air-to-ground
aircraft:
- A-10C Thunderbolt II (Suite 7)
- A-10C Thunderbolt II (Suite 3)
- Su-25 Frogfoot
- primary: BAI
secondary: air-to-ground
aircraft:
- F-16CM Fighting Falcon (Block 50)
- Su-24MK Fencer-D
- primary: DEAD
secondary: air-to-ground
aircraft:
- F-16CM Fighting Falcon (Block 50)
Blue CV:
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-14B Tomcat
- F-14A Tomcat (Block 135-GR Late)
- primary: BARCAP
secondary: any
aircraft:
- F-14B Tomcat
- F-14A Tomcat (Block 135-GR Late)
- primary: Strike
secondary: any
aircraft:
- F/A-18C Hornet (Lot 20)
- F-14A Tomcat (Block 135-GR Late)
- primary: BAI
secondary: any
aircraft:
- F/A-18C Hornet (Lot 20)
- F-14A Tomcat (Block 135-GR Late)
- primary: Refueling
aircraft:
- S-3B Tanker
Blue LHA:
- primary: BAI
secondary: air-to-ground
aircraft:
- AV-8B Harrier II Night Attack
- primary: CAS
secondary: air-to-ground
aircraft:
- UH-1H Iroquois
- SH-60B Seahawk
# Red CPs:
# Squadrons are designed to work with either UAE 2015 (the default) or a
# typical Russian-sourced aircraft faction.
# Al Dhafra AFB:
# This CP has factories attached and is the largest red base, so is the main
# logistics hub, with an airlift, AEW&C, and refueling squadron.
#
# For combat this base operates two pure air-to-air squadrons, two pure air-
# to-ground, and four multi-role. Al Minhad is closest to the front so CAS
# squadrons are placed there, but will retreat here after capture.
4:
- primary: BARCAP
secondary: air-to-air
aircraft:
- Mirage 2000-5
- Mirage 2000C
- Su-30 Flanker-C
- Su-27 Flanker-B
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-16CM Fighting Falcon (Block 50)
- MiG-31 Foxhound
- MiG-25PD Foxbat-E
- primary: Strike
secondary: air-to-ground
aircraft:
- F-16CM Fighting Falcon (Block 50)
- Tu-160 Blackjack
- primary: SEAD
secondary: air-to-ground
aircraft:
- F-16CM Fighting Falcon (Block 50)
- primary: BAI
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
- Su-34 Fullback
- Su-24M Fencer-D
- primary: BAI
secondary: any
- primary: DEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
- primary: DEAD
secondary: any
- primary: AEW&C
aircraft:
- E-3A
- A-50
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- IL-78M
- primary: Transport
aircraft:
- C-17A
- IL-78MD
# Al Minhad AFB:
# The initial front line base. Contains CAS aircraft, as well as an air-to-air
# squadron and an air-to-ground squadron.
12:
- primary: CAS
secondary: air-to-ground
aircraft:
- AH-64D Apache Longbow
- Mi-24V Hind-E
- Mi-24P Hind-F
- primary: CAS
secondary: air-to-ground
aircraft:
- F-16CM Fighting Falcon (Block 50)
- Su-25 Frogfoot
- primary: BARCAP
secondary: air-to-air
aircraft:
- Mirage 2000-5
- Mirage 2000C
- Su-30 Flanker-C
- Su-27 Flanker-B
- primary: BAI
secondary: any
# Liwa AFB:
# The last-stand base. Contains some factories as well. Begins with only an
# air-to-air squadron. Other squadrons can retreat here as the front-line
# moves.
29:
- primary: BARCAP
secondary: air-to-air
aircraft:
- Mirage 2000-5
- Mirage 2000C
- MiG-31 Foxhound
- MiG-25PD Foxbat-E

View File

@ -4,7 +4,7 @@ theater: Caucasus
authors: Colonel Panic authors: Colonel Panic
description: <p>A medium sized theater with bases along the coast of the Black Sea.</p> description: <p>A medium sized theater with bases along the coast of the Black Sea.</p>
miz: black_sea.miz miz: black_sea.miz
performance: 2, performance: 2
version: "9.0" version: "9.0"
squadrons: squadrons:
# Anapa-Vityazevo # Anapa-Vityazevo
@ -148,4 +148,4 @@ squadrons:
- primary: BAI - primary: BAI
secondary: air-to-ground secondary: air-to-ground
- primary: CAS - primary: CAS
secondary: air-to-ground secondary: air-to-ground

View File

@ -270,6 +270,41 @@ local unitPayloads = {
[1] = 31, [1] = 31,
}, },
}, },
[7] = {
["name"] = "Liberation Ferry",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 9,
},
[2] = {
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
["num"] = 8,
},
[3] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[4] = {
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
["num"] = 2,
},
[5] = {
["CLSID"] = "MXU-648-TP",
["num"] = 6,
},
[6] = {
["CLSID"] = "MXU-648-TP",
["num"] = 4,
},
[7] = {
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
},
["tasks"] = {
},
},
}, },
["unitType"] = "F-16C_50", ["unitType"] = "F-16C_50",
} }

View File

@ -13,6 +13,7 @@ if dcsLiberation then
-- specific options -- specific options
local smoke = false local smoke = false
local fc3LaserCode = false
-- retrieve specific options values -- retrieve specific options values
if dcsLiberation.plugins then if dcsLiberation.plugins then
@ -22,6 +23,9 @@ if dcsLiberation then
env.info("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins.jtacautolase") env.info("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins.jtacautolase")
smoke = dcsLiberation.plugins.jtacautolase.smoke smoke = dcsLiberation.plugins.jtacautolase.smoke
env.info(string.format("DCSLiberation|JTACAutolase plugin - smoke = %s",tostring(smoke))) env.info(string.format("DCSLiberation|JTACAutolase plugin - smoke = %s",tostring(smoke)))
fc3LaserCode = dcsLiberation.plugins.jtacautolase.fc3LaserCode
env.info(string.format("DCSLiberation|JTACAutolase plugin - fc3LaserCode = %s",tostring(fc3LaserCode)))
end end
end end
@ -29,6 +33,11 @@ if dcsLiberation then
for _, jtac in pairs(dcsLiberation.JTACs) do for _, jtac in pairs(dcsLiberation.JTACs) do
env.info(string.format("DCSLiberation|JTACAutolase plugin - setting up %s",jtac.dcsUnit)) env.info(string.format("DCSLiberation|JTACAutolase plugin - setting up %s",jtac.dcsUnit))
if JTACAutoLase then if JTACAutoLase then
if fc3LaserCode then
-- If fc3LaserCode is enabled in the plugin configuration, force the JTAC
-- laser code to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs.
jtac.laserCode = 1113
end
env.info("DCSLiberation|JTACAutolase plugin - calling JTACAutoLase") env.info("DCSLiberation|JTACAutolase plugin - calling JTACAutoLase")
JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle') JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle')
end end

View File

@ -6,6 +6,11 @@
"nameInUI": "Use smoke", "nameInUI": "Use smoke",
"mnemonic": "smoke", "mnemonic": "smoke",
"defaultValue": true "defaultValue": true
},
{
"nameInUI": "Use FC3 laser code (1113)",
"mnemonic": "fc3LaserCode",
"defaultValue": false
} }
], ],
"scriptsWorkOrders": [ "scriptsWorkOrders": [

View File

@ -0,0 +1,15 @@
---
name: 104th FS
nickname: Eagles
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 104th FS Maryland ANG, Baltimore (MD)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 118th FS
nickname: Flying Yankees
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 118th FS Bradley ANGB, Connecticut (CT)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 172nd FS
nickname:
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 172nd FS Battle Creek ANGB, Michigan (BC)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 184th FS
nickname: Flying Razorbacks
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 184th FS Arkansas ANG, Fort Smith (FS)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 190th FS
nickname: Skull Bangers
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 190th FS Boise ANGB, Idaho (ID)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 25th FS
nickname: Assam Draggins
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 25th FS Osab AB, Korea (OS)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 354th FS
nickname: Bulldogs
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 354th FS Davis Monthan AFB, Arizona (DM)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 355th FS
nickname: Fightin' Falcons
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 355th FS Eielson AFB, Alaska (AK)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 357th FS
nickname: Dragons
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 357th FS Davis Monthan AFB, Arizona (DM)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 358th FS
nickname: Lobos
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 358th FS Davis Monthan AFB, Arizona (DM)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 47th FS
nickname: Termites
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 47th FS Barksdale AFB, Louisiana (BD)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 74th TFS
nickname: Flying Tigers
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 23rd TFW England AFB (EL)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 81st FS
nickname: Termites
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 3)
livery: 81st FS Spangdahlem AB, Germany (SP) 2
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 25th FS
nickname: Assam Draggins
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 7)
livery: 25th FS Osab AB, Korea (OS)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 354th FS
nickname: Bulldogs
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 7)
livery: 354th FS Davis Monthan AFB, Arizona (DM)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 355th FS
nickname: Fightin' Falcons
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 7)
livery: 355th FS Eielson AFB, Alaska (AK)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 357th FS
nickname: Dragons
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 7)
livery: 357th FS Davis Monthan AFB, Arizona (DM)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 358th FS
nickname: Lobos
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 7)
livery: 358th FS Davis Monthan AFB, Arizona (DM)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 81st FS
nickname: Termites
country: USA
role: Close Air Support
aircraft: A-10C Thunderbolt II (Suite 7)
livery: 81st FS Spangdahlem AB, Germany (SP) 2
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,9 @@
---
name: VAW-125
nickname: Tigertails
country: USA
role: AEW&C
aircraft: E-2C Hawkeye
livery: VAW-125 Tigertails
mission_types:
- AEW&C

View File

@ -0,0 +1,9 @@
---
name: 960th AAC Squadron
nickname: Vikings
country: USA
role: AEW&C
aircraft: E-3A
livery: usaf standard
mission_types:
- AEW&C

View File

@ -0,0 +1,14 @@
---
name: 12th FS
nickname: Dirty Dozen
country: USA
role: Air Superiority Fighter
aircraft: F-15C Eagle
livery: 12th Fighter SQN (AK)
mission_types:
- BARCAP
- Escort
- Intercept
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,14 @@
---
name: 390th FS
nickname: Wild Boars
country: USA
role: Air Superiority Fighter
aircraft: F-15C Eagle
livery: 390th Fighter SQN
mission_types:
- BARCAP
- Escort
- Intercept
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,14 @@
---
name: 493rd FS
nickname: Grim Reapers
country: USA
role: Air Superiority Fighter
aircraft: F-15C Eagle
livery: 493rd Fighter SQN (LN)
mission_types:
- BARCAP
- Escort
- Intercept
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,14 @@
---
name: 58th FS
nickname: Gorillas
country: USA
role: Air Superiority Fighter
aircraft: F-15C Eagle
livery: 58th Fighter SQN (EG)
mission_types:
- BARCAP
- Escort
- Intercept
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,20 @@
---
name: VF-11
nickname: Red Rippers
country: USA
role: Strike Fighter
aircraft: F-14A Tomcat (Block 135-GR Late)
livery: VF-11 Red Rippers 106
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,20 @@
---
name: VF-111
nickname: Sundowners
country: USA
role: Strike Fighter
aircraft: F-14A Tomcat (Block 135-GR Late)
livery: VF-111 Sundowners 200
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,20 @@
---
name: VF-21
nickname: Freelancers
country: USA
role: Strike Fighter
aircraft: F-14A Tomcat (Block 135-GR Late)
livery: VF-21 Freelancers 200
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,20 @@
---
name: VF-211
nickname: Fighting Checkmates
country: USA
role: Strike Fighter
aircraft: F-14A Tomcat (Block 135-GR Late)
livery: VF-211 Fighting Checkmates 105
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,20 @@
---
name: VF-33
nickname: Starfighters
country: USA
role: Strike Fighter
aircraft: F-14A Tomcat (Block 135-GR Late)
livery: VF-33 Starfighters 201
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,20 @@
---
name: VF-101
nickname: Grim Reapers
country: USA
role: Strike Fighter
aircraft: F-14B Tomcat
livery: VF-101 Dark
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,20 @@
---
name: VF-102
nickname: Diamond Backs
country: USA
role: Strike Fighter
aircraft: F-14B Tomcat
livery: VF-102 Diamondbacks 102 (2000)
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,19 @@
---
name: VF-142
nickname: Ghostriders
country: USA
role: Strike Fighter
aircraft: F-14B Tomcat
livery: VF-142 Ghostriders
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,19 @@
---
name: VF-211
nickname: Fighting Checkmates
country: USA
role: Strike Fighter
aircraft: F-14B Tomcat
livery: VF-211 Fighting Checkmates
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,8 @@
---
name: VMGR-352
nickname: Raiders
country: USA
role: Air-to-Air Refueling
aircraft: KC-130
mission_types:
- Refueling

View File

@ -0,0 +1,8 @@
---
name: 18th Air Refueling Squadron
nickname:
country: USA
role: Air-to-Air Refueling
aircraft: KC-135 Stratotanker
mission_types:
- Refueling

View File

@ -0,0 +1,9 @@
---
name: 101st Tanker Squadron
nickname: Asena
country: Turkey
role: Air-to-Air Refueling
aircraft: KC-135 Stratotanker
livery: TurAF Standard
mission_types:
- Refueling

View File

@ -0,0 +1,8 @@
---
name: 340th Expeditionary Air Refueling Squadron
nickname: Pythons
country: USA
role: Air-to-Air Refueling
aircraft: KC-135 Stratotanker MPRS
mission_types:
- Refueling

View File

@ -0,0 +1,14 @@
---
name: 115th Guards Aviation Regiment
nickname: 115th GvIAP
country: Russia
role: Air Superiority Fighter
aircraft: MiG-29S Fulcrum-C
livery: "115 GvIAP_Termez"
mission_types:
- BARCAP
- TARCAP
- Escort
- Intercept
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,14 @@
---
name: 28th Guards Aviation Regiment
nickname: 28th GvIAP
country: Russia
role: Air Superiority Fighter
aircraft: MiG-29S Fulcrum-C
livery: "28 GvIAP_Andreapol"
mission_types:
- BARCAP
- TARCAP
- Escort
- Intercept
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,14 @@
---
name: 31st Guards Aviation Regiment
nickname: 31st GvIAP
country: Russia
role: Air Superiority Fighter
aircraft: MiG-29S Fulcrum-C
livery: "31 GvIAP_Zernograd"
mission_types:
- BARCAP
- TARCAP
- Escort
- Intercept
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,14 @@
---
name: 773rd Aviation Regiment
nickname: 773rd IAP
country: Russia
role: Air Superiority Fighter
aircraft: MiG-29S Fulcrum-C
livery: "773 IAP_Damgarten"
mission_types:
- BARCAP
- TARCAP
- Escort
- Intercept
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,14 @@
---
name: 335th FS
nickname: Chiefs
country: USA
role: Strike Fighter
aircraft: F-15E Strike Eagle
livery: 335th Fighter SQN (SJ)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,15 @@
---
name: 492nd FS
nickname: Chiefs
country: USA
role: Strike Fighter
aircraft: F-15E Strike Eagle
livery: 492d Fighter SQN (LN)
mission_types:
- BAI
- CAS
- DEAD
- OCA/Aircraft
- OCA/Runway
- Strike

View File

@ -0,0 +1,21 @@
---
name: 132nd FW
nickname: Hawkeyes
country: USA
role: Strike Fighter
aircraft: F-16CM Fighting Falcon (Block 50)
livery: 132nd_Wing _Iowa_ANG
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- SEAD Escort
- Strike
- Fighter sweep
- TARCAP

View File

@ -0,0 +1,21 @@
---
name: 13th FS
nickname: Panthers
country: USA
role: Strike Fighter
aircraft: F-16CM Fighting Falcon (Block 50)
livery: 13th_Fighter_Squadron
mission_types:
- BAI
- BARCAP
- CAS
- DEAD
- Escort
- Intercept
- OCA/Aircraft
- OCA/Runway
- SEAD
- SEAD Escort
- Strike
- Fighter sweep
- TARCAP

Some files were not shown because too many files have changed in this diff Show More