mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Merge remote-tracking branch 'khopa/develop' into helipads
# Conflicts: # changelog.md
This commit is contained in:
commit
e84e36fd22
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -36,6 +36,11 @@ jobs:
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: mypy tests
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy tests
|
||||
|
||||
- name: update build number
|
||||
run: |
|
||||
|
||||
33
.github/workflows/test.yml
vendored
Normal file
33
.github/workflows/test.yml
vendored
Normal 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
|
||||
@ -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]** 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]** (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]** 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]** 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]** 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]** 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]** 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.
|
||||
* **[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.
|
||||
* **[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]** Enemy aircraft inventory now viewable in the air wing menu.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[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]** 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]** Fixed bug where an incompatible campaign could be generated if no action is taken on the campaign selection screen.
|
||||
|
||||
# 4.1.1
|
||||
|
||||
|
||||
@ -35,10 +35,10 @@ class DefaultSquadronAssigner:
|
||||
pass
|
||||
|
||||
def assign(self) -> None:
|
||||
for control_point, squadron_configs in self.config.by_location.items():
|
||||
if not control_point.is_friendly(self.coalition.player):
|
||||
continue
|
||||
for squadron_config in squadron_configs:
|
||||
for control_point in self.game.theater.control_points_for(
|
||||
self.coalition.player
|
||||
):
|
||||
for squadron_config in self.config.by_location[control_point]:
|
||||
squadron_def = self.find_squadron_for(squadron_config, control_point)
|
||||
if squadron_def is None:
|
||||
logging.info(
|
||||
|
||||
@ -105,8 +105,7 @@ class MizCampaignLoader:
|
||||
|
||||
@staticmethod
|
||||
def control_point_from_airport(airport: Airport) -> ControlPoint:
|
||||
cp = Airfield(airport)
|
||||
cp.captured = airport.is_blue()
|
||||
cp = Airfield(airport, starts_blue=airport.is_blue())
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
@ -249,30 +248,38 @@ class MizCampaignLoader:
|
||||
for blue in (False, True):
|
||||
for group in self.off_map_spawns(blue):
|
||||
control_point = OffMapSpawn(
|
||||
next(self.control_point_id), str(group.name), group.position
|
||||
next(self.control_point_id),
|
||||
str(group.name),
|
||||
group.position,
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for ship in self.carriers(blue):
|
||||
control_point = Carrier(
|
||||
ship.name, ship.position, next(self.control_point_id)
|
||||
ship.name,
|
||||
ship.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for ship in self.lhas(blue):
|
||||
control_point = Lha(
|
||||
ship.name, ship.position, next(self.control_point_id)
|
||||
ship.name,
|
||||
ship.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for fob in self.fobs(blue):
|
||||
control_point = Fob(
|
||||
str(fob.name), fob.position, next(self.control_point_id)
|
||||
str(fob.name),
|
||||
fob.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
|
||||
@ -61,13 +61,256 @@ class SquadronDefGenerator:
|
||||
adjective = random.choice(
|
||||
(
|
||||
None,
|
||||
"Red",
|
||||
"Blue",
|
||||
"Green",
|
||||
"Golden",
|
||||
"Aggressive",
|
||||
"Alpha",
|
||||
"Ancient",
|
||||
"Angelic",
|
||||
"Angry",
|
||||
"Apoplectic",
|
||||
"Aquamarine",
|
||||
"Astral",
|
||||
"Avenging",
|
||||
"Azure",
|
||||
"Badass",
|
||||
"Barbaric",
|
||||
"Battle",
|
||||
"Battling",
|
||||
"Bellicose",
|
||||
"Belligerent",
|
||||
"Big",
|
||||
"Bionic",
|
||||
"Black",
|
||||
"Bladed",
|
||||
"Blazoned",
|
||||
"Blood",
|
||||
"Bloody",
|
||||
"Blue",
|
||||
"Bold",
|
||||
"Boxing",
|
||||
"Brash",
|
||||
"Brass",
|
||||
"Brave",
|
||||
"Brazen",
|
||||
"Brown",
|
||||
"Brutal",
|
||||
"Brzone",
|
||||
"Burning",
|
||||
"Buzzing",
|
||||
"Celestial",
|
||||
"Clever",
|
||||
"Cloud",
|
||||
"Cobalt",
|
||||
"Copper",
|
||||
"Coral",
|
||||
"Crazy",
|
||||
"Crimson",
|
||||
"Crouching",
|
||||
"Cursed",
|
||||
"Cyan",
|
||||
"Danger",
|
||||
"Dangerous",
|
||||
"Dapper",
|
||||
"Daring",
|
||||
"Dark",
|
||||
"Dawn",
|
||||
"Day",
|
||||
"Deadly",
|
||||
"Death",
|
||||
"Defiant",
|
||||
"Demon",
|
||||
"Desert",
|
||||
"Devil",
|
||||
"Devil's",
|
||||
"Diabolical",
|
||||
"Diamond",
|
||||
"Dire",
|
||||
"Dirty",
|
||||
"Doom",
|
||||
"Doomed",
|
||||
"Double",
|
||||
"Drunken",
|
||||
"Dusk",
|
||||
"Dusty",
|
||||
"Eager",
|
||||
"Ebony",
|
||||
"Electric",
|
||||
"Emerald",
|
||||
"Eternal",
|
||||
"Evil",
|
||||
"Faithful",
|
||||
"Famous",
|
||||
"Fanged",
|
||||
"Fearless",
|
||||
"Feisty",
|
||||
"Ferocious",
|
||||
"Fierce",
|
||||
"Fiery",
|
||||
"Fighting",
|
||||
"Fire",
|
||||
"First",
|
||||
"Flame",
|
||||
"Flaming",
|
||||
"Flying",
|
||||
"Forest",
|
||||
"Frenzied",
|
||||
"Frosty",
|
||||
"Frozen",
|
||||
"Furious",
|
||||
"Gallant",
|
||||
"Ghost",
|
||||
"Giant",
|
||||
"Gigantic",
|
||||
"Glaring",
|
||||
"Global",
|
||||
"Gold",
|
||||
"Golden",
|
||||
"Green",
|
||||
"Grey",
|
||||
"Grim",
|
||||
"Grizzly",
|
||||
"Growling",
|
||||
"Grumpy",
|
||||
"Hammer",
|
||||
"Hard",
|
||||
"Hardy",
|
||||
"Heavy",
|
||||
"Hell",
|
||||
"Hell's",
|
||||
"Hidden",
|
||||
"Homicidal",
|
||||
"Hostile",
|
||||
"Howling",
|
||||
"Hyper",
|
||||
"Ice",
|
||||
"Icy",
|
||||
"Immortal",
|
||||
"Indignant",
|
||||
"Infamous",
|
||||
"Invincible",
|
||||
"Iron",
|
||||
"Jolly",
|
||||
"Laser",
|
||||
"Lava",
|
||||
"Lavender",
|
||||
"Lethal",
|
||||
"Light",
|
||||
"Lightning",
|
||||
"Livid",
|
||||
"Lucky",
|
||||
"Mad",
|
||||
"Magenta",
|
||||
"Magma",
|
||||
"Maroon",
|
||||
"Menacing",
|
||||
"Merciless",
|
||||
"Metal",
|
||||
"Midnight",
|
||||
"Mighty",
|
||||
"Mithril",
|
||||
"Mocking",
|
||||
"Moon",
|
||||
"Mountain",
|
||||
"Muddy",
|
||||
"Nasty",
|
||||
"Naughty",
|
||||
"Night",
|
||||
"Nova",
|
||||
"Nutty",
|
||||
"Obsidian",
|
||||
"Ocean",
|
||||
"Oddball",
|
||||
"Old",
|
||||
"Omega",
|
||||
"Onyx",
|
||||
"Orange",
|
||||
"Perky",
|
||||
"Pink",
|
||||
"Power",
|
||||
"Prickly",
|
||||
"Proud",
|
||||
"Puckered",
|
||||
"Pugnacious",
|
||||
"Puking",
|
||||
"Purple",
|
||||
"Ragged",
|
||||
"Raging",
|
||||
"Rainbow",
|
||||
"Rampant",
|
||||
"Razor",
|
||||
"Ready",
|
||||
"Reaper",
|
||||
"Reckless",
|
||||
"Red",
|
||||
"Roaring",
|
||||
"Rocky",
|
||||
"Rolling",
|
||||
"Royal",
|
||||
"Rusty",
|
||||
"Sable",
|
||||
"Salty",
|
||||
"Sand",
|
||||
"Sarcastic",
|
||||
"Saucy",
|
||||
"Scarlet",
|
||||
"Scarred",
|
||||
"Scary",
|
||||
"Screaming",
|
||||
"Scythed",
|
||||
"Shadow",
|
||||
"Shiny",
|
||||
"Shocking",
|
||||
"Silver",
|
||||
"Sky",
|
||||
"Smoke",
|
||||
"Smokin'",
|
||||
"Snapping",
|
||||
"Snappy",
|
||||
"Snarling",
|
||||
"Snow",
|
||||
"Soaring",
|
||||
"Space",
|
||||
"Spiky",
|
||||
"Spiny",
|
||||
"Star",
|
||||
"Steady",
|
||||
"Steel",
|
||||
"Stone",
|
||||
"Storm",
|
||||
"Striking",
|
||||
"Strong",
|
||||
"Stubborn",
|
||||
"Sun",
|
||||
"Super",
|
||||
"Terrible",
|
||||
"Thorny",
|
||||
"Thunder",
|
||||
"Top",
|
||||
"Tough",
|
||||
"Toxic",
|
||||
"Tricky",
|
||||
"Turquoise",
|
||||
"Typhoon",
|
||||
"Ultimate",
|
||||
"Ultra",
|
||||
"Ultramarine",
|
||||
"Vengeful",
|
||||
"Venom",
|
||||
"Vermillion",
|
||||
"Vicious",
|
||||
"Victorious",
|
||||
"Vigilant",
|
||||
"Violent",
|
||||
"Violet",
|
||||
"War",
|
||||
"Water",
|
||||
"Whistling",
|
||||
"White",
|
||||
"Wicked",
|
||||
"Wild",
|
||||
"Wizard",
|
||||
"Wrathful",
|
||||
"Yellow",
|
||||
"Young",
|
||||
)
|
||||
)
|
||||
if adjective is None:
|
||||
|
||||
@ -40,7 +40,7 @@ class Coalition:
|
||||
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
|
||||
self.bullseye = Bullseye(Point(0, 0))
|
||||
self.faker = Faker(self.faction.locales)
|
||||
self.air_wing = AirWing(game)
|
||||
self.air_wing = AirWing(player)
|
||||
self.transfers = PendingTransfers(game, player)
|
||||
|
||||
# Late initialized because the two coalitions in the game are mutually
|
||||
@ -139,7 +139,7 @@ class Coalition:
|
||||
For more information on turn finalization in general, see the documentation for
|
||||
`Game.finish_turn`.
|
||||
"""
|
||||
self.air_wing.replenish()
|
||||
self.air_wing.end_turn()
|
||||
self.budget += Income(self.game, self.player).total
|
||||
|
||||
# Need to recompute before transfers and deliveries to account for captures.
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from game.commander.missionproposals import ProposedFlight
|
||||
from game.squadrons.airwing import AirWing
|
||||
from game.squadrons.squadron import Squadron
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import meters
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ClosestAirfields
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class AircraftAllocator:
|
||||
"""Finds suitable aircraft for proposed missions."""
|
||||
|
||||
def __init__(
|
||||
self, air_wing: AirWing, closest_airfields: ClosestAirfields, is_player: bool
|
||||
) -> None:
|
||||
self.air_wing = air_wing
|
||||
self.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
|
||||
def find_squadron_for_flight(
|
||||
self, target: MissionTarget, flight: ProposedFlight
|
||||
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||
"""Finds aircraft suitable for the given mission.
|
||||
|
||||
Searches for aircraft capable of performing the given mission within the
|
||||
maximum allowed range. If insufficient aircraft are available for the
|
||||
mission, None is returned.
|
||||
|
||||
Airfields are searched ordered nearest to farthest from the target and
|
||||
searched twice. The first search looks for aircraft which prefer the
|
||||
mission type, and the second search looks for any aircraft which are
|
||||
capable of the mission type. For example, an F-14 from a nearby carrier
|
||||
will be preferred for the CAP of an airfield that has only F-16s, but if
|
||||
the carrier has only F/A-18s the F-16s will be used for CAP instead.
|
||||
|
||||
Note that aircraft *will* be removed from the global inventory on
|
||||
success. This is to ensure that the same aircraft are not matched twice
|
||||
on subsequent calls. If the found aircraft are not used, the caller is
|
||||
responsible for returning them to the inventory.
|
||||
"""
|
||||
return self.find_aircraft_for_task(target, flight, flight.task)
|
||||
|
||||
def find_aircraft_for_task(
|
||||
self, target: MissionTarget, flight: ProposedFlight, task: FlightType
|
||||
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||
types = aircraft_for_task(task)
|
||||
for airfield in self.closest_airfields.operational_airfields:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
continue
|
||||
for aircraft in types:
|
||||
if not airfield.can_operate(aircraft):
|
||||
continue
|
||||
distance_to_target = meters(target.distance_to(airfield))
|
||||
if distance_to_target > aircraft.max_mission_range:
|
||||
continue
|
||||
# Valid location with enough aircraft available. Find a squadron to fit
|
||||
# the role.
|
||||
squadrons = self.air_wing.auto_assignable_for_task_with_type(
|
||||
aircraft, task, airfield
|
||||
)
|
||||
for squadron in squadrons:
|
||||
if squadron.operates_from(airfield) and squadron.can_fulfill_flight(
|
||||
flight.num_aircraft
|
||||
):
|
||||
return airfield, squadron
|
||||
return None
|
||||
@ -157,10 +157,7 @@ class ObjectiveFinder:
|
||||
for control_point in self.enemy_control_points():
|
||||
if not isinstance(control_point, Airfield):
|
||||
continue
|
||||
if (
|
||||
control_point.allocated_aircraft(self.game).total_present
|
||||
>= min_aircraft
|
||||
):
|
||||
if control_point.allocated_aircraft().total_present >= min_aircraft:
|
||||
airfields.append(control_point)
|
||||
return self._targets_by_range(airfields)
|
||||
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.commander.aircraftallocator import AircraftAllocator
|
||||
from game.commander.missionproposals import ProposedFlight
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.airwing import AirWing
|
||||
from game.theater import MissionTarget, OffMapSpawn, ControlPoint
|
||||
from game.utils import nautical_miles
|
||||
from gen.ato import Package
|
||||
from gen.flights.closestairfields import ClosestAirfields
|
||||
from game.theater import MissionTarget, OffMapSpawn, ControlPoint
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.airwing import AirWing
|
||||
from gen.flights.closestairfields import ClosestAirfields
|
||||
from .missionproposals import ProposedFlight
|
||||
|
||||
|
||||
class PackageBuilder:
|
||||
"""Builds a Package for the flights it receives."""
|
||||
|
||||
@ -28,7 +32,7 @@ class PackageBuilder:
|
||||
self.is_player = is_player
|
||||
self.package_country = package_country
|
||||
self.package = Package(location, auto_asap=asap)
|
||||
self.allocator = AircraftAllocator(air_wing, closest_airfields, is_player)
|
||||
self.air_wing = air_wing
|
||||
self.start_type = start_type
|
||||
|
||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||
@ -39,13 +43,13 @@ class PackageBuilder:
|
||||
caller should return any previously planned flights to the inventory
|
||||
using release_planned_aircraft.
|
||||
"""
|
||||
assignment = self.allocator.find_squadron_for_flight(self.package.target, plan)
|
||||
if assignment is None:
|
||||
squadron = self.air_wing.best_squadron_for(
|
||||
self.package.target, plan.task, plan.num_aircraft, this_turn=True
|
||||
)
|
||||
if squadron is None:
|
||||
return False
|
||||
airfield, squadron = assignment
|
||||
if isinstance(airfield, OffMapSpawn):
|
||||
start_type = "In Flight"
|
||||
else:
|
||||
start_type = squadron.location.required_aircraft_start_type
|
||||
if start_type is None:
|
||||
start_type = self.start_type
|
||||
|
||||
flight = Flight(
|
||||
@ -55,9 +59,7 @@ class PackageBuilder:
|
||||
plan.num_aircraft,
|
||||
plan.task,
|
||||
start_type,
|
||||
departure=airfield,
|
||||
arrival=airfield,
|
||||
divert=self.find_divert_field(squadron.aircraft, airfield),
|
||||
divert=self.find_divert_field(squadron.aircraft, squadron.location),
|
||||
)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
@ -8,7 +8,6 @@ from dcs.task import Task
|
||||
|
||||
from game import persistency
|
||||
from game.debriefing import Debriefing
|
||||
from game.infos.information import Information
|
||||
from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen.ato import AirTaskingOrder
|
||||
@ -173,13 +172,10 @@ class Event:
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.kill()
|
||||
self.game.informations.append(
|
||||
Information(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.message(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@ -191,19 +187,16 @@ class Event:
|
||||
for captured in debriefing.base_captures:
|
||||
try:
|
||||
if captured.captured_by_player:
|
||||
info = Information(
|
||||
self.game.message(
|
||||
f"{captured.control_point} captured!",
|
||||
f"We took control of {captured.control_point}.",
|
||||
self.game.turn,
|
||||
)
|
||||
else:
|
||||
info = Information(
|
||||
self.game.message(
|
||||
f"{captured.control_point} lost!",
|
||||
f"The enemy took control of {captured.control_point}.",
|
||||
self.game.turn,
|
||||
)
|
||||
|
||||
self.game.informations.append(info)
|
||||
captured.control_point.capture(self.game, captured.captured_by_player)
|
||||
logging.info(f"Will run redeploy for {captured.control_point}")
|
||||
self.redeploy_units(captured.control_point)
|
||||
@ -330,34 +323,28 @@ class Event:
|
||||
# Handle the case where there are no casualties at all on either side but both sides still have units
|
||||
if delta == 0.0:
|
||||
print(status_msg)
|
||||
info = Information(
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
else:
|
||||
if player_won:
|
||||
print(status_msg)
|
||||
cp.base.affect_strength(delta)
|
||||
enemy_cp.base.affect_strength(-delta)
|
||||
info = Information(
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
|
||||
self.game.turn,
|
||||
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
else:
|
||||
print(status_msg)
|
||||
enemy_cp.base.affect_strength(delta)
|
||||
cp.base.affect_strength(-delta)
|
||||
info = Information(
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} are losing ground against the enemy forces from "
|
||||
f"{enemy_cp.name}. {status_msg}",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
|
||||
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||
""" "
|
||||
@ -410,10 +397,8 @@ class Event:
|
||||
total_units_redeployed += move_count
|
||||
|
||||
if total_units_redeployed > 0:
|
||||
text = (
|
||||
self.game.message(
|
||||
"Units redeployed",
|
||||
f"{total_units_redeployed} units have been redeployed from "
|
||||
f"{source.name} to {destination.name}"
|
||||
f"{source.name} to {destination.name}",
|
||||
)
|
||||
info = Information("Units redeployed", text, self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
logging.info(text)
|
||||
|
||||
19
game/game.py
19
game/game.py
@ -107,8 +107,8 @@ class Game:
|
||||
self.game_stats = GameStats()
|
||||
self.notes = ""
|
||||
self.ground_planners: dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
self.informations: list[Information] = []
|
||||
self.message("Game Start", "-" * 40)
|
||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||
self.__culling_zones: List[Point] = []
|
||||
self.__destroyed_units: list[dict[str, Union[float, str]]] = []
|
||||
@ -125,6 +125,9 @@ class Game:
|
||||
self.blue.set_opponent(self.red)
|
||||
self.red.set_opponent(self.blue)
|
||||
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.finish_init(self)
|
||||
|
||||
self.blue.configure_default_air_wing(air_wing_config)
|
||||
self.red.configure_default_air_wing(air_wing_config)
|
||||
|
||||
@ -279,9 +282,7 @@ class Game:
|
||||
Args:
|
||||
skipped: True if the turn was skipped.
|
||||
"""
|
||||
self.informations.append(
|
||||
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
||||
)
|
||||
self.message("End of turn #" + str(self.turn), "-" * 40)
|
||||
self.turn += 1
|
||||
|
||||
# The coalition-specific turn finalization *must* happen before unit deliveries,
|
||||
@ -297,10 +298,6 @@ class Game:
|
||||
if not skipped:
|
||||
for cp in self.theater.player_points():
|
||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
elif self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
if not cp.is_carrier and not cp.is_lha:
|
||||
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
@ -414,8 +411,8 @@ class Game:
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
def message(self, title: str, text: str = "") -> None:
|
||||
self.informations.append(Information(title, text, turn=self.turn))
|
||||
|
||||
@property
|
||||
def current_turn_time_of_day(self) -> TimeOfDay:
|
||||
|
||||
@ -34,7 +34,7 @@ from gen.kneeboard import KneeboardGenerator
|
||||
from gen.lasercoderegistry import LaserCodeRegistry
|
||||
from gen.naming import namegen
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
from gen.tacan import TacanRegistry, TacanUsage
|
||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
from gen.visualgen import VisualGenerator
|
||||
from .. import db
|
||||
@ -242,7 +242,7 @@ class Operation:
|
||||
if beacon.channel is None:
|
||||
logging.error(f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
cls.tacan_registry.reserve(beacon.tacan_channel)
|
||||
cls.tacan_registry.mark_unavailable(beacon.tacan_channel)
|
||||
|
||||
@classmethod
|
||||
def _create_radio_registry(
|
||||
|
||||
@ -74,7 +74,7 @@ class ProcurementAi:
|
||||
self.game.coalition_for(self.is_player).transfers
|
||||
)
|
||||
armor_investment += cp_ground_units.total_value
|
||||
cp_aircraft = cp.allocated_aircraft(self.game)
|
||||
cp_aircraft = cp.allocated_aircraft()
|
||||
aircraft_investment += cp_aircraft.total_value
|
||||
|
||||
total_investment = aircraft_investment + armor_investment
|
||||
@ -221,45 +221,22 @@ class ProcurementAi:
|
||||
else:
|
||||
return self.game.theater.enemy_points()
|
||||
|
||||
@staticmethod
|
||||
def squadron_rank_for_task(squadron: Squadron, task: FlightType) -> int:
|
||||
return aircraft_for_task(task).index(squadron.aircraft)
|
||||
|
||||
def compatible_squadrons_at_airbase(
|
||||
self, airbase: ControlPoint, request: AircraftProcurementRequest
|
||||
) -> Iterator[Squadron]:
|
||||
compatible: list[Squadron] = []
|
||||
for squadron in airbase.squadrons:
|
||||
if not squadron.can_auto_assign(request.task_capability):
|
||||
continue
|
||||
if not squadron.can_provide_pilots(request.number):
|
||||
continue
|
||||
|
||||
distance_to_target = meters(request.near.distance_to(airbase))
|
||||
if distance_to_target > squadron.aircraft.max_mission_range:
|
||||
continue
|
||||
compatible.append(squadron)
|
||||
yield from sorted(
|
||||
compatible,
|
||||
key=lambda s: self.squadron_rank_for_task(s, request.task_capability),
|
||||
)
|
||||
|
||||
def best_squadrons_for(
|
||||
self, request: AircraftProcurementRequest
|
||||
) -> Iterator[Squadron]:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
|
||||
threatened = []
|
||||
for cp in distance_cache.operational_airfields:
|
||||
if not cp.is_friendly(self.is_player):
|
||||
for squadron in self.air_wing.best_squadrons_for(
|
||||
request.near, request.task_capability, request.number, this_turn=False
|
||||
):
|
||||
if not squadron.can_provide_pilots(request.number):
|
||||
continue
|
||||
if cp.unclaimed_parking(self.game) < request.number:
|
||||
if squadron.location.unclaimed_parking() < request.number:
|
||||
continue
|
||||
if self.threat_zones.threatened(cp.position):
|
||||
threatened.append(cp)
|
||||
if self.threat_zones.threatened(squadron.location.position):
|
||||
threatened.append(squadron)
|
||||
continue
|
||||
yield from self.compatible_squadrons_at_airbase(cp, request)
|
||||
for threatened_base in threatened:
|
||||
yield from self.compatible_squadrons_at_airbase(threatened_base, request)
|
||||
yield squadron
|
||||
yield from threatened
|
||||
|
||||
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
|
||||
worst_supply = math.inf
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from abc import abstractmethod
|
||||
from typing import TypeVar, Generic
|
||||
from typing import TypeVar, Generic, Any
|
||||
|
||||
from game import Game
|
||||
from game.coalition import Coalition
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ControlPoint
|
||||
|
||||
@ -90,14 +92,15 @@ class PurchaseAdapter(Generic[ItemType]):
|
||||
def name_of(self, item: ItemType, multiline: bool = False) -> str:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def unit_type_of(self, item: ItemType) -> UnitType[Any]:
|
||||
...
|
||||
|
||||
|
||||
class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
|
||||
def __init__(
|
||||
self, control_point: ControlPoint, coalition: Coalition, game: Game
|
||||
) -> None:
|
||||
super().__init__(coalition)
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
super().__init__(control_point.coalition)
|
||||
self.control_point = control_point
|
||||
self.game = game
|
||||
|
||||
def pending_delivery_quantity(self, item: Squadron) -> int:
|
||||
return item.pending_deliveries
|
||||
@ -106,10 +109,7 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
|
||||
return item.owned_aircraft
|
||||
|
||||
def can_buy(self, item: Squadron) -> bool:
|
||||
return (
|
||||
super().can_buy(item)
|
||||
and self.control_point.unclaimed_parking(self.game) > 0
|
||||
)
|
||||
return super().can_buy(item) and self.control_point.unclaimed_parking() > 0
|
||||
|
||||
def can_sell(self, item: Squadron) -> bool:
|
||||
return item.untasked_aircraft > 0
|
||||
@ -138,6 +138,9 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]):
|
||||
separator = " "
|
||||
return separator.join([item.aircraft.name, str(item)])
|
||||
|
||||
def unit_type_of(self, item: Squadron) -> AircraftType:
|
||||
return item.aircraft
|
||||
|
||||
|
||||
class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]):
|
||||
def __init__(
|
||||
@ -178,3 +181,6 @@ class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]):
|
||||
|
||||
def name_of(self, item: GroundUnitType, multiline: bool = False) -> str:
|
||||
return f"{item}"
|
||||
|
||||
def unit_type_of(self, item: GroundUnitType) -> GroundUnitType:
|
||||
return item
|
||||
|
||||
@ -2,24 +2,24 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from typing import Sequence, Iterator, TYPE_CHECKING
|
||||
from typing import Sequence, Iterator, TYPE_CHECKING, Optional
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from .squadron import Squadron
|
||||
from ..theater import ControlPoint
|
||||
from ..theater import ControlPoint, MissionTarget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class AirWing:
|
||||
def __init__(self, game: Game) -> None:
|
||||
self.game = game
|
||||
def __init__(self, player: bool) -> None:
|
||||
self.player = player
|
||||
self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
|
||||
|
||||
def add_squadron(self, squadron: Squadron) -> None:
|
||||
squadron.location.squadrons.append(squadron)
|
||||
self.squadrons[squadron.aircraft].append(squadron)
|
||||
|
||||
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
|
||||
@ -32,6 +32,35 @@ class AirWing:
|
||||
except StopIteration:
|
||||
return False
|
||||
|
||||
def best_squadrons_for(
|
||||
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool
|
||||
) -> list[Squadron]:
|
||||
airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
best_aircraft = aircraft_for_task(task)
|
||||
ordered: list[Squadron] = []
|
||||
for control_point in airfield_cache.operational_airfields:
|
||||
if control_point.captured != self.player:
|
||||
continue
|
||||
capable_at_base = []
|
||||
for squadron in control_point.squadrons:
|
||||
if squadron.can_auto_assign_mission(location, task, size, this_turn):
|
||||
capable_at_base.append(squadron)
|
||||
|
||||
ordered.extend(
|
||||
sorted(
|
||||
capable_at_base,
|
||||
key=lambda s: best_aircraft.index(s.aircraft),
|
||||
)
|
||||
)
|
||||
return ordered
|
||||
|
||||
def best_squadron_for(
|
||||
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool
|
||||
) -> Optional[Squadron]:
|
||||
for squadron in self.best_squadrons_for(location, task, size, this_turn):
|
||||
return squadron
|
||||
return None
|
||||
|
||||
@property
|
||||
def available_aircraft_types(self) -> Iterator[AircraftType]:
|
||||
for aircraft, squadrons in self.squadrons.items():
|
||||
@ -52,17 +81,6 @@ class AirWing:
|
||||
if squadron.can_auto_assign(task) and squadron.location == base:
|
||||
yield squadron
|
||||
|
||||
def auto_assignable_for_task_with_type(
|
||||
self, aircraft: AircraftType, task: FlightType, base: ControlPoint
|
||||
) -> Iterator[Squadron]:
|
||||
for squadron in self.squadrons_for(aircraft):
|
||||
if (
|
||||
squadron.location == base
|
||||
and squadron.can_auto_assign(task)
|
||||
and squadron.has_available_pilots
|
||||
):
|
||||
yield squadron
|
||||
|
||||
def squadron_for(self, aircraft: AircraftType) -> Squadron:
|
||||
return self.squadrons_for(aircraft)[0]
|
||||
|
||||
@ -76,9 +94,9 @@ class AirWing:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.populate_for_turn_0()
|
||||
|
||||
def replenish(self) -> None:
|
||||
def end_turn(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.replenish_lost_pilots()
|
||||
squadron.end_turn()
|
||||
|
||||
def reset(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
|
||||
@ -11,17 +11,20 @@ from typing import (
|
||||
|
||||
from faker import Faker
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.settings import AutoAtoBehavior, Settings
|
||||
from game.squadrons.operatingbases import OperatingBases
|
||||
from game.squadrons.pilot import Pilot, PilotStatus
|
||||
from game.squadrons.squadrondef import SquadronDef
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import FlightType, Flight
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from .pilot import Pilot, PilotStatus
|
||||
from ..utils import meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.coalition import Coalition
|
||||
from gen.flights.flight import FlightType
|
||||
from game.theater import ControlPoint
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.theater import ControlPoint, ConflictTheater, MissionTarget
|
||||
from .operatingbases import OperatingBases
|
||||
from .squadrondef import SquadronDef
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -53,6 +56,9 @@ class Squadron:
|
||||
settings: Settings = field(hash=False, compare=False)
|
||||
|
||||
location: ControlPoint
|
||||
destination: Optional[ControlPoint] = field(
|
||||
init=False, hash=False, compare=False, default=None
|
||||
)
|
||||
|
||||
owned_aircraft: int = field(init=False, hash=False, compare=False, default=0)
|
||||
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
|
||||
@ -82,9 +88,7 @@ class Squadron:
|
||||
return self.coalition.player
|
||||
|
||||
def assign_to_base(self, base: ControlPoint) -> None:
|
||||
self.location.squadrons.remove(self)
|
||||
self.location = base
|
||||
self.location.squadrons.append(self)
|
||||
logging.debug(f"Assigned {self} to {base}")
|
||||
|
||||
@property
|
||||
@ -169,6 +173,12 @@ class Squadron:
|
||||
raise ValueError("Squadrons can only be created with active pilots.")
|
||||
self._recruit_pilots(self.settings.squadron_pilot_limit)
|
||||
|
||||
def end_turn(self) -> None:
|
||||
if self.destination is not None:
|
||||
self.relocate_to(self.destination)
|
||||
self.replenish_lost_pilots()
|
||||
self.deliver_orders()
|
||||
|
||||
def replenish_lost_pilots(self) -> None:
|
||||
if not self.pilot_limits_enabled:
|
||||
return
|
||||
@ -243,6 +253,17 @@ class Squadron:
|
||||
def can_auto_assign(self, task: FlightType) -> bool:
|
||||
return task in self.auto_assignable_mission_types
|
||||
|
||||
def can_auto_assign_mission(
|
||||
self, location: MissionTarget, task: FlightType, size: int, this_turn: bool
|
||||
) -> bool:
|
||||
if not self.can_auto_assign(task):
|
||||
return False
|
||||
if this_turn and not self.can_fulfill_flight(size):
|
||||
return False
|
||||
|
||||
distance_to_target = meters(location.distance_to(self.location))
|
||||
return distance_to_target <= self.aircraft.max_mission_range
|
||||
|
||||
def operates_from(self, control_point: ControlPoint) -> bool:
|
||||
if control_point.is_carrier:
|
||||
return self.operating_bases.carrier
|
||||
@ -265,18 +286,133 @@ class Squadron:
|
||||
def can_fulfill_flight(self, count: int) -> bool:
|
||||
return self.can_provide_pilots(count) and self.untasked_aircraft >= count
|
||||
|
||||
def refund_orders(self) -> None:
|
||||
self.coalition.adjust_budget(self.aircraft.price * self.pending_deliveries)
|
||||
self.pending_deliveries = 0
|
||||
def refund_orders(self, count: Optional[int] = None) -> None:
|
||||
if count is None:
|
||||
count = self.pending_deliveries
|
||||
self.coalition.adjust_budget(self.aircraft.price * count)
|
||||
self.pending_deliveries -= count
|
||||
|
||||
def deliver_orders(self) -> None:
|
||||
self.cancel_overflow_orders()
|
||||
self.owned_aircraft += self.pending_deliveries
|
||||
self.pending_deliveries = 0
|
||||
|
||||
def relocate_to(self, destination: ControlPoint) -> None:
|
||||
self.location = destination
|
||||
if self.location == self.destination:
|
||||
self.destination = None
|
||||
|
||||
def cancel_overflow_orders(self) -> None:
|
||||
if self.pending_deliveries <= 0:
|
||||
return
|
||||
overflow = -self.location.unclaimed_parking()
|
||||
if overflow > 0:
|
||||
sell_count = min(overflow, self.pending_deliveries)
|
||||
logging.debug(
|
||||
f"{self.location} is overfull by {overflow} aircraft. Cancelling "
|
||||
f"orders for {sell_count} aircraft to make room."
|
||||
)
|
||||
self.refund_orders(sell_count)
|
||||
|
||||
@property
|
||||
def max_fulfillable_aircraft(self) -> int:
|
||||
return max(self.number_of_available_pilots, self.untasked_aircraft)
|
||||
|
||||
@property
|
||||
def expected_size_next_turn(self) -> int:
|
||||
return self.owned_aircraft + self.pending_deliveries
|
||||
|
||||
@property
|
||||
def arrival(self) -> ControlPoint:
|
||||
return self.location if self.destination is None else self.destination
|
||||
|
||||
def plan_relocation(
|
||||
self, destination: ControlPoint, theater: ConflictTheater
|
||||
) -> None:
|
||||
if destination == self.location:
|
||||
logging.warning(
|
||||
f"Attempted to plan relocation of {self} to current location "
|
||||
f"{destination}. Ignoring."
|
||||
)
|
||||
return
|
||||
if destination == self.destination:
|
||||
logging.warning(
|
||||
f"Attempted to plan relocation of {self} to current destination "
|
||||
f"{destination}. Ignoring."
|
||||
)
|
||||
return
|
||||
|
||||
if self.expected_size_next_turn >= destination.unclaimed_parking():
|
||||
raise RuntimeError(f"Not enough parking for {self} at {destination}.")
|
||||
if not destination.can_operate(self.aircraft):
|
||||
raise RuntimeError(f"{self} cannot operate at {destination}.")
|
||||
self.destination = destination
|
||||
self.replan_ferry_flights(theater)
|
||||
|
||||
def cancel_relocation(self) -> None:
|
||||
if self.destination is None:
|
||||
logging.warning(
|
||||
f"Attempted to cancel relocation of squadron with no transfer order. "
|
||||
"Ignoring."
|
||||
)
|
||||
return
|
||||
|
||||
if self.expected_size_next_turn >= self.location.unclaimed_parking():
|
||||
raise RuntimeError(f"Not enough parking for {self} at {self.location}.")
|
||||
self.destination = None
|
||||
self.cancel_ferry_flights()
|
||||
|
||||
def replan_ferry_flights(self, theater: ConflictTheater) -> None:
|
||||
self.cancel_ferry_flights()
|
||||
self.plan_ferry_flights(theater)
|
||||
|
||||
def cancel_ferry_flights(self) -> None:
|
||||
for package in self.coalition.ato.packages:
|
||||
# Copy the list so our iterator remains consistent throughout the removal.
|
||||
for flight in list(package.flights):
|
||||
if flight.squadron == self and flight.flight_type is FlightType.FERRY:
|
||||
package.remove_flight(flight)
|
||||
flight.return_pilots_and_aircraft()
|
||||
if not package.flights:
|
||||
self.coalition.ato.remove_package(package)
|
||||
|
||||
def plan_ferry_flights(self, theater: ConflictTheater) -> None:
|
||||
if self.destination is None:
|
||||
raise RuntimeError(
|
||||
f"Cannot plan ferry flights for {self} because there is no destination."
|
||||
)
|
||||
remaining = self.untasked_aircraft
|
||||
if not remaining:
|
||||
return
|
||||
|
||||
package = Package(self.destination)
|
||||
builder = FlightPlanBuilder(package, self.coalition, theater)
|
||||
while remaining:
|
||||
size = min(remaining, self.aircraft.max_group_size)
|
||||
self.plan_ferry_flight(builder, package, size)
|
||||
remaining -= size
|
||||
package.set_tot_asap()
|
||||
self.coalition.ato.add_package(package)
|
||||
|
||||
def plan_ferry_flight(
|
||||
self, builder: FlightPlanBuilder, package: Package, size: int
|
||||
) -> None:
|
||||
start_type = self.location.required_aircraft_start_type
|
||||
if start_type is None:
|
||||
start_type = self.settings.default_start_type
|
||||
|
||||
flight = Flight(
|
||||
package,
|
||||
self.coalition.country_name,
|
||||
self,
|
||||
size,
|
||||
FlightType.FERRY,
|
||||
start_type,
|
||||
divert=None,
|
||||
)
|
||||
package.add_flight(flight)
|
||||
builder.populate_flight_plan(flight)
|
||||
|
||||
@classmethod
|
||||
def create_from(
|
||||
cls,
|
||||
|
||||
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
@ -56,6 +57,7 @@ if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
from game.squadrons.squadron import Squadron
|
||||
from ..coalition import Coalition
|
||||
from ..transfers import PendingTransfers
|
||||
|
||||
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
|
||||
@ -281,7 +283,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
position = None # type: Point
|
||||
name = None # type: str
|
||||
|
||||
captured = False
|
||||
has_frontline = True
|
||||
|
||||
alt = 0
|
||||
@ -295,6 +296,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
name: str,
|
||||
position: Point,
|
||||
at: db.StartingPosition,
|
||||
starts_blue: bool,
|
||||
has_frontline: bool = True,
|
||||
cptype: ControlPointType = ControlPointType.AIRBASE,
|
||||
) -> None:
|
||||
@ -303,11 +305,12 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.id = cp_id
|
||||
self.full_name = name
|
||||
self.at = at
|
||||
self.starts_blue = starts_blue
|
||||
self.connected_objectives: List[TheaterGroundObject[Any]] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
self.helipads: List[PointWithHeading] = []
|
||||
|
||||
self.captured = False
|
||||
self._coalition: Optional[Coalition] = None
|
||||
self.captured_invert = False
|
||||
# TODO: Should be Airbase specific.
|
||||
self.has_frontline = has_frontline
|
||||
@ -324,15 +327,33 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
self.target_position: Optional[Point] = None
|
||||
|
||||
self.squadrons: list[Squadron] = []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__}: {self.name}>"
|
||||
|
||||
@property
|
||||
def coalition(self) -> Coalition:
|
||||
if self._coalition is None:
|
||||
raise RuntimeError("ControlPoint not fully initialized: coalition not set")
|
||||
return self._coalition
|
||||
|
||||
def finish_init(self, game: Game) -> None:
|
||||
assert self._coalition is None
|
||||
self._coalition = game.coalition_for(self.starts_blue)
|
||||
|
||||
@property
|
||||
def captured(self) -> bool:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> List[TheaterGroundObject[Any]]:
|
||||
return list(self.connected_objectives)
|
||||
|
||||
@property
|
||||
def squadrons(self) -> Iterator[Squadron]:
|
||||
for squadron in self.coalition.air_wing.iter_squadrons():
|
||||
if squadron.location == self:
|
||||
yield squadron
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def heading(self) -> Heading:
|
||||
@ -564,36 +585,82 @@ class ControlPoint(MissionTarget, ABC):
|
||||
value = airframe.price * count
|
||||
game.adjust_budget(value, player=not self.captured)
|
||||
game.message(
|
||||
f"No valid retreat destination in range of {self.name} for {airframe}"
|
||||
f"No valid retreat destination in range of {self.name} for {airframe} "
|
||||
f"{count} aircraft have been captured and sold for ${value}M."
|
||||
)
|
||||
|
||||
def aircraft_retreat_destination(
|
||||
self, game: Game, airframe: AircraftType
|
||||
self, squadron: Squadron
|
||||
) -> Optional[ControlPoint]:
|
||||
closest = ObjectiveDistanceCache.get_closest_airfields(self)
|
||||
# TODO: Should be airframe dependent.
|
||||
max_retreat_distance = nautical_miles(200)
|
||||
max_retreat_distance = squadron.aircraft.max_mission_range
|
||||
# Skip the first airbase because that's the airbase we're retreating
|
||||
# from.
|
||||
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
|
||||
not_preferred: Optional[ControlPoint] = None
|
||||
overfull: list[ControlPoint] = []
|
||||
for airbase in airfields:
|
||||
if not airbase.can_operate(airframe):
|
||||
continue
|
||||
if airbase.captured != self.captured:
|
||||
continue
|
||||
if airbase.unclaimed_parking(game) > 0:
|
||||
return airbase
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _retreat_squadron(squadron: Squadron) -> None:
|
||||
logging.error("Air unit retreat not currently implemented")
|
||||
if airbase.unclaimed_parking() < squadron.owned_aircraft:
|
||||
if airbase.can_operate(squadron.aircraft):
|
||||
overfull.append(airbase)
|
||||
continue
|
||||
|
||||
if squadron.operates_from(airbase):
|
||||
# Has room, is a preferred base type for this squadron, and is the
|
||||
# closest choice. No need to keep looking.
|
||||
return airbase
|
||||
|
||||
if not_preferred is None and airbase.can_operate(squadron.aircraft):
|
||||
# Has room and is capable of operating from this base, but it isn't
|
||||
# preferred. Remember this option and use it if we can't find a
|
||||
# preferred base type with room.
|
||||
not_preferred = airbase
|
||||
if not_preferred is not None:
|
||||
# It's not our best choice but the other choices don't have room for the
|
||||
# squadron and would lead to aircraft being captured.
|
||||
return not_preferred
|
||||
|
||||
# No base was available with enough room. Find whichever base has the most room
|
||||
# available so we lose as little as possible. The overfull list is already
|
||||
# sorted by distance, and filtered for appropriate destinations.
|
||||
base_for_fewest_losses: Optional[ControlPoint] = None
|
||||
loss_count = math.inf
|
||||
for airbase in overfull:
|
||||
overflow = -(
|
||||
airbase.unclaimed_parking()
|
||||
- squadron.owned_aircraft
|
||||
- squadron.pending_deliveries
|
||||
)
|
||||
if overflow < loss_count:
|
||||
loss_count = overflow
|
||||
base_for_fewest_losses = airbase
|
||||
return base_for_fewest_losses
|
||||
|
||||
def _retreat_squadron(self, game: Game, squadron: Squadron) -> None:
|
||||
destination = self.aircraft_retreat_destination(squadron)
|
||||
if destination is None:
|
||||
squadron.refund_orders()
|
||||
self.capture_aircraft(game, squadron.aircraft, squadron.owned_aircraft)
|
||||
return
|
||||
logging.debug(f"{squadron} retreating to {destination} from {self}")
|
||||
squadron.relocate_to(destination)
|
||||
squadron.cancel_overflow_orders()
|
||||
overflow = -destination.unclaimed_parking()
|
||||
if overflow > 0:
|
||||
logging.debug(
|
||||
f"Not enough room for {squadron} at {destination}. Capturing "
|
||||
f"{overflow} aircraft."
|
||||
)
|
||||
self.capture_aircraft(game, squadron.aircraft, overflow)
|
||||
squadron.owned_aircraft -= overflow
|
||||
|
||||
def retreat_air_units(self, game: Game) -> None:
|
||||
# TODO: Capture in order of price to retain maximum value?
|
||||
for squadron in self.squadrons:
|
||||
self._retreat_squadron(squadron)
|
||||
self._retreat_squadron(game, squadron)
|
||||
|
||||
def depopulate_uncapturable_tgos(self) -> None:
|
||||
for tgo in self.connected_objectives:
|
||||
@ -602,27 +669,25 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
coalition = game.coalition_for(for_player)
|
||||
self.ground_unit_orders.refund_all(coalition)
|
||||
for squadron in self.squadrons:
|
||||
squadron.refund_orders()
|
||||
new_coalition = game.coalition_for(for_player)
|
||||
self.ground_unit_orders.refund_all(self.coalition)
|
||||
self.retreat_ground_units(game)
|
||||
self.retreat_air_units(game)
|
||||
self.depopulate_uncapturable_tgos()
|
||||
|
||||
if for_player:
|
||||
self.captured = True
|
||||
else:
|
||||
self.captured = False
|
||||
|
||||
self._coalition = new_coalition
|
||||
self.base.set_strength_to_minimum()
|
||||
|
||||
@property
|
||||
def required_aircraft_start_type(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
...
|
||||
|
||||
def unclaimed_parking(self, game: Game) -> int:
|
||||
return self.total_aircraft_parking - self.allocated_aircraft(game).total
|
||||
def unclaimed_parking(self) -> int:
|
||||
return self.total_aircraft_parking - self.allocated_aircraft().total
|
||||
|
||||
@abstractmethod
|
||||
def active_runway(
|
||||
@ -630,6 +695,10 @@ class ControlPoint(MissionTarget, ABC):
|
||||
) -> RunwayData:
|
||||
...
|
||||
|
||||
@property
|
||||
def airdrome_id_for_landing(self) -> Optional[int]:
|
||||
return None
|
||||
|
||||
@property
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from []
|
||||
@ -651,8 +720,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
def process_turn(self, game: Game) -> None:
|
||||
self.ground_unit_orders.process(game)
|
||||
for squadron in self.squadrons:
|
||||
squadron.deliver_orders()
|
||||
|
||||
runway_status = self.runway_status
|
||||
if runway_status is not None:
|
||||
@ -674,16 +741,22 @@ class ControlPoint(MissionTarget, ABC):
|
||||
u.position.x = u.position.x + delta.x
|
||||
u.position.y = u.position.y + delta.y
|
||||
|
||||
def allocated_aircraft(self, _game: Game) -> AircraftAllocations:
|
||||
def allocated_aircraft(self) -> AircraftAllocations:
|
||||
present: dict[AircraftType, int] = defaultdict(int)
|
||||
on_order: dict[AircraftType, int] = defaultdict(int)
|
||||
transferring: dict[AircraftType, int] = defaultdict(int)
|
||||
for squadron in self.squadrons:
|
||||
present[squadron.aircraft] += squadron.owned_aircraft
|
||||
# TODO: Only if this is the squadron destination, not location.
|
||||
on_order[squadron.aircraft] += squadron.pending_deliveries
|
||||
if squadron.destination is None:
|
||||
on_order[squadron.aircraft] += squadron.pending_deliveries
|
||||
else:
|
||||
transferring[squadron.aircraft] -= squadron.owned_aircraft
|
||||
for squadron in self.coalition.air_wing.iter_squadrons():
|
||||
if squadron.destination == self:
|
||||
on_order[squadron.aircraft] += squadron.pending_deliveries
|
||||
transferring[squadron.aircraft] += squadron.owned_aircraft
|
||||
|
||||
# TODO: Implement squadron transfers.
|
||||
return AircraftAllocations(present, on_order, transferring={})
|
||||
return AircraftAllocations(present, on_order, transferring)
|
||||
|
||||
def allocated_ground_units(
|
||||
self, transfers: PendingTransfers
|
||||
@ -795,13 +868,14 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
def __init__(self, airport: Airport, has_frontline: bool = True) -> None:
|
||||
def __init__(self, airport: Airport, starts_blue: bool) -> None:
|
||||
super().__init__(
|
||||
airport.id,
|
||||
airport.name,
|
||||
airport.position,
|
||||
airport,
|
||||
has_frontline,
|
||||
starts_blue,
|
||||
has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE,
|
||||
)
|
||||
self.airport = airport
|
||||
@ -863,6 +937,10 @@ class Airfield(ControlPoint):
|
||||
assigner = RunwayAssigner(conditions)
|
||||
return assigner.get_preferred_runway(self.airport)
|
||||
|
||||
@property
|
||||
def airdrome_id_for_landing(self) -> Optional[int]:
|
||||
return self.airport.id
|
||||
|
||||
@property
|
||||
def parking_slots(self) -> Iterator[ParkingSlot]:
|
||||
yield from self.airport.parking_slots
|
||||
@ -970,12 +1048,13 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
|
||||
class Carrier(NavalControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool):
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
starts_blue,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
)
|
||||
@ -1010,12 +1089,13 @@ class Carrier(NavalControlPoint):
|
||||
|
||||
|
||||
class Lha(NavalControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool):
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
starts_blue,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.LHA_GROUP,
|
||||
)
|
||||
@ -1043,12 +1123,13 @@ class OffMapSpawn(ControlPoint):
|
||||
def runway_is_operational(self) -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, cp_id: int, name: str, position: Point):
|
||||
def __init__(self, cp_id: int, name: str, position: Point, starts_blue: bool):
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
position,
|
||||
at=position,
|
||||
position,
|
||||
starts_blue,
|
||||
has_frontline=False,
|
||||
cptype=ControlPointType.OFF_MAP,
|
||||
)
|
||||
@ -1066,6 +1147,10 @@ class OffMapSpawn(ControlPoint):
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def required_aircraft_start_type(self) -> Optional[str]:
|
||||
return "In Flight"
|
||||
|
||||
@property
|
||||
def heading(self) -> Heading:
|
||||
return Heading.from_degrees(0)
|
||||
@ -1096,12 +1181,13 @@ class OffMapSpawn(ControlPoint):
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool):
|
||||
super().__init__(
|
||||
cp_id,
|
||||
name,
|
||||
at,
|
||||
at,
|
||||
starts_blue,
|
||||
has_frontline=True,
|
||||
cptype=ControlPointType.FOB,
|
||||
)
|
||||
|
||||
@ -8,13 +8,11 @@ from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, List, Set
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater import Carrier, Lha, PointWithHeading
|
||||
from game.theater import PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
@ -30,7 +28,6 @@ from game.theater.theatergroundobject import (
|
||||
)
|
||||
from game.utils import Heading
|
||||
from game.version import VERSION
|
||||
from gen.naming import namegen
|
||||
from gen.coastal.coastal_group_generator import generate_coastal_group
|
||||
from gen.defenses.armor_group_generator import generate_armor_group
|
||||
from gen.fleet.ship_group_generator import (
|
||||
@ -39,6 +36,7 @@ from gen.fleet.ship_group_generator import (
|
||||
generate_ship_group,
|
||||
)
|
||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.naming import namegen
|
||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||
from gen.sam.ewr_group_generator import generate_ewr_group
|
||||
from gen.sam.sam_group_generator import generate_anti_air_group
|
||||
@ -61,7 +59,6 @@ class GeneratorSettings:
|
||||
start_date: datetime
|
||||
player_budget: int
|
||||
enemy_budget: int
|
||||
midgame: bool
|
||||
inverted: bool
|
||||
no_carrier: bool
|
||||
no_lha: bool
|
||||
@ -91,13 +88,12 @@ class GameGenerator:
|
||||
generator_settings: GeneratorSettings,
|
||||
mod_settings: ModSettings,
|
||||
) -> None:
|
||||
self.player = player
|
||||
self.enemy = enemy
|
||||
self.player = player.apply_mod_settings(mod_settings)
|
||||
self.enemy = enemy.apply_mod_settings(mod_settings)
|
||||
self.theater = theater
|
||||
self.air_wing_config = air_wing_config
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
self.mod_settings = mod_settings
|
||||
|
||||
def generate(self) -> Game:
|
||||
with logged_duration("TGO population"):
|
||||
@ -105,8 +101,8 @@ class GameGenerator:
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
game = Game(
|
||||
player_faction=self.player.apply_mod_settings(self.mod_settings),
|
||||
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings),
|
||||
player_faction=self.player,
|
||||
enemy_faction=self.enemy,
|
||||
theater=self.theater,
|
||||
air_wing_config=self.air_wing_config,
|
||||
start_date=self.generator_settings.start_date,
|
||||
@ -119,37 +115,31 @@ class GameGenerator:
|
||||
game.settings.version = VERSION
|
||||
return game
|
||||
|
||||
def should_remove_carrier(self, player: bool) -> bool:
|
||||
faction = self.player if player else self.enemy
|
||||
return self.generator_settings.no_carrier or not faction.carrier_names
|
||||
|
||||
def should_remove_lha(self, player: bool) -> bool:
|
||||
faction = self.player if player else self.enemy
|
||||
return self.generator_settings.no_lha or not faction.helicopter_carrier_names
|
||||
|
||||
def prepare_theater(self) -> None:
|
||||
to_remove: List[ControlPoint] = []
|
||||
# Auto-capture half the bases if midgame.
|
||||
if self.generator_settings.midgame:
|
||||
control_points = self.theater.controlpoints
|
||||
for control_point in control_points[: len(control_points) // 2]:
|
||||
control_point.captured = True
|
||||
|
||||
# Remove carrier and lha, invert situation if needed
|
||||
for cp in self.theater.controlpoints:
|
||||
if isinstance(cp, Carrier) and self.generator_settings.no_carrier:
|
||||
to_remove.append(cp)
|
||||
elif isinstance(cp, Lha) and self.generator_settings.no_lha:
|
||||
to_remove.append(cp)
|
||||
|
||||
if self.generator_settings.inverted:
|
||||
cp.captured = cp.captured_invert
|
||||
cp.starts_blue = cp.captured_invert
|
||||
|
||||
if cp.is_carrier and self.should_remove_carrier(cp.starts_blue):
|
||||
to_remove.append(cp)
|
||||
elif cp.is_lha and self.should_remove_lha(cp.starts_blue):
|
||||
to_remove.append(cp)
|
||||
|
||||
# do remove
|
||||
for cp in to_remove:
|
||||
self.theater.controlpoints.remove(cp)
|
||||
|
||||
# TODO: Fix this. This captures all bases for blue.
|
||||
# reapply midgame inverted if needed
|
||||
if self.generator_settings.midgame and self.generator_settings.inverted:
|
||||
for i, cp in enumerate(reversed(self.theater.controlpoints)):
|
||||
if i > len(self.theater.controlpoints):
|
||||
break
|
||||
else:
|
||||
cp.captured = True
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(
|
||||
|
||||
@ -349,15 +349,17 @@ class AirliftPlanner:
|
||||
else:
|
||||
transfer = self.transfer
|
||||
|
||||
start_type = squadron.location.required_aircraft_start_type
|
||||
if start_type is None:
|
||||
start_type = self.game.settings.default_start_type
|
||||
|
||||
flight = Flight(
|
||||
self.package,
|
||||
self.game.country_for(squadron.player),
|
||||
squadron,
|
||||
flight_size,
|
||||
FlightType.TRANSPORT,
|
||||
self.game.settings.default_start_type,
|
||||
departure=squadron.location,
|
||||
arrival=squadron.location,
|
||||
start_type,
|
||||
divert=None,
|
||||
cargo=transfer,
|
||||
)
|
||||
@ -752,7 +754,7 @@ class PendingTransfers:
|
||||
)
|
||||
|
||||
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
||||
unclaimed_parking = control_point.unclaimed_parking(self.game)
|
||||
unclaimed_parking = control_point.unclaimed_parking()
|
||||
# Buy a maximum of unclaimed_parking only to prevent that aircraft procurement
|
||||
# take place at another base
|
||||
gap = min(
|
||||
|
||||
@ -57,6 +57,7 @@ from dcs.task import (
|
||||
Transport,
|
||||
WeaponType,
|
||||
TargetType,
|
||||
Nothing,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
@ -92,7 +93,7 @@ from gen.flights.flight import (
|
||||
from gen.lasercoderegistry import LaserCodeRegistry
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
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 .callsigns import callsign_for_support_unit
|
||||
from .flights.flightplan import (
|
||||
@ -437,7 +438,7 @@ class AircraftConflictGenerator:
|
||||
if isinstance(flight.flight_plan, RefuelingFlightPlan):
|
||||
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(
|
||||
TankerInfo(
|
||||
group_name=str(group.name),
|
||||
@ -656,41 +657,36 @@ class AircraftConflictGenerator:
|
||||
|
||||
for squadron in control_point.squadrons:
|
||||
try:
|
||||
self._spawn_unused_at(control_point, country, faction, squadron)
|
||||
self._spawn_unused_for(squadron, country, faction)
|
||||
except NoParkingSlotError:
|
||||
# If we run out of parking, stop spawning aircraft.
|
||||
return
|
||||
|
||||
def _spawn_unused_at(
|
||||
self,
|
||||
control_point: Airfield,
|
||||
country: Country,
|
||||
faction: Faction,
|
||||
squadron: Squadron,
|
||||
def _spawn_unused_for(
|
||||
self, squadron: Squadron, country: Country, faction: Faction
|
||||
) -> None:
|
||||
assert isinstance(squadron.location, Airfield)
|
||||
for _ in range(squadron.untasked_aircraft):
|
||||
# Creating a flight even those this isn't a fragged mission lets us
|
||||
# reuse the existing debriefing code.
|
||||
# TODO: Special flight type?
|
||||
flight = Flight(
|
||||
Package(control_point),
|
||||
Package(squadron.location),
|
||||
faction.country,
|
||||
squadron,
|
||||
1,
|
||||
FlightType.BARCAP,
|
||||
"Cold",
|
||||
departure=control_point,
|
||||
arrival=control_point,
|
||||
divert=None,
|
||||
)
|
||||
|
||||
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,
|
||||
unit_type=squadron.aircraft.dcs_unit_type,
|
||||
count=1,
|
||||
start_type="Cold",
|
||||
airport=control_point.airport,
|
||||
airport=squadron.location.airport,
|
||||
)
|
||||
|
||||
self._setup_livery(flight, group)
|
||||
@ -1153,6 +1149,23 @@ class AircraftConflictGenerator:
|
||||
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:
|
||||
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||
self.configure_behavior(flight, group)
|
||||
@ -1197,6 +1210,8 @@ class AircraftConflictGenerator:
|
||||
self.configure_oca_strike(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.TRANSPORT:
|
||||
self.configure_transport(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.FERRY:
|
||||
self.configure_ferry(group, package, flight, dynamic_runways)
|
||||
else:
|
||||
self.configure_unknown_task(group, flight)
|
||||
|
||||
@ -1783,6 +1798,8 @@ class LandingPointBuilder(PydcsWaypointBuilder):
|
||||
waypoint = super().build()
|
||||
waypoint.type = "Land"
|
||||
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
|
||||
|
||||
|
||||
@ -1792,6 +1809,8 @@ class CargoStopBuilder(PydcsWaypointBuilder):
|
||||
waypoint.type = "LandingReFuAr"
|
||||
waypoint.action = PointAction.LandingReFuAr
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ from .conflictgen import Conflict
|
||||
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from .naming import namegen
|
||||
from .radios import RadioRegistry
|
||||
from .tacan import TacanBand, TacanRegistry
|
||||
from .tacan import TacanBand, TacanRegistry, TacanUsage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -89,7 +89,9 @@ class AirSupportConflictGenerator:
|
||||
# TODO: Make loiter altitude a property of the unit type.
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
||||
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(
|
||||
self.conflict.red_cp.position.heading_between_point(
|
||||
self.conflict.blue_cp.position
|
||||
|
||||
10
gen/armor.py
10
gen/armor.py
@ -143,9 +143,17 @@ class GroundConflictGenerator:
|
||||
# Add JTAC
|
||||
if self.game.blue.faction.has_jtac:
|
||||
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()
|
||||
|
||||
# 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
|
||||
if utype is None:
|
||||
utype = AircraftType.named("MQ-9 Reaper")
|
||||
|
||||
@ -183,6 +183,7 @@ class Package:
|
||||
FlightType.TARCAP,
|
||||
FlightType.BARCAP,
|
||||
FlightType.AEWC,
|
||||
FlightType.FERRY,
|
||||
FlightType.REFUELING,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
|
||||
@ -70,6 +70,7 @@ class FlightType(Enum):
|
||||
TRANSPORT = "Transport"
|
||||
SEAD_ESCORT = "SEAD Escort"
|
||||
REFUELING = "Refueling"
|
||||
FERRY = "Ferry"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
@ -159,6 +160,7 @@ class FlightWaypoint:
|
||||
x: float,
|
||||
y: float,
|
||||
alt: Distance = meters(0),
|
||||
control_point: Optional[ControlPoint] = None,
|
||||
) -> None:
|
||||
"""Creates a flight waypoint.
|
||||
|
||||
@ -168,11 +170,14 @@ class FlightWaypoint:
|
||||
y: Y coordinate of the waypoint.
|
||||
alt: Altitude of the waypoint. By default this is MSL, but it can be
|
||||
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.x = x
|
||||
self.y = y
|
||||
self.alt = alt
|
||||
self.control_point = control_point
|
||||
self.alt_type = "BARO"
|
||||
self.name = ""
|
||||
# TODO: Merge with pretty_name.
|
||||
@ -282,8 +287,6 @@ class Flight:
|
||||
count: int,
|
||||
flight_type: FlightType,
|
||||
start_type: str,
|
||||
departure: ControlPoint,
|
||||
arrival: ControlPoint,
|
||||
divert: Optional[ControlPoint],
|
||||
custom_name: Optional[str] = None,
|
||||
cargo: Optional[TransferOrder] = None,
|
||||
@ -297,8 +300,8 @@ class Flight:
|
||||
self.roster = FlightRoster(self.squadron, initial_size=count)
|
||||
else:
|
||||
self.roster = roster
|
||||
self.departure = departure
|
||||
self.arrival = arrival
|
||||
self.departure = self.squadron.location
|
||||
self.arrival = self.squadron.arrival
|
||||
self.divert = divert
|
||||
self.flight_type = flight_type
|
||||
# TODO: Replace with FlightPlan.
|
||||
|
||||
@ -37,10 +37,8 @@ from game.theater.theatergroundobject import (
|
||||
NavalGroundObject,
|
||||
BuildingGroundObject,
|
||||
)
|
||||
|
||||
from game.threatzones import ThreatZones
|
||||
from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots
|
||||
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
@ -836,6 +834,39 @@ class AirliftFlightPlan(FlightPlan):
|
||||
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)
|
||||
class CustomFlightPlan(FlightPlan):
|
||||
custom_waypoints: List[FlightWaypoint]
|
||||
@ -958,6 +989,8 @@ class FlightPlanBuilder:
|
||||
return self.generate_transport(flight)
|
||||
elif task == FlightType.REFUELING:
|
||||
return self.generate_refueling_racetrack(flight)
|
||||
elif task == FlightType.FERRY:
|
||||
return self.generate_ferry(flight)
|
||||
raise PlanningError(f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
@ -1244,6 +1277,42 @@ class FlightPlanBuilder:
|
||||
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(
|
||||
self, location: MissionTarget, barcap: bool
|
||||
) -> Tuple[Point, Point]:
|
||||
|
||||
@ -110,7 +110,11 @@ class WaypointBuilder:
|
||||
waypoint.pretty_name = "Exit theater"
|
||||
else:
|
||||
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.alt_type = "RADIO"
|
||||
@ -139,7 +143,11 @@ class WaypointBuilder:
|
||||
altitude_type = "RADIO"
|
||||
|
||||
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.name = "DIVERT"
|
||||
@ -488,6 +496,7 @@ class WaypointBuilder:
|
||||
control_point.position.x,
|
||||
control_point.position.y,
|
||||
meters(0),
|
||||
control_point=control_point,
|
||||
)
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.name = "DROP OFF"
|
||||
|
||||
@ -59,7 +59,7 @@ from game.unitmap import UnitMap
|
||||
from game.utils import Heading, feet, knots, mps
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .runways import RunwayData
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -378,7 +378,9 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO
|
||||
for unit in group.units[1:]:
|
||||
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()
|
||||
icls = next(self.icls_alloc)
|
||||
|
||||
|
||||
569
gen/naming.py
569
gen/naming.py
@ -43,209 +43,406 @@ ALPHA_MILITARY = [
|
||||
]
|
||||
|
||||
ANIMALS: tuple[str, ...] = (
|
||||
"SHARK",
|
||||
"TORTOISE",
|
||||
"BAT",
|
||||
"PANGOLIN",
|
||||
"AARDVARK",
|
||||
"AARDWOLF",
|
||||
"MONKEY",
|
||||
"BUFFALO",
|
||||
"DOG",
|
||||
"BOBCAT",
|
||||
"LYNX",
|
||||
"PANTHER",
|
||||
"TIGER",
|
||||
"LION",
|
||||
"OWL",
|
||||
"BUTTERFLY",
|
||||
"BISON",
|
||||
"DUCK",
|
||||
"COBRA",
|
||||
"MAMBA",
|
||||
"DOLPHIN",
|
||||
"PHEASANT",
|
||||
"ADDER",
|
||||
"ALBACORE",
|
||||
"ALBATROSS",
|
||||
"ALLIGATOR",
|
||||
"ALPACA",
|
||||
"ANACONDA",
|
||||
"ANOLE",
|
||||
"ANTEATER",
|
||||
"ANTELOPE",
|
||||
"ANTLION",
|
||||
"ARAPAIMA",
|
||||
"ARCHERFISH",
|
||||
"ARGALI",
|
||||
"ARMADILLO",
|
||||
"RACOON",
|
||||
"ZEBRA",
|
||||
"ASP",
|
||||
"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",
|
||||
"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",
|
||||
"CRANE",
|
||||
"CRICKET",
|
||||
"CROCODILE",
|
||||
"CROW",
|
||||
"CUTTLEFISH",
|
||||
"DACHSHUND",
|
||||
"DEER",
|
||||
"DINGO",
|
||||
"DIREWOLF",
|
||||
"DODO",
|
||||
"FLAMINGO",
|
||||
"FERRET",
|
||||
"FALCON",
|
||||
"BULLDOG",
|
||||
"DOG",
|
||||
"DOLPHIN",
|
||||
"DONKEY",
|
||||
"IGUANA",
|
||||
"TAMARIN",
|
||||
"HARRIER",
|
||||
"GRIZZLY",
|
||||
"GREYHOUND",
|
||||
"GRASSHOPPER",
|
||||
"JAGUAR",
|
||||
"LADYBUG",
|
||||
"KOMODO",
|
||||
"DOVE",
|
||||
"DRACO",
|
||||
"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",
|
||||
"LLAMA",
|
||||
"LOACH",
|
||||
"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",
|
||||
"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",
|
||||
"SNAIL",
|
||||
"HEDGEHOG",
|
||||
"PIGLET",
|
||||
"FENNEC",
|
||||
"BADGER",
|
||||
"ALPACA",
|
||||
"DINGO",
|
||||
"COLT",
|
||||
"SKUNK",
|
||||
"BUNNY",
|
||||
"IMPALA",
|
||||
"GUANACO",
|
||||
"CAPYBARA",
|
||||
"ELK",
|
||||
"MINK",
|
||||
"PRONGHORN",
|
||||
"CROW",
|
||||
"BUMBLEBEE",
|
||||
"FAWN",
|
||||
"OTTER",
|
||||
"SNAKE",
|
||||
"SNAPPER",
|
||||
"SNOOK",
|
||||
"SPARROW",
|
||||
"SPIDER",
|
||||
"SPRINGBOK",
|
||||
"SQUID",
|
||||
"SQUIRREL",
|
||||
"STAGHORN",
|
||||
"STARFISH",
|
||||
"STINGRAY",
|
||||
"STINKBUG",
|
||||
"STOUT",
|
||||
"STURGEON",
|
||||
"SUGARGLIDER",
|
||||
"SUNBEAR",
|
||||
"SWALLOW",
|
||||
"SWAN",
|
||||
"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",
|
||||
"JERBOA",
|
||||
"KITTEN",
|
||||
"ARGALI",
|
||||
"OX",
|
||||
"MARE",
|
||||
"FINCH",
|
||||
"BASILISK",
|
||||
"GOPHER",
|
||||
"HAMSTER",
|
||||
"CANARY",
|
||||
"WEASEL",
|
||||
"WEEVIL",
|
||||
"WEKA",
|
||||
"WHALE",
|
||||
"WILDCAT",
|
||||
"WILDEBEEST",
|
||||
"WOLF",
|
||||
"WOLFHOUND",
|
||||
"WOLVERINE",
|
||||
"WOMBAT",
|
||||
"WOODCHUCK",
|
||||
"ANACONDA",
|
||||
"WOODPECKER",
|
||||
"WORM",
|
||||
"WRASSE",
|
||||
"WYVERN",
|
||||
"YAK",
|
||||
"ZEBRA",
|
||||
"ZEBU",
|
||||
)
|
||||
|
||||
|
||||
|
||||
45
gen/tacan.py
45
gen/tacan.py
@ -4,13 +4,37 @@ from enum import Enum
|
||||
from typing import Dict, Iterator, Set
|
||||
|
||||
|
||||
class TacanUsage(Enum):
|
||||
TransmitReceive = "transmit receive"
|
||||
AirToAir = "air to air"
|
||||
|
||||
|
||||
class TacanBand(Enum):
|
||||
X = "X"
|
||||
Y = "Y"
|
||||
|
||||
def range(self) -> Iterator["TacanChannel"]:
|
||||
"""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)
|
||||
@ -41,25 +65,30 @@ class TacanRegistry:
|
||||
|
||||
def __init__(self) -> None:
|
||||
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:
|
||||
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.
|
||||
|
||||
Args:
|
||||
band: The TACAN band to allocate a channel for.
|
||||
intended_usage: What the caller intends to use the tacan channel for.
|
||||
|
||||
Returns:
|
||||
A TACAN channel in the given band.
|
||||
|
||||
Raises:
|
||||
OutOfChannelsError: All channels compatible with the given radio are
|
||||
OutOfTacanChannelsError: All channels compatible with the given radio are
|
||||
already allocated.
|
||||
"""
|
||||
allocator = self.band_allocators[band]
|
||||
allocator = self.allocators[band][intended_usage]
|
||||
try:
|
||||
while (channel := next(allocator)) in self.allocated_channels:
|
||||
pass
|
||||
@ -67,7 +96,7 @@ class TacanRegistry:
|
||||
except StopIteration:
|
||||
raise OutOfTacanChannelsError(band)
|
||||
|
||||
def reserve(self, channel: TacanChannel) -> None:
|
||||
def mark_unavailable(self, channel: TacanChannel) -> None:
|
||||
"""Reserves the given channel.
|
||||
|
||||
Reserving a channel ensures that it will not be allocated in the future.
|
||||
@ -76,7 +105,7 @@ class TacanRegistry:
|
||||
channel: The channel to reserve.
|
||||
|
||||
Raises:
|
||||
ChannelInUseError: The given frequency is already in use.
|
||||
TacanChannelInUseError: The given channel is already in use.
|
||||
"""
|
||||
if channel in self.allocated_channels:
|
||||
raise TacanChannelInUseError(channel)
|
||||
|
||||
17
qt_ui/errorreporter.py
Normal file
17
qt_ui/errorreporter.py
Normal 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)
|
||||
@ -13,6 +13,7 @@ from PySide2.QtWidgets import QApplication, QSplashScreen
|
||||
from dcs.payloads import PayloadDirectories
|
||||
|
||||
from game import Game, VERSION, persistency
|
||||
from game.campaignloader.campaign import Campaign
|
||||
from game.data.weapons import WeaponGroup, Pylon, Weapon
|
||||
from game.db import FACTIONS
|
||||
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.QLiberationWindow import QLiberationWindow
|
||||
from game.campaignloader.campaign import Campaign
|
||||
from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET
|
||||
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
||||
QLiberationFirstStartWindow,
|
||||
@ -252,7 +252,6 @@ def create_game(
|
||||
start_date=start_date,
|
||||
player_budget=DEFAULT_BUDGET,
|
||||
enemy_budget=DEFAULT_BUDGET,
|
||||
midgame=False,
|
||||
inverted=inverted,
|
||||
no_carrier=False,
|
||||
no_lha=False,
|
||||
|
||||
42
qt_ui/uncaughtexceptionhandler.py
Normal file
42
qt_ui/uncaughtexceptionhandler.py
Normal 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.")
|
||||
@ -10,7 +10,6 @@ from PySide2.QtCore import (
|
||||
)
|
||||
from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QDialog,
|
||||
QListView,
|
||||
QVBoxLayout,
|
||||
@ -32,38 +31,7 @@ from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons import AirWing, Pilot, Squadron
|
||||
from game.theater import ControlPoint, ConflictTheater
|
||||
from gen.flights.flight import FlightType
|
||||
from qt_ui.models import AirWingModel, SquadronModel
|
||||
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):
|
||||
|
||||
@ -14,12 +14,14 @@ from PySide2.QtWidgets import (
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QWidget,
|
||||
QHBoxLayout,
|
||||
)
|
||||
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ConflictTheater
|
||||
from gen.flights.flight import Flight
|
||||
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
|
||||
|
||||
|
||||
@ -56,9 +58,16 @@ class SquadronDelegate(TwoColumnRowDelegate):
|
||||
class SquadronList(QListView):
|
||||
"""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__()
|
||||
self.ato_model = ato_model
|
||||
self.air_wing_model = air_wing_model
|
||||
self.theater = theater
|
||||
self.dialog: Optional[SquadronDialog] = None
|
||||
|
||||
self.setIconSize(QSize(91, 24))
|
||||
@ -76,7 +85,10 @@ class SquadronList(QListView):
|
||||
if not index.isValid():
|
||||
return
|
||||
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()
|
||||
|
||||
@ -138,30 +150,47 @@ class AircraftInventoryData:
|
||||
class AirInventoryView(QWidget):
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
|
||||
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()
|
||||
self.setLayout(layout)
|
||||
|
||||
self.only_unallocated_cb = QCheckBox("Unallocated Only?")
|
||||
self.only_unallocated_cb.toggled.connect(self.update_table)
|
||||
checkbox_row = QHBoxLayout()
|
||||
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()
|
||||
layout.addWidget(self.table)
|
||||
|
||||
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
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.clear()
|
||||
|
||||
inventory_rows = list(self.get_data(only_unallocated))
|
||||
inventory_rows = list(self.get_data())
|
||||
self.table.setRowCount(len(inventory_rows))
|
||||
headers = AircraftInventoryData.headers()
|
||||
self.table.setColumnCount(len(headers))
|
||||
@ -175,18 +204,19 @@ class AirInventoryView(QWidget):
|
||||
self.table.setSortingEnabled(True)
|
||||
|
||||
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:
|
||||
yield from AircraftInventoryData.from_flight(flight)
|
||||
|
||||
def iter_unallocated_aircraft(self) -> Iterator[AircraftInventoryData]:
|
||||
game = self.game_model.game
|
||||
for squadron in game.blue.air_wing.iter_squadrons():
|
||||
coalition = self.game_model.game.coalition_for(not self.enemy_info)
|
||||
for squadron in coalition.air_wing.iter_squadrons():
|
||||
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()
|
||||
if not only_unallocated:
|
||||
if not self.only_unallocated:
|
||||
yield from self.iter_allocated_aircraft()
|
||||
|
||||
|
||||
@ -194,7 +224,14 @@ class AirWingTabs(QTabWidget):
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
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")
|
||||
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ from qt_ui import liberation_install
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.uiconstants import URLS
|
||||
from qt_ui.uncaughtexceptionhandler import UncaughtExceptionHandler
|
||||
from qt_ui.widgets.QTopPanel import QTopPanel
|
||||
from qt_ui.widgets.ato import QAirTaskingOrderPanel
|
||||
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
|
||||
@ -42,7 +43,9 @@ from qt_ui.windows.logs.QLogsWindow import QLogsWindow
|
||||
|
||||
class QLiberationWindow(QMainWindow):
|
||||
def __init__(self, game: Optional[Game]) -> None:
|
||||
super(QLiberationWindow, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
self._uncaught_exception_handler = UncaughtExceptionHandler(self)
|
||||
|
||||
self.game = game
|
||||
self.game_model = GameModel(game)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Callable
|
||||
from typing import Callable, Iterator, Optional
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QItemSelectionModel,
|
||||
@ -16,12 +16,15 @@ from PySide2.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
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 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):
|
||||
@ -90,12 +93,58 @@ class AutoAssignedTaskControls(QVBoxLayout):
|
||||
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):
|
||||
"""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)
|
||||
self.ato_model = ato_model
|
||||
self.squadron_model = squadron_model
|
||||
self.theater = theater
|
||||
|
||||
self.setMinimumSize(1000, 440)
|
||||
self.setWindowTitle(str(squadron_model.squadron))
|
||||
@ -117,6 +166,15 @@ class SquadronDialog(QDialog):
|
||||
columns.addWidget(self.pilot_list)
|
||||
|
||||
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()
|
||||
layout.addLayout(button_panel)
|
||||
|
||||
@ -132,6 +190,19 @@ class SquadronDialog(QDialog):
|
||||
self.toggle_leave_button.clicked.connect(self.toggle_leave)
|
||||
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(
|
||||
self, button: QPushButton, index: QModelIndex
|
||||
) -> bool:
|
||||
|
||||
@ -190,7 +190,7 @@ class QBaseMenu2(QDialog):
|
||||
self.repair_button.setDisabled(True)
|
||||
|
||||
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
|
||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||
deployable_unit_info = ""
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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.windows.basemenu.DepartingConvoysMenu import DepartingConvoysMenu
|
||||
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
|
||||
@ -13,7 +13,7 @@ class QBaseMenuTabs(QTabWidget):
|
||||
super(QBaseMenuTabs, self).__init__()
|
||||
|
||||
if not cp.captured:
|
||||
self.intel = QIntelInfo(cp, game_model.game)
|
||||
self.intel = QIntelInfo(cp)
|
||||
self.addTab(self.intel, "Intel")
|
||||
|
||||
self.departing_convoys = DepartingConvoysMenu(cp, game_model)
|
||||
|
||||
@ -273,6 +273,8 @@ class UnitTransactionFrame(QFrame, Generic[TransactionItemType]):
|
||||
else:
|
||||
return "Unit can not be sold."
|
||||
|
||||
def info(self, unit_type: UnitType) -> None:
|
||||
self.info_window = QUnitInfoWindow(self.game_model.game, unit_type)
|
||||
def info(self, item: TransactionItemType) -> None:
|
||||
self.info_window = QUnitInfoWindow(
|
||||
self.game_model.game, self.purchase_adapter.unit_type_of(item)
|
||||
)
|
||||
self.info_window.show()
|
||||
|
||||
@ -21,12 +21,7 @@ from game.purchaseadapter import AircraftPurchaseAdapter
|
||||
|
||||
class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]):
|
||||
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
||||
super().__init__(
|
||||
game_model,
|
||||
AircraftPurchaseAdapter(
|
||||
cp, game_model.game.coalition_for(cp.captured), game_model.game
|
||||
),
|
||||
)
|
||||
super().__init__(game_model, AircraftPurchaseAdapter(cp))
|
||||
self.cp = cp
|
||||
self.game_model = game_model
|
||||
self.purchase_groups = {}
|
||||
@ -98,7 +93,7 @@ class QHangarStatus(QHBoxLayout):
|
||||
self.setAlignment(Qt.AlignLeft)
|
||||
|
||||
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
|
||||
|
||||
components = [f"{next_turn.total_present} present"]
|
||||
|
||||
@ -11,22 +11,20 @@ from PySide2.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class QIntelInfo(QFrame):
|
||||
def __init__(self, cp: ControlPoint, game: Game):
|
||||
def __init__(self, cp: ControlPoint):
|
||||
super(QIntelInfo, self).__init__()
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
|
||||
layout = QVBoxLayout()
|
||||
scroll_content = QWidget()
|
||||
intel_layout = QVBoxLayout()
|
||||
|
||||
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:
|
||||
task_type = unit_type.dcs_unit_type.task_default.name
|
||||
units_by_task[task_type][unit_type.name] += count
|
||||
|
||||
@ -77,7 +77,7 @@ class AircraftIntelLayout(IntelTableLayout):
|
||||
|
||||
total = 0
|
||||
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
|
||||
total += base_total
|
||||
if not base_total:
|
||||
|
||||
@ -85,7 +85,7 @@ class QFlightCreator(QDialog):
|
||||
squadron, initial_size=self.flight_size_spinner.value()
|
||||
)
|
||||
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)
|
||||
roster_layout = QHBoxLayout()
|
||||
layout.addLayout(roster_layout)
|
||||
@ -136,10 +136,6 @@ class QFlightCreator(QDialog):
|
||||
def set_custom_name_text(self, text: str):
|
||||
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]:
|
||||
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
|
||||
squadron: Optional[Squadron] = self.squadron_selector.currentData()
|
||||
@ -182,8 +178,6 @@ class QFlightCreator(QDialog):
|
||||
roster.max_size,
|
||||
task,
|
||||
self.start_type.currentText(),
|
||||
squadron.location,
|
||||
squadron.location,
|
||||
divert,
|
||||
custom_name=self.custom_name_text,
|
||||
roster=roster,
|
||||
@ -198,7 +192,6 @@ class QFlightCreator(QDialog):
|
||||
self.squadron_selector.update_items(
|
||||
self.task_selector.currentData(), new_aircraft
|
||||
)
|
||||
self.departure.change_aircraft(new_aircraft)
|
||||
self.divert.change_aircraft(new_aircraft)
|
||||
|
||||
def on_departure_changed(self, departure: ControlPoint) -> None:
|
||||
@ -223,6 +216,7 @@ class QFlightCreator(QDialog):
|
||||
|
||||
def on_squadron_changed(self, index: int) -> None:
|
||||
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
|
||||
# up repopulating from the same squadron we'll get the same pilots back.
|
||||
self.roster_editor.replace(None)
|
||||
@ -230,7 +224,7 @@ class QFlightCreator(QDialog):
|
||||
self.roster_editor.replace(
|
||||
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:
|
||||
aircraft = self.aircraft_selector.currentData()
|
||||
|
||||
@ -176,6 +176,8 @@ class FlightRosterEditor(QVBoxLayout):
|
||||
def resize(self, new_size: int) -> None:
|
||||
if new_size > self.MAX_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]:
|
||||
controls.enable_and_reset()
|
||||
for controls in self.pilot_controls[new_size:]:
|
||||
|
||||
@ -65,6 +65,8 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
logging.info("======================")
|
||||
|
||||
campaign = self.field("selectedCampaign")
|
||||
if campaign is None:
|
||||
campaign = self.theater_page.campaignList.selected_campaign
|
||||
if campaign is None:
|
||||
campaign = self.campaigns[0]
|
||||
|
||||
@ -94,7 +96,6 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
enemy_budget=int(self.field("enemy_starting_money")),
|
||||
# QSlider forces integers, so we use 1 to 50 and divide by 10 to
|
||||
# give 0.1 to 5.0.
|
||||
midgame=False,
|
||||
inverted=self.field("invertMap"),
|
||||
no_carrier=self.field("no_carrier"),
|
||||
no_lha=self.field("no_lha"),
|
||||
@ -300,13 +301,13 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
text="Show incompatible campaigns"
|
||||
)
|
||||
show_incompatible_campaigns_checkbox.setChecked(False)
|
||||
campaignList = QCampaignList(
|
||||
self.campaignList = QCampaignList(
|
||||
campaigns, show_incompatible_campaigns_checkbox.isChecked()
|
||||
)
|
||||
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
|
||||
self.campaignMapDescription = QTextEdit("")
|
||||
@ -366,7 +367,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
template_perf = jinja_env.get_template(
|
||||
"campaign_performance_template_EN.j2"
|
||||
)
|
||||
campaign = campaignList.selected_campaign
|
||||
campaign = self.campaignList.selected_campaign
|
||||
self.setField("selectedCampaign", campaign)
|
||||
if campaign is None:
|
||||
self.campaignMapDescription.setText("No campaign selected")
|
||||
@ -379,11 +380,13 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
template_perf.render({"performance": campaign.performance})
|
||||
)
|
||||
|
||||
campaignList.selectionModel().setCurrentIndex(
|
||||
campaignList.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows
|
||||
self.campaignList.selectionModel().setCurrentIndex(
|
||||
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()
|
||||
|
||||
docsText = QtWidgets.QLabel(
|
||||
@ -410,7 +413,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
|
||||
layout = QtWidgets.QGridLayout()
|
||||
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(docsText, 6, 0, 1, 1)
|
||||
layout.addWidget(self.campaignMapDescription, 0, 1, 1, 1)
|
||||
|
||||
@ -22,7 +22,6 @@ from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.game import Game
|
||||
from game.infos.information import Information
|
||||
from game.settings import Settings, AutoAtoBehavior
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs
|
||||
@ -894,18 +893,6 @@ class QSettingsWindow(QDialog):
|
||||
def cheatMoney(self, amount):
|
||||
logging.info("CHEATING FOR AMOUNT : " + str(amount) + "M")
|
||||
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)
|
||||
|
||||
def applySettings(self):
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
altgraph==0.17
|
||||
appdirs==1.4.4
|
||||
attrs==21.2.0
|
||||
black==21.4b0
|
||||
certifi==2020.12.5
|
||||
cfgv==3.2.0
|
||||
@ -9,7 +10,9 @@ Faker==8.2.1
|
||||
filelock==3.0.12
|
||||
future==0.18.2
|
||||
identify==1.5.13
|
||||
iniconfig==1.1.1
|
||||
Jinja2==2.11.3
|
||||
macholib==1.14
|
||||
MarkupSafe==1.1.1
|
||||
mypy==0.812
|
||||
mypy-extensions==0.4.3
|
||||
@ -17,14 +20,17 @@ nodeenv==1.5.0
|
||||
packaging==20.9
|
||||
pathspec==0.8.1
|
||||
pefile==2019.4.18
|
||||
Pillow==8.2.0
|
||||
Pillow==8.3.2
|
||||
pluggy==0.13.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-hooks-contrib==2021.1
|
||||
pyparsing==2.4.7
|
||||
pyproj==3.0.1
|
||||
PySide2==5.15.2
|
||||
pytest==6.2.4
|
||||
python-dateutil==2.8.1
|
||||
pywin32-ctypes==0.2.0
|
||||
PyYAML==5.4.1
|
||||
|
||||
Binary file not shown.
@ -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>
|
||||
miz: battle_of_abu_dhabi.miz
|
||||
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
|
||||
|
||||
@ -4,7 +4,7 @@ theater: Caucasus
|
||||
authors: Colonel Panic
|
||||
description: <p>A medium sized theater with bases along the coast of the Black Sea.</p>
|
||||
miz: black_sea.miz
|
||||
performance: 2,
|
||||
performance: 2
|
||||
version: "9.0"
|
||||
squadrons:
|
||||
# Anapa-Vityazevo
|
||||
@ -148,4 +148,4 @@ squadrons:
|
||||
- primary: BAI
|
||||
secondary: air-to-ground
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
secondary: air-to-ground
|
||||
|
||||
@ -270,6 +270,41 @@ local unitPayloads = {
|
||||
[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",
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ if dcsLiberation then
|
||||
|
||||
-- specific options
|
||||
local smoke = false
|
||||
local fc3LaserCode = false
|
||||
|
||||
-- retrieve specific options values
|
||||
if dcsLiberation.plugins then
|
||||
@ -22,6 +23,9 @@ if dcsLiberation then
|
||||
env.info("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins.jtacautolase")
|
||||
smoke = dcsLiberation.plugins.jtacautolase.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
|
||||
|
||||
@ -29,6 +33,11 @@ if dcsLiberation then
|
||||
for _, jtac in pairs(dcsLiberation.JTACs) do
|
||||
env.info(string.format("DCSLiberation|JTACAutolase plugin - setting up %s",jtac.dcsUnit))
|
||||
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")
|
||||
JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle')
|
||||
end
|
||||
|
||||
@ -6,6 +6,11 @@
|
||||
"nameInUI": "Use smoke",
|
||||
"mnemonic": "smoke",
|
||||
"defaultValue": true
|
||||
},
|
||||
{
|
||||
"nameInUI": "Use FC3 laser code (1113)",
|
||||
"mnemonic": "fc3LaserCode",
|
||||
"defaultValue": false
|
||||
}
|
||||
],
|
||||
"scriptsWorkOrders": [
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/104th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/104th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/118th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/118th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/172nd FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/172nd FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/184th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/184th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/190th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/190th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/25th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/25th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/354th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/354th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/355th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/355th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/357th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/357th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/358th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/358th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/47th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/47th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/74th TFS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/74th TFS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog I/81st FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog I/81st FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog II/25th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog II/25th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog II/354th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog II/354th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog II/355th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog II/355th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog II/357th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog II/357th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog II/358th FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog II/358th FS.yaml
Normal 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
|
||||
|
||||
15
resources/squadrons/A-10C Warthog II/81st FS.yaml
Normal file
15
resources/squadrons/A-10C Warthog II/81st FS.yaml
Normal 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
|
||||
|
||||
9
resources/squadrons/E-2 Hawkeye/VAW-125.yaml
Normal file
9
resources/squadrons/E-2 Hawkeye/VAW-125.yaml
Normal 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
|
||||
9
resources/squadrons/E-3 Sentry/USAF 960th AACS.yaml
Normal file
9
resources/squadrons/E-3 Sentry/USAF 960th AACS.yaml
Normal 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
|
||||
14
resources/squadrons/Eagle/USAF 12th FS.yaml
Normal file
14
resources/squadrons/Eagle/USAF 12th FS.yaml
Normal 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
|
||||
|
||||
14
resources/squadrons/Eagle/USAF 390th FS.yaml
Normal file
14
resources/squadrons/Eagle/USAF 390th FS.yaml
Normal 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
|
||||
|
||||
14
resources/squadrons/Eagle/USAF 493rd FS.yaml
Normal file
14
resources/squadrons/Eagle/USAF 493rd FS.yaml
Normal 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
|
||||
|
||||
14
resources/squadrons/Eagle/USAF 58th FS.yaml
Normal file
14
resources/squadrons/Eagle/USAF 58th FS.yaml
Normal 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
|
||||
|
||||
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-11.yaml
Normal file
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-11.yaml
Normal 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
|
||||
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-111.yaml
Normal file
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-111.yaml
Normal 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
|
||||
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-21.yaml
Normal file
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-21.yaml
Normal 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
|
||||
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-211.yaml
Normal file
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-211.yaml
Normal 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
|
||||
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-33.yaml
Normal file
20
resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-33.yaml
Normal 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
|
||||
20
resources/squadrons/F-14B Tomcat/VF-101.yaml
Normal file
20
resources/squadrons/F-14B Tomcat/VF-101.yaml
Normal 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
|
||||
20
resources/squadrons/F-14B Tomcat/VF-102.yaml
Normal file
20
resources/squadrons/F-14B Tomcat/VF-102.yaml
Normal 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
|
||||
19
resources/squadrons/F-14B Tomcat/VF-142.yaml
Normal file
19
resources/squadrons/F-14B Tomcat/VF-142.yaml
Normal 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
|
||||
19
resources/squadrons/F-14B Tomcat/VF-211.yaml
Normal file
19
resources/squadrons/F-14B Tomcat/VF-211.yaml
Normal 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
|
||||
8
resources/squadrons/KC-130/VMGR-352.yaml
Normal file
8
resources/squadrons/KC-130/VMGR-352.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
name: VMGR-352
|
||||
nickname: Raiders
|
||||
country: USA
|
||||
role: Air-to-Air Refueling
|
||||
aircraft: KC-130
|
||||
mission_types:
|
||||
- Refueling
|
||||
8
resources/squadrons/KC-135/18th ARS.yaml
Normal file
8
resources/squadrons/KC-135/18th ARS.yaml
Normal 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
|
||||
@ -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
|
||||
8
resources/squadrons/KC-135MPRS/340th EARS.yaml
Normal file
8
resources/squadrons/KC-135MPRS/340th EARS.yaml
Normal 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
|
||||
14
resources/squadrons/Mig-29/Russia VVS 115th GvIAP.yaml
Normal file
14
resources/squadrons/Mig-29/Russia VVS 115th GvIAP.yaml
Normal 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
|
||||
14
resources/squadrons/Mig-29/Russia VVS 28th GvIAP.yaml
Normal file
14
resources/squadrons/Mig-29/Russia VVS 28th GvIAP.yaml
Normal 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
|
||||
14
resources/squadrons/Mig-29/Russia VVS 31st GvIAP.yaml
Normal file
14
resources/squadrons/Mig-29/Russia VVS 31st GvIAP.yaml
Normal 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
|
||||
14
resources/squadrons/Mig-29/Russia VVS 773rd IAP.yaml
Normal file
14
resources/squadrons/Mig-29/Russia VVS 773rd IAP.yaml
Normal 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
|
||||
14
resources/squadrons/Strike Eagle/335th FS.yaml
Normal file
14
resources/squadrons/Strike Eagle/335th FS.yaml
Normal 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
|
||||
15
resources/squadrons/Strike Eagle/492nd FS.yaml
Normal file
15
resources/squadrons/Strike Eagle/492nd FS.yaml
Normal 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
|
||||
|
||||
21
resources/squadrons/viper/USAF 132nd WG.yaml
Normal file
21
resources/squadrons/viper/USAF 132nd WG.yaml
Normal 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
|
||||
21
resources/squadrons/viper/USAF 13th FS.yaml
Normal file
21
resources/squadrons/viper/USAF 13th FS.yaml
Normal 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
Loading…
x
Reference in New Issue
Block a user