mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da4956df8 | ||
|
|
618159c1fa | ||
|
|
d8c662e7f8 | ||
|
|
12c41b57c9 | ||
|
|
85a27845bc | ||
|
|
e3f6347e16 | ||
|
|
fffe1b6e94 | ||
|
|
5a7a730e23 | ||
|
|
576f777320 | ||
|
|
87f7fe5307 | ||
|
|
e1434378a8 | ||
|
|
e03b0d99d8 | ||
|
|
e4eb3dec1b | ||
|
|
b365016496 | ||
|
|
c359b3f7fc | ||
|
|
302613069e | ||
|
|
5a22b62e3b |
22
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
22
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -31,7 +31,7 @@ body:
|
|||||||
If the bug was found in a development build, select "Development build"
|
If the bug was found in a development build, select "Development build"
|
||||||
and provide a link to the build in the field below.
|
and provide a link to the build in the field below.
|
||||||
options:
|
options:
|
||||||
- 14.0.0
|
- 7.1.0
|
||||||
- Development build
|
- Development build
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
@@ -49,19 +49,13 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Save game and other files (save game required, bugs without saves will be closed)
|
label: Save game and other files
|
||||||
description: >
|
description: >
|
||||||
Attach any files needed to reproduce the bug here. **A save game is
|
Attach any files needed to reproduce the bug here. **A save game is
|
||||||
required.** Even if it seems unnecessary to you, this is required.
|
required.** We typically cannot help without a save game (the
|
||||||
Repro steps that are obvious to you might not be obvious to anyone
|
`.liberation.zip` file found in `%USERPROFILE%/Saved
|
||||||
else, and it is impossible for us to know what default settings or mods
|
Games/DCS/Liberation/Saves`), so most bugs filed without saved games
|
||||||
may be impacting behavior without a save game. Bugs filed without a
|
will be closed without investigation.
|
||||||
save game are very often not reproducible, and those waste scarce
|
|
||||||
developer time. It is **much** easier for you to attach a save game
|
|
||||||
than it is for us to recreate your save state by guessing at what you
|
|
||||||
did. As such, bug reports that do not attach a saved game will be
|
|
||||||
closed without investigation. Attach the `.liberation.zip` file found
|
|
||||||
in `%USERPROFILE%/Saved Games/DCS/Liberation/Saves`.
|
|
||||||
|
|
||||||
|
|
||||||
Other useful files to include are:
|
Other useful files to include are:
|
||||||
@@ -82,10 +76,6 @@ body:
|
|||||||
investigating any issues with end-of-turn results processing.
|
investigating any issues with end-of-turn results processing.
|
||||||
|
|
||||||
|
|
||||||
If reporting an issue that occurred during or after flying the mission
|
|
||||||
in DCS, the DCS log file found in `%USERPROFILE%/Saved Games/DCS/Logs`.
|
|
||||||
|
|
||||||
|
|
||||||
You can attach files to the bug by dragging and dropping the file into
|
You can attach files to the bug by dragging and dropping the file into
|
||||||
this text box. GitHub will not allow uploads of all file types, so
|
this text box. GitHub will not allow uploads of all file types, so
|
||||||
attach a zip of the files if needed.
|
attach a zip of the files if needed.
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -39,7 +39,7 @@ body:
|
|||||||
If the bug was found in a development build, select "Development build"
|
If the bug was found in a development build, select "Development build"
|
||||||
and provide a link to the build in the field below.
|
and provide a link to the build in the field below.
|
||||||
options:
|
options:
|
||||||
- 14.0.0
|
- 7.1.0
|
||||||
- Development build
|
- Development build
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: "16"
|
node-version: "16"
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: "3.11.4"
|
python-version: "3.11"
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
||||||
- name: Install environment
|
- name: Install environment
|
||||||
|
|||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
Compress-Archive -Path .\dist\dcs_liberation\ -DestinationPath
|
Compress-Archive -Path .\dist\dcs_liberation\ -DestinationPath
|
||||||
dist\dcs_liberation.zip
|
dist\dcs_liberation.zip
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dcs_liberation
|
name: dcs_liberation
|
||||||
path: dist/dcs_liberation.zip
|
path: dist/dcs_liberation.zip
|
||||||
|
|||||||
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -7,11 +7,11 @@ jobs:
|
|||||||
name: Black
|
name: Black
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v2
|
||||||
- uses: psf/black@stable
|
- uses: psf/black@stable
|
||||||
with:
|
with:
|
||||||
version: ~=24.3.0
|
version: ~=22.12
|
||||||
src: "."
|
src: "."
|
||||||
options: "--check"
|
options: "--check"
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
name: Type checking
|
name: Type checking
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
release: true
|
release: true
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dcs_liberation
|
name: dcs_liberation
|
||||||
path: dist/
|
path: dist/
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
needs: [build]
|
needs: [build]
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4.1.7
|
- uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: dcs_liberation
|
name: dcs_liberation
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -5,7 +5,7 @@ jobs:
|
|||||||
name: Python tests
|
name: Python tests
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
name: Typescript tests
|
name: Typescript tests
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up JS environment
|
- name: Set up JS environment
|
||||||
uses: ./.github/actions/setup-liberation-js
|
uses: ./.github/actions/setup-liberation-js
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.11.0
|
rev: 22.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
language_version: python3
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
|
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
|
||||||
|
|
||||||
|
[](https://patreon.com/khopa)
|
||||||
|
|
||||||
[](https://github.com/dcs-liberation/dcs_liberation/releases)
|
[](https://github.com/dcs-liberation/dcs_liberation/releases)
|
||||||
|
|
||||||
[](https://discord.gg/bKrtrkJ)
|
[](https://discord.gg/bKrtrkJ)
|
||||||
@@ -15,6 +17,11 @@
|
|||||||
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
|
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
|
||||||
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
|
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
|
||||||
|
|
||||||
|
**Note that DCS Liberation does not support the stable release of DCS. We can
|
||||||
|
only guarantee compatibility with either the open beta or the stable release,
|
||||||
|
and more people play the open beta. DCS stable _might_ work sometimes, but it's
|
||||||
|
untested, and we will be unable to fix any bugs unique to stable DCS.**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
|
|||||||
165
changelog.md
165
changelog.md
@@ -1,168 +1,3 @@
|
|||||||
# 14.0.0
|
|
||||||
|
|
||||||
Saves from 13.x are not compatible with 14.0.0.
|
|
||||||
|
|
||||||
## Features/Improvements
|
|
||||||
|
|
||||||
* **[Engine]** Support for DCS 2.9.21.
|
|
||||||
* **[Engine]** Support for MiG-29 Fulcrum (full fidelity version).
|
|
||||||
* **[Engine]** Support for F-14A-135-GR Early.
|
|
||||||
* **[Mods]** A4EC mod version updated to 2.3.
|
|
||||||
* **[UI]** Allow saving after fast forwarding manually with sim speed controls (--show-sim-speed-controls option).
|
|
||||||
* **[UI]** Add new option to fast forward until player is at the IP.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* **[Campaign]** Units are restored to full health when repaired.
|
|
||||||
* **[UI]** Air Wing and Transfers buttons disabled when no game is loaded as pressing them without a game loaded resulted in a crash.
|
|
||||||
* **[UI]** A package is cancelled (deleted) when the last flight in the package is cancelled instead of showing "No mission".
|
|
||||||
|
|
||||||
# 13.0.0
|
|
||||||
|
|
||||||
Saves from 12.x are not compatible with 13.0.0.
|
|
||||||
|
|
||||||
## Features/Improvements
|
|
||||||
|
|
||||||
* **[Engine]** Support for DCS 2.9.12.5536.
|
|
||||||
* **[Data]** Support for CH-47 Chinook.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* **[Data]** Added/updated CBU weapons data.
|
|
||||||
* **[Data]** Added Strike mission type for KA-50 (all playable variants).
|
|
||||||
* **[Mission Generation]** Fixed crash when using factions that have Eastern callsign conventions.
|
|
||||||
* **[Mission Generation]** Fixed issues when spawning on carriers and FARPs in multiplayer sessions.
|
|
||||||
|
|
||||||
|
|
||||||
# 12.0.0
|
|
||||||
|
|
||||||
Saves from 11.x are not compatible with 12.0.0.
|
|
||||||
|
|
||||||
## Features/Improvements
|
|
||||||
|
|
||||||
* **[Engine]** Support for DCS 2.9.9.2280.
|
|
||||||
* **[Campaign]** Flights are assigned different callsigns appropriate to the faction.
|
|
||||||
* **[Campaign]** Removed deprecated settings for generating persistent and invulnerable AWACs and tankers.
|
|
||||||
* **[Data]** Added ability to restrict weapons usage for a faction to a different year from the nominal weapon introduction year. Updated faction data to restrict more advanced missiles from Soviet client states during the cold war. Updated Egypt 2000 faction to restrict AIM-120 usage.
|
|
||||||
* **[Mission Generation]** Added option to skip combat when fast forwarding, which progresses fast forward as if the combat did not occur. Simplified fast forward settings by consolidating "Fast forward mission to first contact" and "Player missions interrupt fast forward" into a single setting and expanding options for "Auto-resolve combat during fast-forward (WIP)".
|
|
||||||
* **[Mods]** F/A-18 E/F/G Super Hornet mod version updated to 2.3.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* **[Data]** Added/updated weapons data for F4E weapons such as AIM-7, AIM-9, AGM-12, AGM-45 and AGM-65.
|
|
||||||
* **[Campaign]** Do not allow aircraft from a captured control point to retreat if the captured control point has a damaged runway.
|
|
||||||
* **[Campaign]** Do not allow ground units to be transferred to LHAs, CVNs or off map spawns.
|
|
||||||
* **[Mission Generation]** Fixed aircraft not spawning correctly on CVNs, LHAs and FARPs.
|
|
||||||
|
|
||||||
# 11.1.1
|
|
||||||
|
|
||||||
Saves from 11.0.0 are compatible with 11.1.1. See Known Issues section for exceptions.
|
|
||||||
|
|
||||||
## Features/Improvements
|
|
||||||
|
|
||||||
* **[Engine]** Support for DCS 2.9.5.55918 including Heatblur F-4E and Polychop OH-58D Kiowa Warrior support.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* **[Campaign]** Fixed double counting of parked aircraft kills when DCS reports multiple kill events.
|
|
||||||
* **[Campaign]** Fixed error where frontline units are not re-deployed when multiple control points were captured in one turn or when control points are captured "out of order" using air-assault missions.
|
|
||||||
* **[Cheat Menu]** Re-deploy frontline units when using cheats to capture control points, so that cheats behave the same way as capturing a control point in-mission.
|
|
||||||
* **[Data]** Added FuSe-65 Early Warning Radar.
|
|
||||||
* **[Data]** Updated Peru 1995 and Germany 1944 factions.
|
|
||||||
* **[Flight Planning]** Theater refuelling flight plans (those not tied to a particular package) will remain on station for a longer period, specifically the desired mission duration + 30 minutes. By default, this increases the on-station time from 1 hour to 1.5 hours.
|
|
||||||
* **[Mission Generation]** Patched bug where Liberation crashed when aborting a turn when Fighter Sweep missions were planned.
|
|
||||||
* **[Radios]** Added radio setup for F-5E, F-86, Mi-8 and Mi-24.
|
|
||||||
* **[UI]** Naval control points (carriers, LHAs) can no longer be moved onto land.
|
|
||||||
|
|
||||||
## Known Issues
|
|
||||||
|
|
||||||
* When loading saves from 11.0, loadouts with AGM-45B (Imp), typically on A-4E-C mod, will have the AGM-45B replaced with an empty pylon due to changes in DCS for this weapon. The AGM-45A is not affected by this issue.
|
|
||||||
|
|
||||||
# 11.0.0
|
|
||||||
|
|
||||||
Saves from 10.x are not compatible with 11.0.0.
|
|
||||||
|
|
||||||
## Features/Improvements
|
|
||||||
|
|
||||||
* **[Engine]** Support for DCS 2.9.3.51704.
|
|
||||||
* **[Campaign]** Improved tracking of parked aircraft deaths. Parked aircraft are now considered dead once sufficient damage is done, meaning guns, rockets and AGMs are viable weapons for OCA/Aircraft missions. Previously Liberation relied on DCS death tracking which required parked aircraft to be hit with more powerful weapons e.g. 2000lb bombs as they were uncontrolled.
|
|
||||||
* **[Campaign]** Track damage to theater ground objects across turns. Damage can accumulate across turns leading to death of the unit. This behavior only applies to SAMs, ships and other units that appear on the Liberation map. Frontline units and buildings are not tracked (yet).
|
|
||||||
* **[Mods]** F/A-18 E/F/G Super Hornet mod (v2.2.5) support added.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* **[Mission Generation]** When planning anti-ship missions against carriers or LHAs, target escorts (if present) if the carrier/LHA is sunk.
|
|
||||||
* **[UI]** Identify that a carrier or LHA is sunk instead of "damaged".
|
|
||||||
|
|
||||||
# 10.0.0
|
|
||||||
|
|
||||||
Saves from 9.x are not compatible with 10.0.0.
|
|
||||||
|
|
||||||
## Features/Improvements
|
|
||||||
|
|
||||||
* **[Engine]** Support for DCS 2.9.2.49629 Open Beta. (F-15E JDAM and JSOW, F-16 AIM-9P, updated Falklands and Normandy airfields).
|
|
||||||
* **[UI]** Improved the description of "runway" state for FARPs, FOBs, carriers, and off-map spawns.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* **[Flight Planning]** Aircraft from even numbered flights will no longer become inaccessible when canceling a draft package.
|
|
||||||
* **[UI]** Flight members in the loadout menu are now numbered starting from 1 instead of 0.
|
|
||||||
* **[UI]** Flight plan paths are now drawn behind all other map elements, fixing rare cases where they could prevent other UI elements from being clickable.
|
|
||||||
|
|
||||||
# 9.0.0
|
|
||||||
|
|
||||||
Saves from 8.x are not compatible with 9.0.0.
|
|
||||||
|
|
||||||
## Features/Improvements
|
|
||||||
|
|
||||||
* **[Engine]** Support for DCS Open Beta 2.9.0.46801.
|
|
||||||
* **[Campaign]** Added ferry only control points, which offer campaign designers a way to add squadrons that can be brought in after additional airfields are captured.
|
|
||||||
* **[Campaign]** The new squadron rules (size limits, beginning the campaign at full strength) are now the default and required. The old style of unlimited squadron sizes and starting with zero aircraft has been removed.
|
|
||||||
* **[Data]** Added support for the ARA Veinticinco de Mayo.
|
|
||||||
* **[Data]** Changed display name of the AI-only F-15E Strike Eagle for clarity.
|
|
||||||
* **[Flight Planning]** Improved IP selection for targets that are near the center of a threat zone.
|
|
||||||
* **[Flight Planning]** Moved CAS ingress point off the front line so that the AI begins their target search earlier.
|
|
||||||
* **[Flight Planning]** Loadouts and aircraft properties can now be set per-flight member. Warning: AI flights should not use mixed loadouts.
|
|
||||||
* **[Flight Planning]** Laser codes that are pre-assigned to weapons at mission start can now be chosen from a list in the loadout UI. This does not affect the aircraft's TGP, just the weapons. Currently only implemented for the F-15E S4+ and F-16C.
|
|
||||||
* **[Mission Generation]** Configured target and initial points for F-15E S4+.
|
|
||||||
* **[Mission Generation]** Added a package kneeboard page that shows the radio frequencies, tasks, and laser codes for each member of your package.
|
|
||||||
* **[Mission Generation]** Added option to generate AI flights with unlimited fuel (enabled by default).
|
|
||||||
* **[Modding]** Factions can now specify the ship type to be used for cargo shipping. The Handy Wind will be used by default, but WW2 factions can pick something more appropriate.
|
|
||||||
* **[Modding]** Unit variants can now set a display name separate from their ID.
|
|
||||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.2.0 release.
|
|
||||||
* **[UI]** An error will be displayed when invalid fast-forward options are selected rather than beginning a never ending simulation.
|
|
||||||
* **[UI]** Added cheats for instantly repairing and destroying runways.
|
|
||||||
* **[UI]** Improved usability of the flight properties UI. It now shows human-readable names and uses more appropriate UI elements.
|
|
||||||
* **[UI]** The map now shows the real front line bounds.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* **[Campaign]** Fixed error when canceling squadron transfer if the current location would be exactly full.
|
|
||||||
* **[Data]** Fixed the class of the Samuel Chase so it can't be picked for a AAA or SHORAD site.
|
|
||||||
* **[Data]** Allow CH-47D, CH-53E and UH-60A to operate from carriers and LHAs.
|
|
||||||
* **[Data]** Added the F-15E's LANTIRN to the list of known targeting pods. Player F-15E flight with TGPs will now be assigned laser codes.
|
|
||||||
* **[Flight Planning]** Patrolling flight plans (CAS, CAP, refueling, etc) now handle TOT offsets.
|
|
||||||
* **[Loadouts]** Fixed error when loading certain DCS loadouts which contained an empty pylon (notably the Mosquito).
|
|
||||||
* **[Mission Generation]** Restored previous AI behavior for anti-ship missions. A DCS update caused only a single aircraft in a flight to attack. The full flight will now attack like they used to.
|
|
||||||
* **[Mission Generation]** Fix generation of OCA Runway missions to allow LGBs to be used.
|
|
||||||
* **[Mission Generation]** Fixed AI flights flying far too slowly toward NAV points.
|
|
||||||
* **[Mission Generation]** Fixed Recovery Tanker mission type intermittently failing due to not being able to find the CVN.
|
|
||||||
* **[Mission Generation]** Fixed "division by zero" error on mission generation when a flight has an "In-Flight" start type and starts on top of a mission waypoint.
|
|
||||||
* **[Mission Generation]** Fixed flights not being selectable in the mission editor if fast-forward was used and they were generated at a waypoint that had a fixed TOT (such as a BARCAP that was on-station).
|
|
||||||
* **[Mission Generation]** Fixed error when planning TARCAPs on the sole remaining enemy airfield.
|
|
||||||
* **[Mission Generation]** Fixed allocation range for carrier Link 4 datalink.
|
|
||||||
* **[Modding]** Unit variants can now actually override base unit type properties.
|
|
||||||
* **[New Game Wizard]** Factions are reset to default after clicking "Back" to Theater Configuration screen.
|
|
||||||
* **[Plugins]** Fixed Lua errors in Skynet plugin that would occur whenever one coalition had no IADS nodes.
|
|
||||||
* **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint.
|
|
||||||
* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes.
|
|
||||||
* **[UI]** In unit transfer dialog, only list control points that are reachable from the control point units are being transferred from.
|
|
||||||
* **[UI]** Fixed UI bug where altering an "ahead of package" TOT offset would change the offset back to a "behind package" offset.
|
|
||||||
* **[UI]** Fixed bug where changing TOT offsets could result in flight startup times that are in the past.
|
|
||||||
* **[UI]** Fixed odd spacing of the finance window when there were not enough items to fill the page.
|
|
||||||
* **[UI]** Fixed regression where waypoint altitude changes in the waypoint list screen are applied to the wrong waypoint.
|
|
||||||
* **[UI]** Fixed regression where waypoint additions in custom flight plans are not reflected until the window is reloaded.
|
|
||||||
|
|
||||||
# 8.1.0
|
# 8.1.0
|
||||||
|
|
||||||
Saves from 8.0.0 are compatible with 8.1.0
|
Saves from 8.0.0 are compatible with 8.1.0
|
||||||
|
|||||||
4806
client/package-lock.json
generated
4806
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@
|
|||||||
"@types/react": "^18.0.21",
|
"@types/react": "^18.0.21",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/react-redux": "^7.1.24",
|
"@types/react-redux": "^7.1.24",
|
||||||
"axios": "^1.12.1",
|
"axios": "^1.1.2",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"esri-leaflet": "^3.0.8",
|
"esri-leaflet": "^3.0.8",
|
||||||
"leaflet": "^1.9.2",
|
"leaflet": "^1.9.2",
|
||||||
@@ -62,26 +62,25 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rtk-query/codegen-openapi": "^1.0.0",
|
"@rtk-query/codegen-openapi": "^1.0.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.2.1",
|
"@trivago/prettier-plugin-sort-imports": "^3.3.0",
|
||||||
"@types/leaflet": "^1.8.0",
|
"@types/leaflet": "^1.8.0",
|
||||||
"@types/redux-logger": "^3.0.9",
|
"@types/redux-logger": "^3.0.9",
|
||||||
"@types/websocket": "^1.0.5",
|
"@types/websocket": "^1.0.5",
|
||||||
"electron": "^35.7.5",
|
"electron": "^21.1.0",
|
||||||
"electron-is-dev": "^2.0.0",
|
"electron-is-dev": "^2.0.0",
|
||||||
"generate-license-file": "^2.0.0",
|
"generate-license-file": "^2.0.0",
|
||||||
"jest-transform-stub": "^2.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"msw": "^1.2.2",
|
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"wait-on": "^8.0.0"
|
"wait-on": "^6.0.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"node_modules/(?!(@?react-leaflet|axios)/)"
|
"node_modules/(?!(@?react-leaflet|axios)/)"
|
||||||
],
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
|
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "identity-obj-proxy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ const injectedRtkApi = api.injectEndpoints({
|
|||||||
url: `/debug/waypoint-geometries/hold/${queryArg.flightId}`,
|
url: `/debug/waypoint-geometries/hold/${queryArg.flightId}`,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
getDebugIpZones: build.query<
|
||||||
|
GetDebugIpZonesApiResponse,
|
||||||
|
GetDebugIpZonesApiArg
|
||||||
|
>({
|
||||||
|
query: (queryArg) => ({
|
||||||
|
url: `/debug/waypoint-geometries/ip/${queryArg.flightId}`,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
getDebugJoinZones: build.query<
|
getDebugJoinZones: build.query<
|
||||||
GetDebugJoinZonesApiResponse,
|
GetDebugJoinZonesApiResponse,
|
||||||
GetDebugJoinZonesApiArg
|
GetDebugJoinZonesApiArg
|
||||||
@@ -237,6 +245,11 @@ export type GetDebugHoldZonesApiResponse =
|
|||||||
export type GetDebugHoldZonesApiArg = {
|
export type GetDebugHoldZonesApiArg = {
|
||||||
flightId: string;
|
flightId: string;
|
||||||
};
|
};
|
||||||
|
export type GetDebugIpZonesApiResponse =
|
||||||
|
/** status 200 Successful Response */ IpZones;
|
||||||
|
export type GetDebugIpZonesApiArg = {
|
||||||
|
flightId: string;
|
||||||
|
};
|
||||||
export type GetDebugJoinZonesApiResponse =
|
export type GetDebugJoinZonesApiResponse =
|
||||||
/** status 200 Successful Response */ JoinZones;
|
/** status 200 Successful Response */ JoinZones;
|
||||||
export type GetDebugJoinZonesApiArg = {
|
export type GetDebugJoinZonesApiArg = {
|
||||||
@@ -366,6 +379,12 @@ export type HoldZones = {
|
|||||||
permissibleZones: LatLng[][][];
|
permissibleZones: LatLng[][][];
|
||||||
preferredLines: LatLng[][];
|
preferredLines: LatLng[][];
|
||||||
};
|
};
|
||||||
|
export type IpZones = {
|
||||||
|
homeBubble: LatLng[][];
|
||||||
|
ipBubble: LatLng[][];
|
||||||
|
permissibleZone: LatLng[][];
|
||||||
|
safeZones: LatLng[][][];
|
||||||
|
};
|
||||||
export type JoinZones = {
|
export type JoinZones = {
|
||||||
homeBubble: LatLng[][];
|
homeBubble: LatLng[][];
|
||||||
targetBubble: LatLng[][];
|
targetBubble: LatLng[][];
|
||||||
@@ -478,6 +497,7 @@ export const {
|
|||||||
useSetControlPointDestinationMutation,
|
useSetControlPointDestinationMutation,
|
||||||
useClearControlPointDestinationMutation,
|
useClearControlPointDestinationMutation,
|
||||||
useGetDebugHoldZonesQuery,
|
useGetDebugHoldZonesQuery,
|
||||||
|
useGetDebugIpZonesQuery,
|
||||||
useGetDebugJoinZonesQuery,
|
useGetDebugJoinZonesQuery,
|
||||||
useListFlightsQuery,
|
useListFlightsQuery,
|
||||||
useGetFlightByIdQuery,
|
useGetFlightByIdQuery,
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ const backendAddr =
|
|||||||
new URL(window.location.toString()).searchParams.get("server") ??
|
new URL(window.location.toString()).searchParams.get("server") ??
|
||||||
"[::1]:16880";
|
"[::1]:16880";
|
||||||
|
|
||||||
// MSW can't handle IPv6 URLs...
|
export const HTTP_URL = `http://${backendAddr}/`;
|
||||||
// https://github.com/mswjs/msw/issues/1388
|
|
||||||
export const HTTP_URL =
|
|
||||||
process.env.NODE_ENV === "test" ? "" : `http://${backendAddr}/`;
|
|
||||||
|
|
||||||
export const backend = axios.create({
|
export const backend = axios.create({
|
||||||
baseURL: HTTP_URL,
|
baseURL: HTTP_URL,
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ export const liberationApi = _liberationApi.enhanceEndpoints({
|
|||||||
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
getDebugIpZones: {
|
||||||
|
providesTags: (result, error, arg) => [
|
||||||
|
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
||||||
|
],
|
||||||
|
},
|
||||||
getDebugJoinZones: {
|
getDebugJoinZones: {
|
||||||
providesTags: (result, error, arg) => [
|
providesTags: (result, error, arg) => [
|
||||||
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
{ type: Tags.FLIGHT_PLAN, id: arg.flightId },
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Flight } from "../../api/liberationApi";
|
import { Flight } from "../../api/liberationApi";
|
||||||
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
|
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
|
||||||
import WaypointMarker from "../waypointmarker";
|
import WaypointMarker from "../waypointmarker";
|
||||||
import { Polyline as LPolyline } from "leaflet";
|
import { ReactElement } from "react";
|
||||||
import { ReactElement, useEffect, useRef } from "react";
|
|
||||||
import { Polyline } from "react-leaflet";
|
import { Polyline } from "react-leaflet";
|
||||||
|
|
||||||
const BLUE_PATH = "#0084ff";
|
const BLUE_PATH = "#0084ff";
|
||||||
@@ -28,41 +27,16 @@ const pathColor = (props: FlightPlanProps) => {
|
|||||||
function FlightPlanPath(props: FlightPlanProps) {
|
function FlightPlanPath(props: FlightPlanProps) {
|
||||||
const color = pathColor(props);
|
const color = pathColor(props);
|
||||||
const waypoints = props.flight.waypoints;
|
const waypoints = props.flight.waypoints;
|
||||||
|
|
||||||
const polylineRef = useRef<LPolyline | null>(null);
|
|
||||||
|
|
||||||
// Flight paths should be drawn under everything else. There seems to be an
|
|
||||||
// issue where `interactive: false` doesn't do as its told (there's nuance,
|
|
||||||
// see the bug for details). It looks better if we draw the other elements on
|
|
||||||
// top of the flight plans anyway, so just push the flight plan to the back.
|
|
||||||
//
|
|
||||||
// https://github.com/dcs-liberation/dcs_liberation/issues/3295
|
|
||||||
//
|
|
||||||
// It's not possible to z-index a polyline (and leaflet says it never will be,
|
|
||||||
// because this is a limitation of SVG, not leaflet:
|
|
||||||
// https://github.com/Leaflet/Leaflet/issues/185), so we need to use
|
|
||||||
// bringToBack() to push the flight paths to the back of the drawing once
|
|
||||||
// they've been added to the map. They'll still draw on top of the map, but
|
|
||||||
// behind everything than was added before them. Anything added after always
|
|
||||||
// goes on top.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!props.selected) {
|
|
||||||
polylineRef.current?.bringToBack();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (waypoints == null) {
|
if (waypoints == null) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
const points = waypoints
|
const points = waypoints
|
||||||
.filter((waypoint) => waypoint.include_in_path)
|
.filter((waypoint) => waypoint.include_in_path)
|
||||||
.map((waypoint) => waypoint.position);
|
.map((waypoint) => waypoint.position);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Polyline
|
<Polyline
|
||||||
positions={points}
|
positions={points}
|
||||||
pathOptions={{ color: color, interactive: false }}
|
pathOptions={{ color: color, interactive: false }}
|
||||||
ref={polylineRef}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,12 +95,8 @@ describe("FlightPlansLayer", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||||
// For some reason passing ref to PolyLine causes it and its group to be
|
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||||
// redrawn, so these numbers don't match what you'd expect from the test.
|
|
||||||
// It probably needs to be rewritten without mocks.
|
|
||||||
expect(mockPolyline).toHaveBeenCalledTimes(3);
|
|
||||||
expect(mockLayerGroup).toBeCalledTimes(2);
|
|
||||||
});
|
});
|
||||||
it("are not drawn if wrong coalition", () => {
|
it("are not drawn if wrong coalition", () => {
|
||||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||||
|
|||||||
73
client/src/components/waypointdebugzones/IpZones.tsx
Normal file
73
client/src/components/waypointdebugzones/IpZones.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { useGetDebugIpZonesQuery } from "../../api/liberationApi";
|
||||||
|
import { LayerGroup, Polygon } from "react-leaflet";
|
||||||
|
|
||||||
|
interface IpZonesProps {
|
||||||
|
flightId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IpZones(props: IpZonesProps) {
|
||||||
|
const { data, error, isLoading } = useGetDebugIpZonesQuery({
|
||||||
|
flightId: props.flightId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error("Error while loading waypoint IP zone info", error);
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
console.log("Waypoint IP zone returned empty response");
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Polygon
|
||||||
|
positions={data.homeBubble}
|
||||||
|
color="#ffff00"
|
||||||
|
fillOpacity={0.1}
|
||||||
|
interactive={false}
|
||||||
|
/>
|
||||||
|
<Polygon
|
||||||
|
positions={data.ipBubble}
|
||||||
|
color="#bb89ff"
|
||||||
|
fillOpacity={0.1}
|
||||||
|
interactive={false}
|
||||||
|
/>
|
||||||
|
<Polygon
|
||||||
|
positions={data.permissibleZone}
|
||||||
|
color="#ffffff"
|
||||||
|
fillOpacity={0.1}
|
||||||
|
interactive={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{data.safeZones.map((zone, idx) => {
|
||||||
|
return (
|
||||||
|
<Polygon
|
||||||
|
key={idx}
|
||||||
|
positions={zone}
|
||||||
|
color="#80BA80"
|
||||||
|
fillOpacity={0.1}
|
||||||
|
interactive={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IpZonesLayerProps {
|
||||||
|
flightId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IpZonesLayer(props: IpZonesLayerProps) {
|
||||||
|
return (
|
||||||
|
<LayerGroup>
|
||||||
|
{props.flightId ? <IpZones flightId={props.flightId} /> : <></>}
|
||||||
|
</LayerGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { selectSelectedFlightId } from "../../api/flightsSlice";
|
import { selectSelectedFlightId } from "../../api/flightsSlice";
|
||||||
import { useAppSelector } from "../../app/hooks";
|
import { useAppSelector } from "../../app/hooks";
|
||||||
import { HoldZonesLayer } from "./HoldZones";
|
import { HoldZonesLayer } from "./HoldZones";
|
||||||
|
import { IpZonesLayer } from "./IpZones";
|
||||||
import { JoinZonesLayer } from "./JoinZones";
|
import { JoinZonesLayer } from "./JoinZones";
|
||||||
import { LayersControl } from "react-leaflet";
|
import { LayersControl } from "react-leaflet";
|
||||||
|
|
||||||
@@ -15,6 +16,9 @@ export function WaypointDebugZonesControls() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<LayersControl.Overlay name="IP zones">
|
||||||
|
<IpZonesLayer flightId={selectedFlightId} />
|
||||||
|
</LayersControl.Overlay>
|
||||||
<LayersControl.Overlay name="Join zones">
|
<LayersControl.Overlay name="Join zones">
|
||||||
<JoinZonesLayer flightId={selectedFlightId} />
|
<JoinZonesLayer flightId={selectedFlightId} />
|
||||||
</LayersControl.Overlay>
|
</LayersControl.Overlay>
|
||||||
|
|||||||
@@ -1,277 +0,0 @@
|
|||||||
import { HTTP_URL } from "../../api/backend";
|
|
||||||
import { renderWithProviders } from "../../testutils";
|
|
||||||
import WaypointMarker, { TOOLTIP_ZOOM_LEVEL } from "./WaypointMarker";
|
|
||||||
import { Map, Marker } from "leaflet";
|
|
||||||
import { rest, MockedRequest, matchRequestUrl } from "msw";
|
|
||||||
import { setupServer } from "msw/node";
|
|
||||||
import React from "react";
|
|
||||||
import { MapContainer } from "react-leaflet";
|
|
||||||
|
|
||||||
// https://mswjs.io/docs/extensions/life-cycle-events#asserting-request-payload
|
|
||||||
const waitForRequest = (method: string, url: string) => {
|
|
||||||
let requestId = "";
|
|
||||||
|
|
||||||
return new Promise<MockedRequest>((resolve, reject) => {
|
|
||||||
server.events.on("request:start", (req) => {
|
|
||||||
const matchesMethod = req.method.toLowerCase() === method.toLowerCase();
|
|
||||||
const matchesUrl = matchRequestUrl(req.url, url).matches;
|
|
||||||
|
|
||||||
if (matchesMethod && matchesUrl) {
|
|
||||||
requestId = req.id;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.events.on("request:match", (req) => {
|
|
||||||
if (req.id === requestId) {
|
|
||||||
resolve(req);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.events.on("request:unhandled", (req) => {
|
|
||||||
if (req.id === requestId) {
|
|
||||||
reject(
|
|
||||||
new Error(`The ${req.method} ${req.url.href} request was unhandled.`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const server = setupServer(
|
|
||||||
rest.post(
|
|
||||||
`${HTTP_URL}/waypoints/:flightId/:waypointIdx/position`,
|
|
||||||
(req, res, ctx) => {
|
|
||||||
if (req.params.flightId === "") {
|
|
||||||
return res(ctx.status(500));
|
|
||||||
}
|
|
||||||
if (req.params.waypointIdx === "0") {
|
|
||||||
return res(ctx.status(403));
|
|
||||||
}
|
|
||||||
return res(ctx.status(204));
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
|
|
||||||
afterEach(() => server.resetHandlers());
|
|
||||||
afterAll(() => server.close());
|
|
||||||
|
|
||||||
describe("WaypointMarker", () => {
|
|
||||||
it("is placed in the correct location", () => {
|
|
||||||
const waypoint = {
|
|
||||||
name: "",
|
|
||||||
position: { lat: 0, lng: 0 },
|
|
||||||
altitude_ft: 0,
|
|
||||||
altitude_reference: "MSL",
|
|
||||||
is_movable: false,
|
|
||||||
should_mark: false,
|
|
||||||
include_in_path: true,
|
|
||||||
timing: "",
|
|
||||||
};
|
|
||||||
const marker = React.createRef<Marker>();
|
|
||||||
renderWithProviders(
|
|
||||||
<MapContainer>
|
|
||||||
<WaypointMarker
|
|
||||||
number={0}
|
|
||||||
waypoint={waypoint}
|
|
||||||
flight={{
|
|
||||||
id: "",
|
|
||||||
blue: true,
|
|
||||||
sidc: "",
|
|
||||||
waypoints: [waypoint],
|
|
||||||
}}
|
|
||||||
ref={marker}
|
|
||||||
/>
|
|
||||||
</MapContainer>
|
|
||||||
);
|
|
||||||
expect(marker.current?.getLatLng()).toEqual({ lat: 0, lng: 0 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tooltip is hidden when zoomed out", () => {
|
|
||||||
const waypoint = {
|
|
||||||
name: "",
|
|
||||||
position: { lat: 0, lng: 0 },
|
|
||||||
altitude_ft: 0,
|
|
||||||
altitude_reference: "MSL",
|
|
||||||
is_movable: false,
|
|
||||||
should_mark: false,
|
|
||||||
include_in_path: true,
|
|
||||||
timing: "",
|
|
||||||
};
|
|
||||||
const map = React.createRef<Map>();
|
|
||||||
const marker = React.createRef<Marker>();
|
|
||||||
renderWithProviders(
|
|
||||||
<MapContainer zoom={0} ref={map}>
|
|
||||||
<WaypointMarker
|
|
||||||
number={0}
|
|
||||||
waypoint={waypoint}
|
|
||||||
flight={{
|
|
||||||
id: "",
|
|
||||||
blue: true,
|
|
||||||
sidc: "",
|
|
||||||
waypoints: [waypoint],
|
|
||||||
}}
|
|
||||||
ref={marker}
|
|
||||||
/>
|
|
||||||
</MapContainer>
|
|
||||||
);
|
|
||||||
map.current?.setView({ lat: 0, lng: 0 }, TOOLTIP_ZOOM_LEVEL - 1);
|
|
||||||
expect(marker.current?.getTooltip()?.isOpen()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tooltip is shown when zoomed in", () => {
|
|
||||||
const waypoint = {
|
|
||||||
name: "",
|
|
||||||
position: { lat: 0, lng: 0 },
|
|
||||||
altitude_ft: 0,
|
|
||||||
altitude_reference: "MSL",
|
|
||||||
is_movable: false,
|
|
||||||
should_mark: false,
|
|
||||||
include_in_path: true,
|
|
||||||
timing: "",
|
|
||||||
};
|
|
||||||
const map = React.createRef<Map>();
|
|
||||||
const marker = React.createRef<Marker>();
|
|
||||||
renderWithProviders(
|
|
||||||
<MapContainer ref={map}>
|
|
||||||
<WaypointMarker
|
|
||||||
number={0}
|
|
||||||
waypoint={waypoint}
|
|
||||||
flight={{
|
|
||||||
id: "",
|
|
||||||
blue: true,
|
|
||||||
sidc: "",
|
|
||||||
waypoints: [waypoint],
|
|
||||||
}}
|
|
||||||
ref={marker}
|
|
||||||
/>
|
|
||||||
</MapContainer>
|
|
||||||
);
|
|
||||||
map.current?.setView({ lat: 0, lng: 0 }, TOOLTIP_ZOOM_LEVEL);
|
|
||||||
expect(marker.current?.getTooltip()?.isOpen()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tooltip has correct contents", () => {
|
|
||||||
const waypoint = {
|
|
||||||
name: "",
|
|
||||||
position: { lat: 0, lng: 0 },
|
|
||||||
altitude_ft: 25000,
|
|
||||||
altitude_reference: "MSL",
|
|
||||||
is_movable: false,
|
|
||||||
should_mark: false,
|
|
||||||
include_in_path: true,
|
|
||||||
timing: "09:00:00",
|
|
||||||
};
|
|
||||||
const map = React.createRef<Map>();
|
|
||||||
const marker = React.createRef<Marker>();
|
|
||||||
renderWithProviders(
|
|
||||||
<MapContainer ref={map}>
|
|
||||||
<WaypointMarker
|
|
||||||
number={0}
|
|
||||||
waypoint={waypoint}
|
|
||||||
flight={{
|
|
||||||
id: "",
|
|
||||||
blue: true,
|
|
||||||
sidc: "",
|
|
||||||
waypoints: [waypoint],
|
|
||||||
}}
|
|
||||||
ref={marker}
|
|
||||||
/>
|
|
||||||
</MapContainer>
|
|
||||||
);
|
|
||||||
expect(marker.current?.getTooltip()?.getContent()).toEqual(
|
|
||||||
"0 <br />25000 ft MSL<br />09:00:00"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("resets the tooltip while dragging", () => {
|
|
||||||
const waypoint = {
|
|
||||||
name: "",
|
|
||||||
position: { lat: 0, lng: 0 },
|
|
||||||
altitude_ft: 25000,
|
|
||||||
altitude_reference: "MSL",
|
|
||||||
is_movable: false,
|
|
||||||
should_mark: false,
|
|
||||||
include_in_path: true,
|
|
||||||
timing: "09:00:00",
|
|
||||||
};
|
|
||||||
const marker = React.createRef<Marker>();
|
|
||||||
renderWithProviders(
|
|
||||||
<MapContainer>
|
|
||||||
<WaypointMarker
|
|
||||||
number={0}
|
|
||||||
waypoint={waypoint}
|
|
||||||
flight={{
|
|
||||||
id: "",
|
|
||||||
blue: true,
|
|
||||||
sidc: "",
|
|
||||||
waypoints: [waypoint],
|
|
||||||
}}
|
|
||||||
ref={marker}
|
|
||||||
/>
|
|
||||||
</MapContainer>
|
|
||||||
);
|
|
||||||
marker.current?.fireEvent("dragstart");
|
|
||||||
expect(marker.current?.getTooltip()?.getContent()).toEqual(
|
|
||||||
"Waiting to recompute TOT..."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sends the new position to the backend on dragend", async () => {
|
|
||||||
const departure = {
|
|
||||||
name: "",
|
|
||||||
position: { lat: 0, lng: 0 },
|
|
||||||
altitude_ft: 25000,
|
|
||||||
altitude_reference: "MSL",
|
|
||||||
is_movable: false,
|
|
||||||
should_mark: false,
|
|
||||||
include_in_path: true,
|
|
||||||
timing: "09:00:00",
|
|
||||||
};
|
|
||||||
const waypoint = {
|
|
||||||
name: "",
|
|
||||||
position: { lat: 1, lng: 1 },
|
|
||||||
altitude_ft: 25000,
|
|
||||||
altitude_reference: "MSL",
|
|
||||||
is_movable: false,
|
|
||||||
should_mark: false,
|
|
||||||
include_in_path: true,
|
|
||||||
timing: "09:00:00",
|
|
||||||
};
|
|
||||||
const flight = {
|
|
||||||
id: "1234",
|
|
||||||
blue: true,
|
|
||||||
sidc: "",
|
|
||||||
waypoints: [departure, waypoint],
|
|
||||||
};
|
|
||||||
const marker = React.createRef<Marker>();
|
|
||||||
|
|
||||||
// There is no observable UI change from moving a waypoint, just a message
|
|
||||||
// to the backend to record the frontend change. The real backend will then
|
|
||||||
// push an updated game state which will update redux, but that's not part
|
|
||||||
// of this component's behavior.
|
|
||||||
const pendingRequest = waitForRequest(
|
|
||||||
"POST",
|
|
||||||
`${HTTP_URL}/waypoints/1234/1/position`
|
|
||||||
);
|
|
||||||
|
|
||||||
renderWithProviders(
|
|
||||||
<MapContainer>
|
|
||||||
<WaypointMarker number={0} waypoint={departure} flight={flight} />
|
|
||||||
<WaypointMarker
|
|
||||||
number={1}
|
|
||||||
waypoint={waypoint}
|
|
||||||
flight={flight}
|
|
||||||
ref={marker}
|
|
||||||
/>
|
|
||||||
</MapContainer>
|
|
||||||
);
|
|
||||||
|
|
||||||
marker.current?.fireEvent("dragstart");
|
|
||||||
marker.current?.fireEvent("dragend", { target: marker.current });
|
|
||||||
|
|
||||||
const request = await pendingRequest;
|
|
||||||
const response = await request.json();
|
|
||||||
expect(response).toEqual({ lat: 1, lng: 1 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -3,23 +3,13 @@ import {
|
|||||||
Waypoint,
|
Waypoint,
|
||||||
useSetWaypointPositionMutation,
|
useSetWaypointPositionMutation,
|
||||||
} from "../../api/liberationApi";
|
} from "../../api/liberationApi";
|
||||||
import mergeRefs from "../../mergeRefs";
|
|
||||||
import { Icon } from "leaflet";
|
import { Icon } from "leaflet";
|
||||||
import { Marker as LMarker } from "leaflet";
|
import { Marker as LMarker } from "leaflet";
|
||||||
import icon from "leaflet/dist/images/marker-icon.png";
|
import icon from "leaflet/dist/images/marker-icon.png";
|
||||||
import iconShadow from "leaflet/dist/images/marker-shadow.png";
|
import iconShadow from "leaflet/dist/images/marker-shadow.png";
|
||||||
import {
|
import { MutableRefObject, useCallback, useEffect, useRef } from "react";
|
||||||
ForwardedRef,
|
|
||||||
MutableRefObject,
|
|
||||||
forwardRef,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
} from "react";
|
|
||||||
import { Marker, Tooltip, useMap, useMapEvent } from "react-leaflet";
|
import { Marker, Tooltip, useMap, useMapEvent } from "react-leaflet";
|
||||||
|
|
||||||
export const TOOLTIP_ZOOM_LEVEL = 9;
|
|
||||||
|
|
||||||
const WAYPOINT_ICON = new Icon({
|
const WAYPOINT_ICON = new Icon({
|
||||||
iconUrl: icon,
|
iconUrl: icon,
|
||||||
shadowUrl: iconShadow,
|
shadowUrl: iconShadow,
|
||||||
@@ -32,8 +22,7 @@ interface WaypointMarkerProps {
|
|||||||
flight: Flight;
|
flight: Flight;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WaypointMarker = forwardRef(
|
const WaypointMarker = (props: WaypointMarkerProps) => {
|
||||||
(props: WaypointMarkerProps, ref: ForwardedRef<LMarker>) => {
|
|
||||||
// Most props of react-leaflet types are immutable and components will not
|
// Most props of react-leaflet types are immutable and components will not
|
||||||
// update to account for changes, so we can't simply use the `permanent`
|
// update to account for changes, so we can't simply use the `permanent`
|
||||||
// property of the tooltip to control tooltip visibility based on the zoom
|
// property of the tooltip to control tooltip visibility based on the zoom
|
||||||
@@ -47,12 +36,12 @@ const WaypointMarker = forwardRef(
|
|||||||
// Instead, listen for zoom changes and rebind the tooltip when the zoom level
|
// Instead, listen for zoom changes and rebind the tooltip when the zoom level
|
||||||
// changes.
|
// changes.
|
||||||
const map = useMap();
|
const map = useMap();
|
||||||
const marker: MutableRefObject<LMarker | null> = useRef(null);
|
const marker: MutableRefObject<LMarker | undefined> = useRef();
|
||||||
|
|
||||||
const [putDestination] = useSetWaypointPositionMutation();
|
const [putDestination] = useSetWaypointPositionMutation();
|
||||||
|
|
||||||
const rebindTooltip = useCallback(() => {
|
const rebindTooltip = useCallback(() => {
|
||||||
if (marker.current === null) {
|
if (marker.current === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +50,7 @@ const WaypointMarker = forwardRef(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permanent = map.getZoom() >= TOOLTIP_ZOOM_LEVEL;
|
const permanent = map.getZoom() >= 9;
|
||||||
marker.current
|
marker.current
|
||||||
.unbindTooltip()
|
.unbindTooltip()
|
||||||
.bindTooltip(tooltip, { permanent: permanent });
|
.bindTooltip(tooltip, { permanent: permanent });
|
||||||
@@ -72,9 +61,7 @@ const WaypointMarker = forwardRef(
|
|||||||
const waypoint = props.waypoint;
|
const waypoint = props.waypoint;
|
||||||
marker.current?.setTooltipContent(
|
marker.current?.setTooltipContent(
|
||||||
`${props.number} ${waypoint.name}<br />` +
|
`${props.number} ${waypoint.name}<br />` +
|
||||||
`${waypoint.altitude_ft.toFixed()} ft ${
|
`${waypoint.altitude_ft.toFixed()} ft ${waypoint.altitude_reference}<br />` +
|
||||||
waypoint.altitude_reference
|
|
||||||
}<br />` +
|
|
||||||
waypoint.timing
|
waypoint.timing
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -104,12 +91,15 @@ const WaypointMarker = forwardRef(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
ref={mergeRefs(ref, marker)}
|
ref={(ref) => {
|
||||||
|
if (ref != null) {
|
||||||
|
marker.current = ref;
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip position={waypoint.position} />
|
<Tooltip position={waypoint.position} />
|
||||||
</Marker>
|
</Marker>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
);
|
|
||||||
|
|
||||||
export default WaypointMarker;
|
export default WaypointMarker;
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import mergeRefs from "./mergeRefs";
|
|
||||||
|
|
||||||
describe("mergeRefs", () => {
|
|
||||||
it("merges all kinds of refs", () => {
|
|
||||||
const referent = "foobar";
|
|
||||||
const ref = { current: null };
|
|
||||||
var callbackResult = null;
|
|
||||||
const callbackRef = (node: string | null) => {
|
|
||||||
if (node != null) {
|
|
||||||
callbackResult = node;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mergeRefs(ref, callbackRef)(referent);
|
|
||||||
expect(callbackResult).toEqual("foobar");
|
|
||||||
expect(ref.current).toEqual("foobar");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { ForwardedRef } from "react";
|
|
||||||
|
|
||||||
const mergeRefs = <T extends any>(...refs: ForwardedRef<T>[]) => {
|
|
||||||
return (node: T) => {
|
|
||||||
for (const ref of refs) {
|
|
||||||
if (ref == null) {
|
|
||||||
} else if (typeof ref === "function") {
|
|
||||||
ref(node);
|
|
||||||
} else {
|
|
||||||
ref.current = node;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default mergeRefs;
|
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
project = "DCS Liberation"
|
project = "DCS Liberation"
|
||||||
copyright = "2025, DCS Liberation Team"
|
copyright = "2023, DCS Liberation Team"
|
||||||
author = "DCS Liberation Team"
|
author = "DCS Liberation Team"
|
||||||
release = "14.0.0"
|
release = "8.1.0"
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|||||||
@@ -10,18 +10,19 @@ import yaml
|
|||||||
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
|
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType, VehicleType
|
||||||
|
|
||||||
from game.data.groups import GroupTask
|
from game.data.groups import GroupTask
|
||||||
|
from game.data.radar_db import UNITS_WITH_RADAR
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.dcs.helpers import static_type_from_name
|
from game.dcs.helpers import static_type_from_name
|
||||||
from game.dcs.shipunittype import ShipUnitType
|
from game.dcs.shipunittype import ShipUnitType
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
from game.layout import LAYOUTS
|
|
||||||
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
|
|
||||||
from game.point_with_heading import PointWithHeading
|
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
IadsGroundObject,
|
IadsGroundObject,
|
||||||
IadsBuildingGroundObject,
|
IadsBuildingGroundObject,
|
||||||
NavalGroundObject,
|
NavalGroundObject,
|
||||||
)
|
)
|
||||||
|
from game.layout import LAYOUTS
|
||||||
|
from game.layout.layout import TgoLayout, TgoLayoutUnitGroup
|
||||||
|
from game.point_with_heading import PointWithHeading
|
||||||
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
|
from game.theater.theatergroup import IadsGroundGroup, IadsRole, TheaterGroup
|
||||||
from game.utils import escape_string_for_lua
|
from game.utils import escape_string_for_lua
|
||||||
|
|
||||||
@@ -287,7 +288,7 @@ class ForceGroup:
|
|||||||
unit.id = game.next_unit_id()
|
unit.id = game.next_unit_id()
|
||||||
# Add unit name escaped so that we do not have scripting issues later
|
# Add unit name escaped so that we do not have scripting issues later
|
||||||
unit.name = escape_string_for_lua(
|
unit.name = escape_string_for_lua(
|
||||||
unit.unit_type.variant_id if unit.unit_type else unit.type.name
|
unit.unit_type.name if unit.unit_type else unit.type.name
|
||||||
)
|
)
|
||||||
unit.position = PointWithHeading.from_point(
|
unit.position = PointWithHeading.from_point(
|
||||||
ground_object.position + unit.position,
|
ground_object.position + unit.position,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""Objective adjacency lists."""
|
"""Objective adjacency lists."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Iterator
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, List, Optional, TYPE_CHECKING
|
from typing import Any, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from dcs.planes import C_101CC, C_101EB, Su_33
|
from dcs.planes import C_101CC, C_101EB, Su_33
|
||||||
|
|
||||||
from .flightmembers import FlightMembers
|
|
||||||
from .flightroster import FlightRoster
|
from .flightroster import FlightRoster
|
||||||
from .flightstate import FlightState, Navigating, Uninitialized
|
from .flightstate import FlightState, Navigating, Uninitialized
|
||||||
from .flightstate.killed import Killed
|
from .flightstate.killed import Killed
|
||||||
from .flighttype import FlightType
|
from .loadouts import Loadout
|
||||||
from ..sidc import (
|
from ..sidc import (
|
||||||
Entity,
|
Entity,
|
||||||
SidcDescribable,
|
SidcDescribable,
|
||||||
@@ -22,16 +20,14 @@ from ..sidc import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.callsigns.callsigngenerator import Callsign
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.sim.gameupdateevents import GameUpdateEvents
|
from game.sim.gameupdateevents import GameUpdateEvents
|
||||||
from game.sim.simulationresults import SimulationResults
|
from game.sim.simulationresults import SimulationResults
|
||||||
from game.squadrons import Squadron, Pilot
|
from game.squadrons import Squadron, Pilot
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from game.transfers import TransferOrder
|
from game.transfers import TransferOrder
|
||||||
from game.data.weapons import WeaponType
|
|
||||||
from .flightmember import FlightMember
|
|
||||||
from .flightplans.flightplan import FlightPlan
|
from .flightplans.flightplan import FlightPlan
|
||||||
|
from .flighttype import FlightType
|
||||||
from .flightwaypoint import FlightWaypoint
|
from .flightwaypoint import FlightWaypoint
|
||||||
from .package import Package
|
from .package import Package
|
||||||
from .starttype import StartType
|
from .starttype import StartType
|
||||||
@@ -50,30 +46,27 @@ class Flight(SidcDescribable):
|
|||||||
custom_name: Optional[str] = None,
|
custom_name: Optional[str] = None,
|
||||||
cargo: Optional[TransferOrder] = None,
|
cargo: Optional[TransferOrder] = None,
|
||||||
roster: Optional[FlightRoster] = None,
|
roster: Optional[FlightRoster] = None,
|
||||||
callsign: Optional[Callsign] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.id = uuid.uuid4()
|
self.id = uuid.uuid4()
|
||||||
self.package = package
|
self.package = package
|
||||||
self.country = country
|
self.country = country
|
||||||
self.coalition = squadron.coalition
|
self.coalition = squadron.coalition
|
||||||
self.squadron = squadron
|
self.squadron = squadron
|
||||||
self.flight_type = flight_type
|
|
||||||
if flight_type != FlightType.IDLE:
|
|
||||||
self.squadron.claim_inventory(count)
|
self.squadron.claim_inventory(count)
|
||||||
if roster is None:
|
if roster is None:
|
||||||
self.roster = FlightMembers(self, initial_size=count)
|
self.roster = FlightRoster(self.squadron, initial_size=count)
|
||||||
else:
|
else:
|
||||||
self.roster = FlightMembers.from_roster(self, roster)
|
self.roster = roster
|
||||||
self.divert = divert
|
self.divert = divert
|
||||||
|
self.flight_type = flight_type
|
||||||
|
self.loadout = Loadout.default_for(self)
|
||||||
self.start_type = start_type
|
self.start_type = start_type
|
||||||
|
self.use_custom_loadout = False
|
||||||
self.custom_name = custom_name
|
self.custom_name = custom_name
|
||||||
self.use_same_loadout_for_all_members = True
|
|
||||||
|
|
||||||
# Only used by transport missions.
|
# Only used by transport missions.
|
||||||
self.cargo = cargo
|
self.cargo = cargo
|
||||||
|
|
||||||
self.callsign = callsign
|
|
||||||
|
|
||||||
# Flight properties that can be set in the mission editor. This is used for
|
# Flight properties that can be set in the mission editor. This is used for
|
||||||
# things like HMD selection, ripple quantity, etc. Any values set here will take
|
# things like HMD selection, ripple quantity, etc. Any values set here will take
|
||||||
# the place of the defaults defined by DCS.
|
# the place of the defaults defined by DCS.
|
||||||
@@ -102,13 +95,19 @@ class Flight(SidcDescribable):
|
|||||||
|
|
||||||
self._flight_plan_builder = CustomBuilder(self, self.flight_plan.waypoints[1:])
|
self._flight_plan_builder = CustomBuilder(self, self.flight_plan.waypoints[1:])
|
||||||
self.recreate_flight_plan()
|
self.recreate_flight_plan()
|
||||||
# We need to clear the existing actions/options when moving the waypoints into
|
|
||||||
# the new flight plan because the actions/options that are currently set will be
|
def __getstate__(self) -> dict[str, Any]:
|
||||||
# the actions of whatever flight plan was previously used.
|
state = self.__dict__.copy()
|
||||||
# https://github.com/dcs-liberation/dcs_liberation/issues/3189
|
# Avoid persisting the flight state since that's not (currently) used outside
|
||||||
for waypoint in self.flight_plan.iter_waypoints():
|
# mission generation. This is a bit of a hack for the moment and in the future
|
||||||
waypoint.actions.clear()
|
# we will need to persist the flight state, but for now keep it out of save
|
||||||
waypoint.options.clear()
|
# compat (it also contains a generator that cannot be pickled).
|
||||||
|
del state["state"]
|
||||||
|
return state
|
||||||
|
|
||||||
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
|
state["state"] = Uninitialized(self, state["squadron"].settings)
|
||||||
|
self.__dict__.update(state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def blue(self) -> bool:
|
def blue(self) -> bool:
|
||||||
@@ -150,6 +149,10 @@ class Flight(SidcDescribable):
|
|||||||
def is_helo(self) -> bool:
|
def is_helo(self) -> bool:
|
||||||
return self.unit_type.dcs_unit_type.helicopter
|
return self.unit_type.dcs_unit_type.helicopter
|
||||||
|
|
||||||
|
@property
|
||||||
|
def from_cp(self) -> ControlPoint:
|
||||||
|
return self.departure
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def points(self) -> List[FlightWaypoint]:
|
def points(self) -> List[FlightWaypoint]:
|
||||||
return self.flight_plan.waypoints[1:]
|
return self.flight_plan.waypoints[1:]
|
||||||
@@ -168,9 +171,6 @@ class Flight(SidcDescribable):
|
|||||||
def missing_pilots(self) -> int:
|
def missing_pilots(self) -> int:
|
||||||
return self.roster.missing_pilots
|
return self.roster.missing_pilots
|
||||||
|
|
||||||
def iter_members(self) -> Iterator[FlightMember]:
|
|
||||||
yield from self.roster.members
|
|
||||||
|
|
||||||
def set_flight_type(self, var: FlightType) -> None:
|
def set_flight_type(self, var: FlightType) -> None:
|
||||||
self.flight_type = var
|
self.flight_type = var
|
||||||
|
|
||||||
@@ -196,11 +196,6 @@ class Flight(SidcDescribable):
|
|||||||
return unit_type.fuel_max * 0.5
|
return unit_type.fuel_max * 0.5
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def any_member_has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
|
||||||
return any(
|
|
||||||
m.loadout.has_weapon_of_type(weapon_type) for m in self.iter_members()
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
if self.custom_name:
|
if self.custom_name:
|
||||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||||
@@ -261,9 +256,9 @@ class Flight(SidcDescribable):
|
|||||||
Killed(self.state.estimate_position(), self, self.squadron.settings)
|
Killed(self.state.estimate_position(), self, self.squadron.settings)
|
||||||
)
|
)
|
||||||
events.update_flight(self)
|
events.update_flight(self)
|
||||||
for pilot in self.roster.iter_pilots():
|
for pilot in self.roster.pilots:
|
||||||
if pilot is not None:
|
if pilot is not None:
|
||||||
results.kill_pilot(self, pilot)
|
results.kill_pilot(self, pilot)
|
||||||
|
|
||||||
def recreate_flight_plan(self, dump_debug_info: bool = False) -> None:
|
def recreate_flight_plan(self) -> None:
|
||||||
self._flight_plan_builder.regenerate(dump_debug_info)
|
self._flight_plan_builder.regenerate()
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from game.ato.loadouts import Loadout
|
|
||||||
from game.lasercodes import LaserCode
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.squadrons import Pilot
|
|
||||||
|
|
||||||
|
|
||||||
class FlightMember:
|
|
||||||
def __init__(self, pilot: Pilot | None, loadout: Loadout) -> None:
|
|
||||||
self.pilot = pilot
|
|
||||||
self.loadout = loadout
|
|
||||||
self.use_custom_loadout = False
|
|
||||||
self.tgp_laser_code: LaserCode | None = None
|
|
||||||
self.weapon_laser_code: LaserCode | None = None
|
|
||||||
self.properties: dict[str, bool | float | int] = {}
|
|
||||||
|
|
||||||
def assign_tgp_laser_code(self, code: LaserCode) -> None:
|
|
||||||
if self.tgp_laser_code is not None:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"{self.pilot} already has already been assigned laser code "
|
|
||||||
f"{self.tgp_laser_code}"
|
|
||||||
)
|
|
||||||
self.tgp_laser_code = code
|
|
||||||
|
|
||||||
def release_tgp_laser_code(self) -> None:
|
|
||||||
if self.tgp_laser_code is None:
|
|
||||||
raise RuntimeError(f"{self.pilot} has no assigned laser code")
|
|
||||||
|
|
||||||
if self.weapon_laser_code == self.tgp_laser_code:
|
|
||||||
self.weapon_laser_code = None
|
|
||||||
self.tgp_laser_code.release()
|
|
||||||
self.tgp_laser_code = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_player(self) -> bool:
|
|
||||||
if self.pilot is None:
|
|
||||||
return False
|
|
||||||
return self.pilot.player
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Iterator
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
|
||||||
|
|
||||||
from .flightmember import FlightMember
|
|
||||||
from .flightroster import FlightRoster
|
|
||||||
from .iflightroster import IFlightRoster
|
|
||||||
from .loadouts import Loadout
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.squadrons import Pilot
|
|
||||||
from .flight import Flight
|
|
||||||
|
|
||||||
|
|
||||||
class FlightMembers(IFlightRoster):
|
|
||||||
def __init__(self, flight: Flight, initial_size: int = 0) -> None:
|
|
||||||
self.flight = flight
|
|
||||||
self.members: list[FlightMember] = []
|
|
||||||
self.resize(initial_size)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_roster(flight: Flight, roster: FlightRoster) -> FlightMembers:
|
|
||||||
members = FlightMembers(flight)
|
|
||||||
loadout = Loadout.default_for(flight)
|
|
||||||
members.members = [FlightMember(p, loadout) for p in roster.pilots]
|
|
||||||
return members
|
|
||||||
|
|
||||||
def iter_pilots(self) -> Iterator[Pilot | None]:
|
|
||||||
yield from (m.pilot for m in self.members)
|
|
||||||
|
|
||||||
def pilot_at(self, idx: int) -> Pilot | None:
|
|
||||||
return self.members[idx].pilot
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_size(self) -> int:
|
|
||||||
return len(self.members)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def player_count(self) -> int:
|
|
||||||
return len([m for m in self.members if m.is_player])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def missing_pilots(self) -> int:
|
|
||||||
return len([m for m in self.members if m.pilot is None])
|
|
||||||
|
|
||||||
def resize(self, new_size: int) -> None:
|
|
||||||
if self.max_size > new_size:
|
|
||||||
for member in self.members[new_size:]:
|
|
||||||
if (pilot := member.pilot) is not None:
|
|
||||||
self.flight.squadron.return_pilot(pilot)
|
|
||||||
if (code := member.tgp_laser_code) is not None:
|
|
||||||
code.release()
|
|
||||||
self.members = self.members[:new_size]
|
|
||||||
return
|
|
||||||
if self.max_size:
|
|
||||||
loadout = self.members[0].loadout.clone()
|
|
||||||
else:
|
|
||||||
loadout = Loadout.default_for(self.flight)
|
|
||||||
for _ in range(new_size - self.max_size):
|
|
||||||
member = FlightMember(self.flight.squadron.claim_available_pilot(), loadout)
|
|
||||||
member.use_custom_loadout = loadout.is_custom
|
|
||||||
self.members.append(member)
|
|
||||||
|
|
||||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
|
||||||
if pilot is not None:
|
|
||||||
self.flight.squadron.claim_pilot(pilot)
|
|
||||||
if (current_pilot := self.pilot_at(index)) is not None:
|
|
||||||
self.flight.squadron.return_pilot(current_pilot)
|
|
||||||
self.members[index].pilot = pilot
|
|
||||||
|
|
||||||
def remove_pilot(self, pilot: Pilot) -> None:
|
|
||||||
for i, member in enumerate(self.members):
|
|
||||||
if member.pilot is not None and member.pilot.name == pilot.name:
|
|
||||||
self.members.pop(i)
|
|
||||||
if (code := member.tgp_laser_code) is not None:
|
|
||||||
code.release()
|
|
||||||
return
|
|
||||||
raise ValueError(f"Pilot {pilot.name} not a member")
|
|
||||||
|
|
||||||
def clear(self) -> None:
|
|
||||||
self.flight.squadron.return_pilots(
|
|
||||||
[p for p in self.iter_pilots() if p is not None]
|
|
||||||
)
|
|
||||||
for member in self.members:
|
|
||||||
if (code := member.tgp_laser_code) is not None:
|
|
||||||
code.release()
|
|
||||||
|
|
||||||
def use_same_loadout_for_all_members(self) -> None:
|
|
||||||
if not self.members:
|
|
||||||
return
|
|
||||||
loadout = self.members[0].loadout
|
|
||||||
for member in self.members[1:]:
|
|
||||||
# Do not clone the loadout, we want any changes in the UI to be mirrored
|
|
||||||
# across all flight members.
|
|
||||||
member.loadout = loadout
|
|
||||||
|
|
||||||
def use_distinct_loadouts_for_each_member(self) -> None:
|
|
||||||
for member in self.members:
|
|
||||||
member.loadout = member.loadout.clone()
|
|
||||||
@@ -12,7 +12,7 @@ from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_m
|
|||||||
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
class AewcFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
||||||
@property
|
@property
|
||||||
def patrol_duration(self) -> timedelta:
|
def patrol_duration(self) -> timedelta:
|
||||||
return self.flight.coalition.doctrine.aewc.duration
|
return timedelta(hours=4)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
@@ -90,5 +90,5 @@ class Builder(IBuilder[AewcFlightPlan, PatrollingLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> AewcFlightPlan:
|
def build(self) -> AewcFlightPlan:
|
||||||
return AewcFlightPlan(self.flight, self.layout())
|
return AewcFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
|||||||
raise PlanningError("Air assault is only usable by helicopters")
|
raise PlanningError("Air assault is only usable by helicopters")
|
||||||
assert self.package.waypoints is not None
|
assert self.package.waypoints is not None
|
||||||
|
|
||||||
altitude = self.doctrine.helicopter.air_assault_nav_altitude
|
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
|
||||||
altitude_is_agl = self.flight.is_helo
|
altitude_is_agl = self.flight.is_helo
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
@@ -152,5 +152,5 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> AirAssaultFlightPlan:
|
def build(self) -> AirAssaultFlightPlan:
|
||||||
return AirAssaultFlightPlan(self.flight, self.layout())
|
return AirAssaultFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -155,5 +155,5 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> AirliftFlightPlan:
|
def build(self) -> AirliftFlightPlan:
|
||||||
return AirliftFlightPlan(self.flight, self.layout())
|
return AirliftFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ class Builder(FormationAttackBuilder[AntiShipFlightPlan, FormationAttackLayout])
|
|||||||
else:
|
else:
|
||||||
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
raise InvalidObjectiveLocation(self.flight.flight_type, location)
|
||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_ANTI_SHIP, targets)
|
return self._build(FlightWaypointType.INGRESS_BAI, targets)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
|
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> list[StrikeTarget]:
|
||||||
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
|
return [StrikeTarget(f"{g.group_name} at {tgo.name}", g) for g in tgo.groups]
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> AntiShipFlightPlan:
|
def build(self) -> AntiShipFlightPlan:
|
||||||
return AntiShipFlightPlan(self.flight, self.layout())
|
return AntiShipFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -39,5 +39,5 @@ class Builder(FormationAttackBuilder[BaiFlightPlan, FormationAttackLayout]):
|
|||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_BAI, targets)
|
return self._build(FlightWaypointType.INGRESS_BAI, targets)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> BaiFlightPlan:
|
def build(self) -> BaiFlightPlan:
|
||||||
return BaiFlightPlan(self.flight, self.layout())
|
return BaiFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_duration(self) -> timedelta:
|
def patrol_duration(self) -> timedelta:
|
||||||
return self.flight.coalition.doctrine.cap.duration
|
return self.flight.coalition.doctrine.cap_duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
@@ -29,7 +29,7 @@ class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def engagement_distance(self) -> Distance:
|
def engagement_distance(self) -> Distance:
|
||||||
return self.flight.coalition.doctrine.cap.engagement_range
|
return self.flight.coalition.doctrine.cap_engagement_range
|
||||||
|
|
||||||
|
|
||||||
class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
||||||
@@ -44,8 +44,8 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
|||||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||||
patrol_alt = max(
|
patrol_alt = max(
|
||||||
self.doctrine.cap.min_patrol_altitude,
|
self.doctrine.min_patrol_altitude,
|
||||||
min(self.doctrine.cap.max_patrol_altitude, randomized_alt),
|
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
@@ -66,5 +66,5 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> BarCapFlightPlan:
|
def build(self) -> BarCapFlightPlan:
|
||||||
return BarCapFlightPlan(self.flight, self.layout())
|
return BarCapFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
|
||||||
import random
|
import random
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Any, TYPE_CHECKING, TypeVar
|
from typing import Any, TYPE_CHECKING, TypeVar
|
||||||
@@ -27,9 +26,6 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
self, location: MissionTarget, barcap: bool
|
self, location: MissionTarget, barcap: bool
|
||||||
) -> tuple[Point, Point]:
|
) -> tuple[Point, Point]:
|
||||||
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||||
closest_friendly_field = (
|
|
||||||
None # keep track of closest frieldly airfield in case we need it
|
|
||||||
)
|
|
||||||
for airfield in closest_cache.operational_airfields:
|
for airfield in closest_cache.operational_airfields:
|
||||||
# If the mission is a BARCAP of an enemy airfield, find the *next*
|
# If the mission is a BARCAP of an enemy airfield, find the *next*
|
||||||
# closest enemy airfield.
|
# closest enemy airfield.
|
||||||
@@ -38,43 +34,8 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
if airfield.captured != self.is_player:
|
if airfield.captured != self.is_player:
|
||||||
closest_airfield = airfield
|
closest_airfield = airfield
|
||||||
break
|
break
|
||||||
elif closest_friendly_field is None:
|
|
||||||
closest_friendly_field = airfield
|
|
||||||
else:
|
else:
|
||||||
if barcap:
|
|
||||||
# If planning a BARCAP, we should be able to find at least one enemy
|
|
||||||
# airfield. If we can't, it's an error.
|
|
||||||
raise PlanningError("Could not find any enemy airfields")
|
raise PlanningError("Could not find any enemy airfields")
|
||||||
else:
|
|
||||||
# if we cannot find any friendly or enemy airfields other than the target,
|
|
||||||
# there's nothing we can do
|
|
||||||
if closest_friendly_field is None:
|
|
||||||
raise PlanningError(
|
|
||||||
"Could not find any enemy or friendly airfields"
|
|
||||||
)
|
|
||||||
|
|
||||||
# If planning other race tracks (TARCAPs, currently), the target may be
|
|
||||||
# the only enemy airfield. In this case, set the race track orientation using
|
|
||||||
# a virtual point equi-distant from but opposite to the target from the closest
|
|
||||||
# friendly airfield like below, where F is the closest friendly airfield, T is
|
|
||||||
# the sole enemy airfield and V the virtual point
|
|
||||||
#
|
|
||||||
# F ---- T ----- V
|
|
||||||
#
|
|
||||||
# We need to create this virtual point, rather than using F to make sure
|
|
||||||
# the race track is aligned towards the target.
|
|
||||||
closest_friendly_field_position = copy.deepcopy(
|
|
||||||
closest_friendly_field.position
|
|
||||||
)
|
|
||||||
closest_airfield = closest_friendly_field
|
|
||||||
closest_airfield.position.x = (
|
|
||||||
2 * self.package.target.position.x
|
|
||||||
- closest_friendly_field_position.x
|
|
||||||
)
|
|
||||||
closest_airfield.position.y = (
|
|
||||||
2 * self.package.target.position.y
|
|
||||||
- closest_friendly_field_position.y
|
|
||||||
)
|
|
||||||
|
|
||||||
heading = Heading.from_degrees(
|
heading = Heading.from_degrees(
|
||||||
location.position.heading_between_point(closest_airfield.position)
|
location.position.heading_between_point(closest_airfield.position)
|
||||||
@@ -90,10 +51,10 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
# buffer.
|
# buffer.
|
||||||
distance_to_no_fly = (
|
distance_to_no_fly = (
|
||||||
meters(position.distance(self.threat_zones.all))
|
meters(position.distance(self.threat_zones.all))
|
||||||
- self.doctrine.cap.engagement_range
|
- self.doctrine.cap_engagement_range
|
||||||
- nautical_miles(5)
|
- nautical_miles(5)
|
||||||
)
|
)
|
||||||
max_track_length = self.doctrine.cap.max_track_length
|
max_track_length = self.doctrine.cap_max_track_length
|
||||||
else:
|
else:
|
||||||
# Other race tracks (TARCAPs, currently) just try to keep some
|
# Other race tracks (TARCAPs, currently) just try to keep some
|
||||||
# distance from the nearest enemy airbase, but since they are by
|
# distance from the nearest enemy airbase, but since they are by
|
||||||
@@ -108,15 +69,15 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||||
|
|
||||||
# TARCAPs fly short racetracks because they need to react faster.
|
# TARCAPs fly short racetracks because they need to react faster.
|
||||||
max_track_length = self.doctrine.cap.min_track_length + 0.3 * (
|
max_track_length = self.doctrine.cap_min_track_length + 0.3 * (
|
||||||
self.doctrine.cap.max_track_length - self.doctrine.cap.min_track_length
|
self.doctrine.cap_max_track_length - self.doctrine.cap_min_track_length
|
||||||
)
|
)
|
||||||
|
|
||||||
min_cap_distance = min(
|
min_cap_distance = min(
|
||||||
self.doctrine.cap.min_distance_from_cp, distance_to_no_fly
|
self.doctrine.cap_min_distance_from_cp, distance_to_no_fly
|
||||||
)
|
)
|
||||||
max_cap_distance = min(
|
max_cap_distance = min(
|
||||||
self.doctrine.cap.max_distance_from_cp, distance_to_no_fly
|
self.doctrine.cap_max_distance_from_cp, distance_to_no_fly
|
||||||
)
|
)
|
||||||
|
|
||||||
end = location.position.point_from_heading(
|
end = location.position.point_from_heading(
|
||||||
@@ -125,7 +86,7 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
track_length = random.randint(
|
track_length = random.randint(
|
||||||
int(self.doctrine.cap.min_track_length.meters),
|
int(self.doctrine.cap_min_track_length.meters),
|
||||||
int(max_track_length.meters),
|
int(max_track_length.meters),
|
||||||
)
|
)
|
||||||
start = end.point_from_heading(heading.opposite.degrees, track_length)
|
start = end.point_from_heading(heading.opposite.degrees, track_length)
|
||||||
|
|||||||
@@ -6,15 +6,13 @@ from datetime import timedelta
|
|||||||
from typing import TYPE_CHECKING, Type
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
from game.theater import FrontLine
|
from game.theater import FrontLine
|
||||||
from game.utils import Distance, Speed, kph, meters, dcs_to_shapely_point
|
from game.utils import Distance, Speed, kph, meters
|
||||||
from .ibuilder import IBuilder
|
from .ibuilder import IBuilder
|
||||||
from .invalidobjectivelocation import InvalidObjectiveLocation
|
from .invalidobjectivelocation import InvalidObjectiveLocation
|
||||||
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
from .patrolling import PatrollingFlightPlan, PatrollingLayout
|
||||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
from ..flightwaypointtype import FlightWaypointType
|
from ..flightwaypointtype import FlightWaypointType
|
||||||
from ...flightplan.ipsolver import IpSolver
|
|
||||||
from ...persistence.paths import waypoint_debug_directory
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
@@ -22,13 +20,13 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CasLayout(PatrollingLayout):
|
class CasLayout(PatrollingLayout):
|
||||||
ingress: FlightWaypoint
|
target: FlightWaypoint
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
yield self.departure
|
yield self.departure
|
||||||
yield from self.nav_to
|
yield from self.nav_to
|
||||||
yield self.ingress
|
|
||||||
yield self.patrol_start
|
yield self.patrol_start
|
||||||
|
yield self.target
|
||||||
yield self.patrol_end
|
yield self.patrol_end
|
||||||
yield from self.nav_from
|
yield from self.nav_from
|
||||||
yield self.arrival
|
yield self.arrival
|
||||||
@@ -44,7 +42,7 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_duration(self) -> timedelta:
|
def patrol_duration(self) -> timedelta:
|
||||||
return self.flight.coalition.doctrine.cas.duration
|
return self.flight.coalition.doctrine.cas_duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
@@ -61,20 +59,23 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||||
return {self.layout.ingress, self.layout.patrol_start, self.layout.patrol_end}
|
return {self.layout.patrol_start, self.layout.target, self.layout.patrol_end}
|
||||||
|
|
||||||
|
def request_escort_at(self) -> FlightWaypoint | None:
|
||||||
|
return self.layout.patrol_start
|
||||||
|
|
||||||
|
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||||
|
return self.layout.patrol_end
|
||||||
|
|
||||||
def ui_zone(self) -> UiZone:
|
def ui_zone(self) -> UiZone:
|
||||||
midpoint = (
|
|
||||||
self.layout.patrol_start.position + self.layout.patrol_end.position
|
|
||||||
) / 2
|
|
||||||
return UiZone(
|
return UiZone(
|
||||||
[midpoint],
|
[self.layout.target.position],
|
||||||
self.engagement_distance,
|
self.engagement_distance,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
||||||
def layout(self, dump_debug_info: bool) -> CasLayout:
|
def layout(self) -> CasLayout:
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
if not isinstance(location, FrontLine):
|
if not isinstance(location, FrontLine):
|
||||||
@@ -85,79 +86,46 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
bounds = FrontLineConflictDescription.frontline_bounds(location, self.theater)
|
bounds = FrontLineConflictDescription.frontline_bounds(location, self.theater)
|
||||||
patrol_start = bounds.left_position
|
ingress = bounds.left_position
|
||||||
patrol_end = bounds.right_position
|
center = bounds.center
|
||||||
|
egress = bounds.right_position
|
||||||
|
|
||||||
start_distance = patrol_start.distance_to_point(self.flight.departure.position)
|
ingress_distance = ingress.distance_to_point(self.flight.departure.position)
|
||||||
end_distance = patrol_end.distance_to_point(self.flight.departure.position)
|
egress_distance = egress.distance_to_point(self.flight.departure.position)
|
||||||
if end_distance < start_distance:
|
if egress_distance < ingress_distance:
|
||||||
patrol_start, patrol_end = patrol_end, patrol_start
|
ingress, egress = egress, ingress
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
|
||||||
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
|
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
|
||||||
patrol_altitude = self.doctrine.resolve_combat_altitude(is_helo)
|
ingress_egress_altitude = (
|
||||||
use_agl_patrol_altitude = is_helo
|
self.doctrine.ingress_altitude if not is_helo else meters(50)
|
||||||
|
|
||||||
ip_solver = IpSolver(
|
|
||||||
dcs_to_shapely_point(self.flight.departure.position),
|
|
||||||
dcs_to_shapely_point(patrol_start),
|
|
||||||
self.doctrine,
|
|
||||||
self.threat_zones.all,
|
|
||||||
)
|
)
|
||||||
ip_solver.set_debug_properties(
|
use_agl_ingress_egress = is_helo
|
||||||
waypoint_debug_directory() / "IP", self.theater.terrain
|
|
||||||
)
|
|
||||||
ingress_point_shapely = ip_solver.solve()
|
|
||||||
if dump_debug_info:
|
|
||||||
ip_solver.dump_debug_info()
|
|
||||||
|
|
||||||
ingress_point = patrol_start.new_in_same_map(
|
|
||||||
ingress_point_shapely.x, ingress_point_shapely.y
|
|
||||||
)
|
|
||||||
|
|
||||||
patrol_start_waypoint = builder.nav(
|
|
||||||
patrol_start, patrol_altitude, use_agl_patrol_altitude
|
|
||||||
)
|
|
||||||
patrol_start_waypoint.name = "FLOT START"
|
|
||||||
patrol_start_waypoint.pretty_name = "FLOT start"
|
|
||||||
patrol_start_waypoint.description = "FLOT boundary"
|
|
||||||
patrol_start_waypoint.wants_escort = True
|
|
||||||
|
|
||||||
patrol_end_waypoint = builder.nav(
|
|
||||||
patrol_end, patrol_altitude, use_agl_patrol_altitude
|
|
||||||
)
|
|
||||||
patrol_end_waypoint.name = "FLOT END"
|
|
||||||
patrol_end_waypoint.pretty_name = "FLOT end"
|
|
||||||
patrol_end_waypoint.description = "FLOT boundary"
|
|
||||||
patrol_end_waypoint.wants_escort = True
|
|
||||||
|
|
||||||
ingress = builder.ingress(
|
|
||||||
FlightWaypointType.INGRESS_CAS, ingress_point, location
|
|
||||||
)
|
|
||||||
ingress.description = f"Ingress to provide CAS at {location}"
|
|
||||||
|
|
||||||
return CasLayout(
|
return CasLayout(
|
||||||
departure=builder.takeoff(self.flight.departure),
|
departure=builder.takeoff(self.flight.departure),
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
self.flight.departure.position,
|
self.flight.departure.position,
|
||||||
ingress_point,
|
ingress,
|
||||||
patrol_altitude,
|
ingress_egress_altitude,
|
||||||
use_agl_patrol_altitude,
|
use_agl_ingress_egress,
|
||||||
),
|
),
|
||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
patrol_end,
|
egress,
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
patrol_altitude,
|
ingress_egress_altitude,
|
||||||
use_agl_patrol_altitude,
|
use_agl_ingress_egress,
|
||||||
),
|
),
|
||||||
ingress=ingress,
|
patrol_start=builder.ingress(
|
||||||
patrol_start=patrol_start_waypoint,
|
FlightWaypointType.INGRESS_CAS, ingress, location
|
||||||
patrol_end=patrol_end_waypoint,
|
),
|
||||||
|
target=builder.cas(center),
|
||||||
|
patrol_end=builder.egress(egress, location),
|
||||||
arrival=builder.land(self.flight.arrival),
|
arrival=builder.land(self.flight.arrival),
|
||||||
divert=builder.divert(self.flight.divert),
|
divert=builder.divert(self.flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> CasFlightPlan:
|
def build(self) -> CasFlightPlan:
|
||||||
return CasFlightPlan(self.flight, self.layout(dump_debug_info))
|
return CasFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -72,5 +72,5 @@ class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
|
|||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints)
|
return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> CustomFlightPlan:
|
def build(self) -> CustomFlightPlan:
|
||||||
return CustomFlightPlan(self.flight, self.layout())
|
return CustomFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -37,5 +37,5 @@ class Builder(FormationAttackBuilder[DeadFlightPlan, FormationAttackLayout]):
|
|||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_DEAD)
|
return self._build(FlightWaypointType.INGRESS_DEAD)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> DeadFlightPlan:
|
def build(self) -> DeadFlightPlan:
|
||||||
return DeadFlightPlan(self.flight, self.layout())
|
return DeadFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
|||||||
departure=builder.takeoff(self.flight.departure),
|
departure=builder.takeoff(self.flight.departure),
|
||||||
hold=hold,
|
hold=hold,
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
hold.position, join.position, self.doctrine.combat_altitude
|
hold.position, join.position, self.doctrine.ingress_altitude
|
||||||
),
|
),
|
||||||
join=join,
|
join=join,
|
||||||
ingress=ingress,
|
ingress=ingress,
|
||||||
@@ -43,12 +43,12 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
|||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
refuel.position,
|
refuel.position,
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
self.doctrine.combat_altitude,
|
self.doctrine.ingress_altitude,
|
||||||
),
|
),
|
||||||
arrival=builder.land(self.flight.arrival),
|
arrival=builder.land(self.flight.arrival),
|
||||||
divert=builder.divert(self.flight.divert),
|
divert=builder.divert(self.flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> EscortFlightPlan:
|
def build(self) -> EscortFlightPlan:
|
||||||
return EscortFlightPlan(self.flight, self.layout())
|
return EscortFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -83,5 +83,5 @@ class Builder(IBuilder[FerryFlightPlan, FerryLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> FerryFlightPlan:
|
def build(self) -> FerryFlightPlan:
|
||||||
return FerryFlightPlan(self.flight, self.layout())
|
return FerryFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ MissionPlanner. Those only plan basic information like the objective, aircraft
|
|||||||
type, and the size of the flight. The FlightPlanBuilder is responsible for
|
type, and the size of the flight. The FlightPlanBuilder is responsible for
|
||||||
generating the waypoints for the mission.
|
generating the waypoints for the mission.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import math
|
import math
|
||||||
@@ -13,6 +12,7 @@ from abc import ABC, abstractmethod
|
|||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from functools import cached_property
|
||||||
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
||||||
|
|
||||||
from game.typeguard import self_type_guard
|
from game.typeguard import self_type_guard
|
||||||
@@ -20,9 +20,11 @@ from game.utils import Distance, Speed, meters
|
|||||||
from .planningerror import PlanningError
|
from .planningerror import PlanningError
|
||||||
from ..flightwaypointtype import FlightWaypointType
|
from ..flightwaypointtype import FlightWaypointType
|
||||||
from ..starttype import StartType
|
from ..starttype import StartType
|
||||||
from ..traveltime import GroundSpeed
|
from ..traveltime import GroundSpeed, TravelTime
|
||||||
|
from ...savecompat import has_save_compat_for
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from game.dcs.aircrafttype import FuelConsumption
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from ..flight import Flight
|
from ..flight import Flight
|
||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
@@ -31,6 +33,14 @@ if TYPE_CHECKING:
|
|||||||
from .loiter import LoiterFlightPlan
|
from .loiter import LoiterFlightPlan
|
||||||
from .patrolling import PatrollingFlightPlan
|
from .patrolling import PatrollingFlightPlan
|
||||||
|
|
||||||
|
INGRESS_TYPES = {
|
||||||
|
FlightWaypointType.INGRESS_CAS,
|
||||||
|
FlightWaypointType.INGRESS_ESCORT,
|
||||||
|
FlightWaypointType.INGRESS_SEAD,
|
||||||
|
FlightWaypointType.INGRESS_STRIKE,
|
||||||
|
FlightWaypointType.INGRESS_DEAD,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Layout(ABC):
|
class Layout(ABC):
|
||||||
@@ -55,6 +65,12 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
self.layout = layout
|
self.layout = layout
|
||||||
self.tot_offset = self.default_tot_offset()
|
self.tot_offset = self.default_tot_offset()
|
||||||
|
|
||||||
|
@has_save_compat_for(7)
|
||||||
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
|
if "tot_offset" not in state:
|
||||||
|
state["tot_offset"] = self.default_tot_offset()
|
||||||
|
self.__dict__.update(state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def package(self) -> Package:
|
def package(self) -> Package:
|
||||||
return self.flight.package
|
return self.flight.package
|
||||||
@@ -144,6 +160,39 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
def tot(self) -> datetime:
|
def tot(self) -> datetime:
|
||||||
return self.package.time_over_target + self.tot_offset
|
return self.package.time_over_target + self.tot_offset
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def bingo_fuel(self) -> int:
|
||||||
|
"""Bingo fuel value for the FlightPlan"""
|
||||||
|
if (fuel := self.flight.unit_type.fuel_consumption) is not None:
|
||||||
|
return self._bingo_estimate(fuel)
|
||||||
|
return self._legacy_bingo_estimate()
|
||||||
|
|
||||||
|
def _bingo_estimate(self, fuel: FuelConsumption) -> int:
|
||||||
|
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||||
|
fuel_consumed = fuel.cruise * distance_to_arrival.nautical_miles
|
||||||
|
bingo = fuel_consumed + fuel.min_safe
|
||||||
|
return math.ceil(bingo / 100) * 100
|
||||||
|
|
||||||
|
def _legacy_bingo_estimate(self) -> int:
|
||||||
|
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||||
|
|
||||||
|
bingo = 1000.0 # Minimum Emergency Fuel
|
||||||
|
bingo += 500 # Visual Traffic
|
||||||
|
bingo += 15 * distance_to_arrival.nautical_miles
|
||||||
|
|
||||||
|
# TODO: Per aircraft tweaks.
|
||||||
|
|
||||||
|
if self.flight.divert is not None:
|
||||||
|
max_divert_distance = self.max_distance_from(self.flight.divert)
|
||||||
|
bingo += 10 * max_divert_distance.nautical_miles
|
||||||
|
|
||||||
|
return round(bingo / 100) * 100
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def joker_fuel(self) -> int:
|
||||||
|
"""Joker fuel value for the FlightPlan"""
|
||||||
|
return self.bingo_fuel + 1000
|
||||||
|
|
||||||
def max_distance_from(self, cp: ControlPoint) -> Distance:
|
def max_distance_from(self, cp: ControlPoint) -> Distance:
|
||||||
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
|
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
|
||||||
:arg cp The ControlPoint to measure distance from.
|
:arg cp The ControlPoint to measure distance from.
|
||||||
@@ -172,7 +221,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for previous_waypoint, waypoint in self.edges(until=destination):
|
for previous_waypoint, waypoint in self.edges(until=destination):
|
||||||
total += self.total_time_between_waypoints(previous_waypoint, waypoint)
|
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
|
||||||
|
|
||||||
# Trim microseconds. Our simulation tick rate is 1 second, so anything that
|
# Trim microseconds. Our simulation tick rate is 1 second, so anything that
|
||||||
# takes 100.1 or 100.9 seconds will take 100 seconds. DCS doesn't handle
|
# takes 100.1 or 100.9 seconds will take 100 seconds. DCS doesn't handle
|
||||||
@@ -181,23 +230,12 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
# model.
|
# model.
|
||||||
return timedelta(seconds=math.floor(total.total_seconds()))
|
return timedelta(seconds=math.floor(total.total_seconds()))
|
||||||
|
|
||||||
def total_time_between_waypoints(
|
|
||||||
self, a: FlightWaypoint, b: FlightWaypoint
|
|
||||||
) -> timedelta:
|
|
||||||
"""Returns the total time spent between a and b.
|
|
||||||
|
|
||||||
The total time between waypoints differs from the travel time in that it may
|
|
||||||
include additional time for actions such as loitering.
|
|
||||||
"""
|
|
||||||
return self.travel_time_between_waypoints(a, b)
|
|
||||||
|
|
||||||
def travel_time_between_waypoints(
|
def travel_time_between_waypoints(
|
||||||
self, a: FlightWaypoint, b: FlightWaypoint
|
self, a: FlightWaypoint, b: FlightWaypoint
|
||||||
) -> timedelta:
|
) -> timedelta:
|
||||||
error_factor = 1.05
|
return TravelTime.between_points(
|
||||||
speed = self.speed_between_waypoints(a, b)
|
a.position, b.position, self.speed_between_waypoints(a, b)
|
||||||
distance = meters(a.position.distance_to_point(b.position))
|
)
|
||||||
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@@ -206,21 +244,24 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def request_escort_at(self) -> FlightWaypoint | None:
|
def request_escort_at(self) -> FlightWaypoint | None:
|
||||||
try:
|
|
||||||
return next(self.escorted_waypoints())
|
|
||||||
except StopIteration:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||||
try:
|
|
||||||
return list(self.escorted_waypoints())[-1]
|
|
||||||
except IndexError:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def escorted_waypoints(self) -> Iterator[FlightWaypoint]:
|
def escorted_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
for waypoint in self.iter_waypoints():
|
begin = self.request_escort_at()
|
||||||
if waypoint.wants_escort:
|
end = self.dismiss_escort_at()
|
||||||
|
if begin is None or end is None:
|
||||||
|
return
|
||||||
|
escorting = False
|
||||||
|
for waypoint in self.waypoints:
|
||||||
|
if waypoint == begin:
|
||||||
|
escorting = True
|
||||||
|
if escorting:
|
||||||
yield waypoint
|
yield waypoint
|
||||||
|
if waypoint == end:
|
||||||
|
return
|
||||||
|
|
||||||
def takeoff_time(self) -> datetime:
|
def takeoff_time(self) -> datetime:
|
||||||
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
|
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
|
||||||
@@ -249,7 +290,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
def estimate_ground_ops(self) -> timedelta:
|
def estimate_ground_ops(self) -> timedelta:
|
||||||
if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}:
|
if self.flight.start_type in {StartType.RUNWAY, StartType.IN_FLIGHT}:
|
||||||
return timedelta()
|
return timedelta()
|
||||||
if self.flight.departure.is_fleet:
|
if self.flight.from_cp.is_fleet:
|
||||||
return timedelta(minutes=2)
|
return timedelta(minutes=2)
|
||||||
else:
|
else:
|
||||||
return timedelta(minutes=8)
|
return timedelta(minutes=8)
|
||||||
@@ -270,9 +311,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@self_type_guard
|
@self_type_guard
|
||||||
def is_loiter(
|
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
|
||||||
self, flight_plan: FlightPlan[Any]
|
|
||||||
) -> TypeGuard[LoiterFlightPlan[Any]]:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@self_type_guard
|
@self_type_guard
|
||||||
@@ -284,8 +323,5 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
|||||||
@self_type_guard
|
@self_type_guard
|
||||||
def is_formation(
|
def is_formation(
|
||||||
self, flight_plan: FlightPlan[Any]
|
self, flight_plan: FlightPlan[Any]
|
||||||
) -> TypeGuard[FormationFlightPlan[Any]]:
|
) -> TypeGuard[FormationFlightPlan]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add_waypoint_actions(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ class FlightPlanBuilderTypes:
|
|||||||
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
|
FlightType.TRANSPORT: AirliftFlightPlan.builder_type(),
|
||||||
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
FlightType.FERRY: FerryFlightPlan.builder_type(),
|
||||||
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
FlightType.AIR_ASSAULT: AirAssaultFlightPlan.builder_type(),
|
||||||
FlightType.IDLE: BarCapFlightPlan.builder_type(),
|
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
return builder_dict[flight.flight_type]
|
return builder_dict[flight.flight_type]
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ from abc import ABC, abstractmethod
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
|
from typing import Any, TYPE_CHECKING, TypeGuard
|
||||||
|
|
||||||
from game.typeguard import self_type_guard
|
from game.typeguard import self_type_guard
|
||||||
from game.utils import Speed
|
from game.utils import Speed
|
||||||
from .flightplan import FlightPlan
|
from .flightplan import FlightPlan
|
||||||
from .loiter import LoiterFlightPlan, LoiterLayout
|
from .loiter import LoiterFlightPlan, LoiterLayout
|
||||||
|
from ..traveltime import GroundSpeed, TravelTime
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
@@ -24,18 +25,22 @@ class FormationLayout(LoiterLayout, ABC):
|
|||||||
nav_from: list[FlightWaypoint]
|
nav_from: list[FlightWaypoint]
|
||||||
|
|
||||||
|
|
||||||
LayoutT = TypeVar("LayoutT", bound=FormationLayout)
|
class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||||
|
|
||||||
|
|
||||||
class FormationFlightPlan(LoiterFlightPlan[LayoutT], ABC):
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def package_speed_waypoints(self) -> set[FlightWaypoint]: ...
|
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||||
return self.package_speed_waypoints
|
return self.package_speed_waypoints
|
||||||
|
|
||||||
|
def request_escort_at(self) -> FlightWaypoint | None:
|
||||||
|
return self.layout.join
|
||||||
|
|
||||||
|
def dismiss_escort_at(self) -> FlightWaypoint | None:
|
||||||
|
return self.layout.split
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def best_flight_formation_speed(self) -> Speed:
|
def best_flight_formation_speed(self) -> Speed:
|
||||||
"""The best speed this flight is capable at all formation waypoints.
|
"""The best speed this flight is capable at all formation waypoints.
|
||||||
@@ -68,11 +73,13 @@ class FormationFlightPlan(LoiterFlightPlan[LayoutT], ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def join_time(self) -> datetime: ...
|
def join_time(self) -> datetime:
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def split_time(self) -> datetime: ...
|
def split_time(self) -> datetime:
|
||||||
|
...
|
||||||
|
|
||||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.join:
|
if waypoint == self.layout.join:
|
||||||
@@ -83,8 +90,10 @@ class FormationFlightPlan(LoiterFlightPlan[LayoutT], ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def push_time(self) -> datetime:
|
def push_time(self) -> datetime:
|
||||||
return self.join_time - self.travel_time_between_waypoints(
|
return self.join_time - TravelTime.between_points(
|
||||||
self.layout.hold, self.layout.join
|
self.layout.hold.position,
|
||||||
|
self.layout.join.position,
|
||||||
|
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -98,5 +107,5 @@ class FormationFlightPlan(LoiterFlightPlan[LayoutT], ABC):
|
|||||||
@self_type_guard
|
@self_type_guard
|
||||||
def is_formation(
|
def is_formation(
|
||||||
self, flight_plan: FlightPlan[Any]
|
self, flight_plan: FlightPlan[Any]
|
||||||
) -> TypeGuard[FormationFlightPlan[Any]]:
|
) -> TypeGuard[FormationFlightPlan]:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from game.utils import Speed, meters
|
|||||||
from .flightplan import FlightPlan
|
from .flightplan import FlightPlan
|
||||||
from .formation import FormationFlightPlan, FormationLayout
|
from .formation import FormationFlightPlan, FormationLayout
|
||||||
from .ibuilder import IBuilder
|
from .ibuilder import IBuilder
|
||||||
|
from .planningerror import PlanningError
|
||||||
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
||||||
from .. import FlightType
|
from .. import FlightType
|
||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
@@ -23,29 +24,7 @@ if TYPE_CHECKING:
|
|||||||
from ..flight import Flight
|
from ..flight import Flight
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||||
class FormationAttackLayout(FormationLayout):
|
|
||||||
ingress: FlightWaypoint
|
|
||||||
targets: list[FlightWaypoint]
|
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
|
||||||
yield self.departure
|
|
||||||
yield self.hold
|
|
||||||
yield from self.nav_to
|
|
||||||
yield self.join
|
|
||||||
yield self.ingress
|
|
||||||
yield from self.targets
|
|
||||||
yield self.split
|
|
||||||
if self.refuel is not None:
|
|
||||||
yield self.refuel
|
|
||||||
yield from self.nav_from
|
|
||||||
yield self.arrival
|
|
||||||
if self.divert is not None:
|
|
||||||
yield self.divert
|
|
||||||
yield self.bullseye
|
|
||||||
|
|
||||||
|
|
||||||
class FormationAttackFlightPlan(FormationFlightPlan[FormationAttackLayout], ABC):
|
|
||||||
@property
|
@property
|
||||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||||
return {
|
return {
|
||||||
@@ -77,19 +56,42 @@ class FormationAttackFlightPlan(FormationFlightPlan[FormationAttackLayout], ABC)
|
|||||||
"RADIO",
|
"RADIO",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def travel_time_to_target(self) -> timedelta:
|
||||||
|
"""The estimated time between the first waypoint and the target."""
|
||||||
|
destination = self.tot_waypoint
|
||||||
|
total = timedelta()
|
||||||
|
for previous_waypoint, waypoint in self.edges():
|
||||||
|
if waypoint == self.tot_waypoint:
|
||||||
|
# For anything strike-like the TOT waypoint is the *flight's*
|
||||||
|
# mission target, but to synchronize with the rest of the
|
||||||
|
# package we need to use the travel time to the same position as
|
||||||
|
# the others.
|
||||||
|
total += self.travel_time_between_waypoints(
|
||||||
|
previous_waypoint, self.target_area_waypoint
|
||||||
|
)
|
||||||
|
break
|
||||||
|
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
|
||||||
|
else:
|
||||||
|
raise PlanningError(
|
||||||
|
f"Did not find destination waypoint {destination} in "
|
||||||
|
f"waypoints for {self.flight}"
|
||||||
|
)
|
||||||
|
return total
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def join_time(self) -> datetime:
|
def join_time(self) -> datetime:
|
||||||
travel_time = self.total_time_between_waypoints(
|
travel_time = self.travel_time_between_waypoints(
|
||||||
self.layout.join, self.layout.ingress
|
self.layout.join, self.layout.ingress
|
||||||
)
|
)
|
||||||
return self.ingress_time - travel_time
|
return self.ingress_time - travel_time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def split_time(self) -> datetime:
|
def split_time(self) -> datetime:
|
||||||
travel_time_ingress = self.total_time_between_waypoints(
|
travel_time_ingress = self.travel_time_between_waypoints(
|
||||||
self.layout.ingress, self.target_area_waypoint
|
self.layout.ingress, self.target_area_waypoint
|
||||||
)
|
)
|
||||||
travel_time_egress = self.total_time_between_waypoints(
|
travel_time_egress = self.travel_time_between_waypoints(
|
||||||
self.target_area_waypoint, self.layout.split
|
self.target_area_waypoint, self.layout.split
|
||||||
)
|
)
|
||||||
minutes_at_target = 0.75 * len(self.layout.targets)
|
minutes_at_target = 0.75 * len(self.layout.targets)
|
||||||
@@ -104,7 +106,7 @@ class FormationAttackFlightPlan(FormationFlightPlan[FormationAttackLayout], ABC)
|
|||||||
@property
|
@property
|
||||||
def ingress_time(self) -> datetime:
|
def ingress_time(self) -> datetime:
|
||||||
tot = self.tot
|
tot = self.tot
|
||||||
travel_time = self.total_time_between_waypoints(
|
travel_time = self.travel_time_between_waypoints(
|
||||||
self.layout.ingress, self.target_area_waypoint
|
self.layout.ingress, self.target_area_waypoint
|
||||||
)
|
)
|
||||||
return tot - travel_time
|
return tot - travel_time
|
||||||
@@ -117,6 +119,28 @@ class FormationAttackFlightPlan(FormationFlightPlan[FormationAttackLayout], ABC)
|
|||||||
return super().tot_for_waypoint(waypoint)
|
return super().tot_for_waypoint(waypoint)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FormationAttackLayout(FormationLayout):
|
||||||
|
ingress: FlightWaypoint
|
||||||
|
targets: list[FlightWaypoint]
|
||||||
|
|
||||||
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
|
yield self.departure
|
||||||
|
yield self.hold
|
||||||
|
yield from self.nav_to
|
||||||
|
yield self.join
|
||||||
|
yield self.ingress
|
||||||
|
yield from self.targets
|
||||||
|
yield self.split
|
||||||
|
if self.refuel is not None:
|
||||||
|
yield self.refuel
|
||||||
|
yield from self.nav_from
|
||||||
|
yield self.arrival
|
||||||
|
if self.divert is not None:
|
||||||
|
yield self.divert
|
||||||
|
yield self.bullseye
|
||||||
|
|
||||||
|
|
||||||
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
|
FlightPlanT = TypeVar("FlightPlanT", bound=FlightPlan[FormationAttackLayout])
|
||||||
LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout)
|
LayoutT = TypeVar("LayoutT", bound=FormationAttackLayout)
|
||||||
|
|
||||||
@@ -145,35 +169,26 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
|
|
||||||
hold = builder.hold(self._hold_point())
|
hold = builder.hold(self._hold_point())
|
||||||
join = builder.join(self.package.waypoints.join)
|
join = builder.join(self.package.waypoints.join)
|
||||||
join.wants_escort = True
|
|
||||||
|
|
||||||
ingress = builder.ingress(
|
|
||||||
ingress_type, self.package.waypoints.ingress, self.package.target
|
|
||||||
)
|
|
||||||
ingress.wants_escort = True
|
|
||||||
|
|
||||||
for target_waypoint in target_waypoints:
|
|
||||||
target_waypoint.wants_escort = True
|
|
||||||
|
|
||||||
split = builder.split(self.package.waypoints.split)
|
split = builder.split(self.package.waypoints.split)
|
||||||
split.wants_escort = True
|
|
||||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||||
|
|
||||||
return FormationAttackLayout(
|
return FormationAttackLayout(
|
||||||
departure=builder.takeoff(self.flight.departure),
|
departure=builder.takeoff(self.flight.departure),
|
||||||
hold=hold,
|
hold=hold,
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
hold.position, join.position, self.doctrine.combat_altitude
|
hold.position, join.position, self.doctrine.ingress_altitude
|
||||||
),
|
),
|
||||||
join=join,
|
join=join,
|
||||||
ingress=ingress,
|
ingress=builder.ingress(
|
||||||
|
ingress_type, self.package.waypoints.ingress, self.package.target
|
||||||
|
),
|
||||||
targets=target_waypoints,
|
targets=target_waypoints,
|
||||||
split=split,
|
split=split,
|
||||||
refuel=refuel,
|
refuel=refuel,
|
||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
refuel.position,
|
refuel.position,
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
self.doctrine.combat_altitude,
|
self.doctrine.ingress_altitude,
|
||||||
),
|
),
|
||||||
arrival=builder.land(self.flight.arrival),
|
arrival=builder.land(self.flight.arrival),
|
||||||
divert=builder.divert(self.flight.divert),
|
divert=builder.divert(self.flight.divert),
|
||||||
|
|||||||
@@ -32,11 +32,10 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
|||||||
assert self._flight_plan is not None
|
assert self._flight_plan is not None
|
||||||
return self._flight_plan
|
return self._flight_plan
|
||||||
|
|
||||||
def regenerate(self, dump_debug_info: bool = False) -> None:
|
def regenerate(self) -> None:
|
||||||
try:
|
try:
|
||||||
self._generate_package_waypoints_if_needed(dump_debug_info)
|
self._generate_package_waypoints_if_needed()
|
||||||
self._flight_plan = self.build(dump_debug_info)
|
self._flight_plan = self.build()
|
||||||
self._flight_plan.add_waypoint_actions()
|
|
||||||
except NavMeshError as ex:
|
except NavMeshError as ex:
|
||||||
color = "blue" if self.flight.squadron.player else "red"
|
color = "blue" if self.flight.squadron.player else "red"
|
||||||
raise PlanningError(
|
raise PlanningError(
|
||||||
@@ -44,15 +43,10 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
|||||||
f"{self.flight.departure} to {self.package.target}"
|
f"{self.flight.departure} to {self.package.target}"
|
||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
def _generate_package_waypoints_if_needed(self, dump_debug_info: bool) -> None:
|
def _generate_package_waypoints_if_needed(self) -> None:
|
||||||
# Package waypoints are only valid for offensive missions. Skip this if the
|
if self.package.waypoints is None:
|
||||||
# target is friendly.
|
|
||||||
if self.package.target.is_friendly(self.is_player):
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.package.waypoints is None or dump_debug_info:
|
|
||||||
self.package.waypoints = PackageWaypoints.create(
|
self.package.waypoints = PackageWaypoints.create(
|
||||||
self.package, self.coalition, dump_debug_info
|
self.package, self.coalition
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -60,7 +54,12 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
|
|||||||
return self.flight.departure.theater
|
return self.flight.departure.theater
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def build(self, dump_debug_info: bool = False) -> FlightPlanT: ...
|
def layout(self) -> LayoutT:
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def build(self) -> FlightPlanT:
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def package(self) -> Package:
|
def package(self) -> Package:
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ from __future__ import annotations
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
|
from typing import Any, TYPE_CHECKING, TypeGuard
|
||||||
|
|
||||||
from game.flightplan.waypointactions.hold import Hold
|
|
||||||
from game.typeguard import self_type_guard
|
from game.typeguard import self_type_guard
|
||||||
from game.utils import Speed
|
|
||||||
from .flightplan import FlightPlan
|
from .flightplan import FlightPlan
|
||||||
from .standard import StandardFlightPlan, StandardLayout
|
from .standard import StandardFlightPlan, StandardLayout
|
||||||
|
|
||||||
@@ -20,43 +18,29 @@ class LoiterLayout(StandardLayout, ABC):
|
|||||||
hold: FlightWaypoint
|
hold: FlightWaypoint
|
||||||
|
|
||||||
|
|
||||||
LayoutT = TypeVar("LayoutT", bound=LoiterLayout)
|
class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
|
||||||
|
|
||||||
|
|
||||||
class LoiterFlightPlan(StandardFlightPlan[LayoutT], ABC):
|
|
||||||
@property
|
@property
|
||||||
def hold_duration(self) -> timedelta:
|
def hold_duration(self) -> timedelta:
|
||||||
return timedelta(minutes=5)
|
return timedelta(minutes=5)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def push_time(self) -> datetime: ...
|
def push_time(self) -> datetime:
|
||||||
|
...
|
||||||
|
|
||||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||||
if waypoint == self.layout.hold:
|
if waypoint == self.layout.hold:
|
||||||
return self.push_time
|
return self.push_time
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def total_time_between_waypoints(
|
def travel_time_between_waypoints(
|
||||||
self, a: FlightWaypoint, b: FlightWaypoint
|
self, a: FlightWaypoint, b: FlightWaypoint
|
||||||
) -> timedelta:
|
) -> timedelta:
|
||||||
travel_time = super().total_time_between_waypoints(a, b)
|
travel_time = super().travel_time_between_waypoints(a, b)
|
||||||
if a != self.layout.hold:
|
if a != self.layout.hold:
|
||||||
return travel_time
|
return travel_time
|
||||||
return travel_time + self.hold_duration
|
return travel_time + self.hold_duration
|
||||||
|
|
||||||
@self_type_guard
|
@self_type_guard
|
||||||
def is_loiter(
|
def is_loiter(self, flight_plan: FlightPlan[Any]) -> TypeGuard[LoiterFlightPlan]:
|
||||||
self, flight_plan: FlightPlan[Any]
|
|
||||||
) -> TypeGuard[LoiterFlightPlan[Any]]:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def provide_push_time(self) -> datetime:
|
|
||||||
return self.push_time
|
|
||||||
|
|
||||||
def add_waypoint_actions(self) -> None:
|
|
||||||
hold = self.layout.hold
|
|
||||||
speed = self.flight.unit_type.patrol_speed
|
|
||||||
if speed is None:
|
|
||||||
speed = Speed.from_mach(0.6, hold.alt)
|
|
||||||
hold.add_action(Hold(self.provide_push_time, hold.alt, speed))
|
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaAircraftFlightPlan, FormationAttackLayou
|
|||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)
|
return self._build(FlightWaypointType.INGRESS_OCA_AIRCRAFT)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> OcaAircraftFlightPlan:
|
def build(self) -> OcaAircraftFlightPlan:
|
||||||
return OcaAircraftFlightPlan(self.flight, self.layout())
|
return OcaAircraftFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[OcaRunwayFlightPlan, FormationAttackLayout]
|
|||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)
|
return self._build(FlightWaypointType.INGRESS_OCA_RUNWAY)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> OcaRunwayFlightPlan:
|
def build(self) -> OcaRunwayFlightPlan:
|
||||||
return OcaRunwayFlightPlan(self.flight, self.layout())
|
return OcaRunwayFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
|||||||
"REFUEL", FlightWaypointType.REFUEL, refuel, altitude
|
"REFUEL", FlightWaypointType.REFUEL, refuel, altitude
|
||||||
)
|
)
|
||||||
|
|
||||||
delay_target_to_split: timedelta = self.total_time_between_waypoints(
|
delay_target_to_split: timedelta = self.travel_time_between_waypoints(
|
||||||
self.target_area_waypoint(), split_waypoint
|
self.target_area_waypoint(), split_waypoint
|
||||||
)
|
)
|
||||||
delay_split_to_refuel: timedelta = self.total_time_between_waypoints(
|
delay_split_to_refuel: timedelta = self.travel_time_between_waypoints(
|
||||||
split_waypoint, refuel_waypoint
|
split_waypoint, refuel_waypoint
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -121,5 +121,5 @@ class Builder(IBuilder[PackageRefuelingFlightPlan, PatrollingLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> PackageRefuelingFlightPlan:
|
def build(self) -> PackageRefuelingFlightPlan:
|
||||||
return PackageRefuelingFlightPlan(self.flight, self.layout())
|
return PackageRefuelingFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_start_time(self) -> datetime:
|
def patrol_start_time(self) -> datetime:
|
||||||
return self.tot
|
return self.package.time_over_target
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_end_time(self) -> datetime:
|
def patrol_end_time(self) -> datetime:
|
||||||
|
|||||||
@@ -93,5 +93,5 @@ class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> RtbFlightPlan:
|
def build(self) -> RtbFlightPlan:
|
||||||
return RtbFlightPlan(self.flight, self.layout())
|
return RtbFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -24,5 +24,5 @@ class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
|
|||||||
def layout(self) -> FormationAttackLayout:
|
def layout(self) -> FormationAttackLayout:
|
||||||
return self._build(FlightWaypointType.INGRESS_SEAD)
|
return self._build(FlightWaypointType.INGRESS_SEAD)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> SeadFlightPlan:
|
def build(self) -> SeadFlightPlan:
|
||||||
return SeadFlightPlan(self.flight, self.layout())
|
return SeadFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
|
|||||||
|
|
||||||
class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]):
|
class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]):
|
||||||
def layout(self) -> RecoveryTankerLayout:
|
def layout(self) -> RecoveryTankerLayout:
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
|
||||||
# TODO: Propagate the ship position to the Tanker's TOT,
|
# TODO: Propagate the ship position to the Tanker's TOT,
|
||||||
@@ -90,5 +91,5 @@ class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> RecoveryTankerFlightPlan:
|
def build(self) -> RecoveryTankerFlightPlan:
|
||||||
return RecoveryTankerFlightPlan(self.flight, self.layout())
|
return RecoveryTankerFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -32,5 +32,5 @@ class Builder(FormationAttackBuilder[StrikeFlightPlan, FormationAttackLayout]):
|
|||||||
|
|
||||||
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
|
return self._build(FlightWaypointType.INGRESS_STRIKE, targets)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> StrikeFlightPlan:
|
def build(self) -> StrikeFlightPlan:
|
||||||
return StrikeFlightPlan(self.flight, self.layout())
|
return StrikeFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -5,15 +5,13 @@ from datetime import datetime, timedelta
|
|||||||
from typing import Iterator, TYPE_CHECKING, Type
|
from typing import Iterator, TYPE_CHECKING, Type
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from dcs.task import Targets
|
|
||||||
|
|
||||||
from game.flightplan import HoldZoneGeometry
|
from game.utils import Heading
|
||||||
from game.flightplan.waypointactions.engagetargets import EngageTargets
|
|
||||||
from game.flightplan.waypointoptions.formation import Formation
|
|
||||||
from game.utils import Heading, nautical_miles
|
|
||||||
from .ibuilder import IBuilder
|
from .ibuilder import IBuilder
|
||||||
from .loiter import LoiterFlightPlan, LoiterLayout
|
from .loiter import LoiterFlightPlan, LoiterLayout
|
||||||
from .waypointbuilder import WaypointBuilder
|
from .waypointbuilder import WaypointBuilder
|
||||||
|
from ..traveltime import GroundSpeed, TravelTime
|
||||||
|
from ...flightplan import HoldZoneGeometry
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..flightwaypoint import FlightWaypoint
|
from ..flightwaypoint import FlightWaypoint
|
||||||
@@ -39,7 +37,7 @@ class SweepLayout(LoiterLayout):
|
|||||||
yield self.bullseye
|
yield self.bullseye
|
||||||
|
|
||||||
|
|
||||||
class SweepFlightPlan(LoiterFlightPlan[SweepLayout]):
|
class SweepFlightPlan(LoiterFlightPlan):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def builder_type() -> Type[Builder]:
|
def builder_type() -> Type[Builder]:
|
||||||
return Builder
|
return Builder
|
||||||
@@ -57,7 +55,7 @@ class SweepFlightPlan(LoiterFlightPlan[SweepLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sweep_start_time(self) -> datetime:
|
def sweep_start_time(self) -> datetime:
|
||||||
travel_time = self.total_time_between_waypoints(
|
travel_time = self.travel_time_between_waypoints(
|
||||||
self.layout.sweep_start, self.layout.sweep_end
|
self.layout.sweep_start, self.layout.sweep_end
|
||||||
)
|
)
|
||||||
return self.sweep_end_time - travel_time
|
return self.sweep_end_time - travel_time
|
||||||
@@ -80,8 +78,10 @@ class SweepFlightPlan(LoiterFlightPlan[SweepLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def push_time(self) -> datetime:
|
def push_time(self) -> datetime:
|
||||||
return self.sweep_end_time - self.travel_time_between_waypoints(
|
return self.sweep_end_time - TravelTime.between_points(
|
||||||
self.layout.hold, self.layout.sweep_end
|
self.layout.hold.position,
|
||||||
|
self.layout.sweep_end.position,
|
||||||
|
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -92,19 +92,6 @@ class SweepFlightPlan(LoiterFlightPlan[SweepLayout]):
|
|||||||
def mission_departure_time(self) -> datetime:
|
def mission_departure_time(self) -> datetime:
|
||||||
return self.sweep_end_time
|
return self.sweep_end_time
|
||||||
|
|
||||||
def add_waypoint_actions(self) -> None:
|
|
||||||
super().add_waypoint_actions()
|
|
||||||
self.layout.sweep_start.set_option(Formation.LINE_ABREAST_OPEN)
|
|
||||||
self.layout.sweep_start.add_action(
|
|
||||||
EngageTargets(
|
|
||||||
nautical_miles(50),
|
|
||||||
[
|
|
||||||
Targets.All.Air.Planes.Fighters,
|
|
||||||
Targets.All.Air.Planes.MultiroleFighters,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
||||||
def layout(self) -> SweepLayout:
|
def layout(self) -> SweepLayout:
|
||||||
@@ -114,11 +101,11 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
|||||||
self.package.waypoints.join.heading_between_point(target)
|
self.package.waypoints.join.heading_between_point(target)
|
||||||
)
|
)
|
||||||
start_pos = target.point_from_heading(
|
start_pos = target.point_from_heading(
|
||||||
heading.degrees, -self.doctrine.sweep.distance.meters
|
heading.degrees, -self.doctrine.sweep_distance.meters
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
start, end = builder.sweep(start_pos, target, self.doctrine.combat_altitude)
|
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||||
|
|
||||||
hold = builder.hold(self._hold_point())
|
hold = builder.hold(self._hold_point())
|
||||||
|
|
||||||
@@ -126,12 +113,12 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
|||||||
departure=builder.takeoff(self.flight.departure),
|
departure=builder.takeoff(self.flight.departure),
|
||||||
hold=hold,
|
hold=hold,
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
hold.position, start.position, self.doctrine.combat_altitude
|
hold.position, start.position, self.doctrine.ingress_altitude
|
||||||
),
|
),
|
||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
end.position,
|
end.position,
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
self.doctrine.combat_altitude,
|
self.doctrine.ingress_altitude,
|
||||||
),
|
),
|
||||||
sweep_start=start,
|
sweep_start=start,
|
||||||
sweep_end=end,
|
sweep_end=end,
|
||||||
@@ -150,5 +137,5 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
|||||||
target, origin, ip, join, self.coalition, self.theater
|
target, origin, ip, join, self.coalition, self.theater
|
||||||
).find_best_hold_point()
|
).find_best_hold_point()
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> SweepFlightPlan:
|
def build(self) -> SweepFlightPlan:
|
||||||
return SweepFlightPlan(self.flight, self.layout())
|
return SweepFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
|||||||
# flights in the package that have requested escort. If the package
|
# flights in the package that have requested escort. If the package
|
||||||
# requests an escort the CAP self.flight will remain on station for the
|
# requests an escort the CAP self.flight will remain on station for the
|
||||||
# duration of the escorted mission, or until it is winchester/bingo.
|
# duration of the escorted mission, or until it is winchester/bingo.
|
||||||
return self.flight.coalition.doctrine.cap.duration
|
return self.flight.coalition.doctrine.cap_duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
@@ -50,7 +50,7 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def engagement_distance(self) -> Distance:
|
def engagement_distance(self) -> Distance:
|
||||||
return self.flight.coalition.doctrine.cap.engagement_range
|
return self.flight.coalition.doctrine.cap_engagement_range
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def builder_type() -> Type[Builder]:
|
def builder_type() -> Type[Builder]:
|
||||||
@@ -90,8 +90,8 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
|||||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||||
patrol_alt = max(
|
patrol_alt = max(
|
||||||
self.doctrine.cap.min_patrol_altitude,
|
self.doctrine.min_patrol_altitude,
|
||||||
min(self.doctrine.cap.max_patrol_altitude, randomized_alt),
|
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
@@ -122,5 +122,5 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> TarCapFlightPlan:
|
def build(self) -> TarCapFlightPlan:
|
||||||
return TarCapFlightPlan(self.flight, self.layout())
|
return TarCapFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -17,13 +17,7 @@ class TheaterRefuelingFlightPlan(RefuelingFlightPlan):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_duration(self) -> timedelta:
|
def patrol_duration(self) -> timedelta:
|
||||||
# Add 30 minutes to desired_player_mission_duration as TOTs for flights
|
return timedelta(hours=1)
|
||||||
# can sit up to this time. This extension means the tanker remains on
|
|
||||||
# station for the flights' return.
|
|
||||||
return (
|
|
||||||
self.flight.coalition.game.settings.desired_player_mission_duration
|
|
||||||
+ timedelta(minutes=30)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||||
@@ -85,5 +79,5 @@ class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
|||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(self, dump_debug_info: bool = False) -> TheaterRefuelingFlightPlan:
|
def build(self) -> TheaterRefuelingFlightPlan:
|
||||||
return TheaterRefuelingFlightPlan(self.flight, self.layout())
|
return TheaterRefuelingFlightPlan(self.flight, self.layout())
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ class UiZone:
|
|||||||
|
|
||||||
class UiZoneDisplay(abc.ABC):
|
class UiZoneDisplay(abc.ABC):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def ui_zone(self) -> UiZone: ...
|
def ui_zone(self) -> UiZone:
|
||||||
|
...
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class WaypointBuilder:
|
|||||||
"NAV",
|
"NAV",
|
||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position,
|
position,
|
||||||
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
description="Enter theater",
|
description="Enter theater",
|
||||||
pretty_name="Enter theater",
|
pretty_name="Enter theater",
|
||||||
)
|
)
|
||||||
@@ -99,7 +99,7 @@ class WaypointBuilder:
|
|||||||
"NAV",
|
"NAV",
|
||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position,
|
position,
|
||||||
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
description="Exit theater",
|
description="Exit theater",
|
||||||
pretty_name="Exit theater",
|
pretty_name="Exit theater",
|
||||||
)
|
)
|
||||||
@@ -127,7 +127,10 @@ class WaypointBuilder:
|
|||||||
position = divert.position
|
position = divert.position
|
||||||
altitude_type: AltitudeReference
|
altitude_type: AltitudeReference
|
||||||
if isinstance(divert, OffMapSpawn):
|
if isinstance(divert, OffMapSpawn):
|
||||||
altitude = self.doctrine.resolve_rendezvous_altitude(self.is_helo)
|
if self.is_helo:
|
||||||
|
altitude = meters(500)
|
||||||
|
else:
|
||||||
|
altitude = self.doctrine.rendezvous_altitude
|
||||||
altitude_type = "BARO"
|
altitude_type = "BARO"
|
||||||
else:
|
else:
|
||||||
altitude = meters(0)
|
altitude = meters(0)
|
||||||
@@ -165,7 +168,7 @@ class WaypointBuilder:
|
|||||||
"HOLD",
|
"HOLD",
|
||||||
FlightWaypointType.LOITER,
|
FlightWaypointType.LOITER,
|
||||||
position,
|
position,
|
||||||
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
|
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Wait until push time",
|
description="Wait until push time",
|
||||||
pretty_name="Hold",
|
pretty_name="Hold",
|
||||||
@@ -180,7 +183,7 @@ class WaypointBuilder:
|
|||||||
"JOIN",
|
"JOIN",
|
||||||
FlightWaypointType.JOIN,
|
FlightWaypointType.JOIN,
|
||||||
position,
|
position,
|
||||||
self.doctrine.resolve_combat_altitude(self.is_helo),
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Rendezvous with package",
|
description="Rendezvous with package",
|
||||||
pretty_name="Join",
|
pretty_name="Join",
|
||||||
@@ -195,7 +198,7 @@ class WaypointBuilder:
|
|||||||
"REFUEL",
|
"REFUEL",
|
||||||
FlightWaypointType.REFUEL,
|
FlightWaypointType.REFUEL,
|
||||||
position,
|
position,
|
||||||
self.doctrine.resolve_combat_altitude(self.is_helo),
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Refuel from tanker",
|
description="Refuel from tanker",
|
||||||
pretty_name="Refuel",
|
pretty_name="Refuel",
|
||||||
@@ -223,7 +226,7 @@ class WaypointBuilder:
|
|||||||
"SPLIT",
|
"SPLIT",
|
||||||
FlightWaypointType.SPLIT,
|
FlightWaypointType.SPLIT,
|
||||||
position,
|
position,
|
||||||
self.doctrine.resolve_combat_altitude(self.is_helo),
|
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Depart from package",
|
description="Depart from package",
|
||||||
pretty_name="Split",
|
pretty_name="Split",
|
||||||
@@ -243,13 +246,28 @@ class WaypointBuilder:
|
|||||||
"INGRESS",
|
"INGRESS",
|
||||||
ingress_type,
|
ingress_type,
|
||||||
position,
|
position,
|
||||||
self.doctrine.resolve_combat_altitude(self.is_helo),
|
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description=f"INGRESS on {objective.name}",
|
description=f"INGRESS on {objective.name}",
|
||||||
pretty_name=f"INGRESS on {objective.name}",
|
pretty_name=f"INGRESS on {objective.name}",
|
||||||
targets=objective.strike_targets,
|
targets=objective.strike_targets,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
|
||||||
|
alt_type: AltitudeReference = "BARO"
|
||||||
|
if self.is_helo:
|
||||||
|
alt_type = "RADIO"
|
||||||
|
|
||||||
|
return FlightWaypoint(
|
||||||
|
"EGRESS",
|
||||||
|
FlightWaypointType.EGRESS,
|
||||||
|
position,
|
||||||
|
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
|
alt_type,
|
||||||
|
description=f"EGRESS from {target.name}",
|
||||||
|
pretty_name=f"EGRESS from {target.name}",
|
||||||
|
)
|
||||||
|
|
||||||
def bai_group(self, target: StrikeTarget) -> FlightWaypoint:
|
def bai_group(self, target: StrikeTarget) -> FlightWaypoint:
|
||||||
return self._target_point(target, f"ATTACK {target.name}")
|
return self._target_point(target, f"ATTACK {target.name}")
|
||||||
|
|
||||||
@@ -288,7 +306,7 @@ class WaypointBuilder:
|
|||||||
f"SEAD on {target.name}",
|
f"SEAD on {target.name}",
|
||||||
target,
|
target,
|
||||||
flyover=True,
|
flyover=True,
|
||||||
altitude=self.doctrine.combat_altitude,
|
altitude=self.doctrine.ingress_altitude,
|
||||||
alt_type="BARO",
|
alt_type="BARO",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -339,6 +357,17 @@ class WaypointBuilder:
|
|||||||
waypoint.only_for_player = True
|
waypoint.only_for_player = True
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
|
def cas(self, position: Point) -> FlightWaypoint:
|
||||||
|
return FlightWaypoint(
|
||||||
|
"CAS",
|
||||||
|
FlightWaypointType.CAS,
|
||||||
|
position,
|
||||||
|
meters(60) if self.is_helo else meters(1000),
|
||||||
|
"RADIO",
|
||||||
|
description="Provide CAS",
|
||||||
|
pretty_name="CAS",
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||||
"""Creates a racetrack start waypoint.
|
"""Creates a racetrack start waypoint.
|
||||||
@@ -478,7 +507,7 @@ class WaypointBuilder:
|
|||||||
"TARGET",
|
"TARGET",
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
target.position,
|
target.position,
|
||||||
self.doctrine.resolve_combat_altitude(self.is_helo),
|
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Escort the package",
|
description="Escort the package",
|
||||||
pretty_name="Target area",
|
pretty_name="Target area",
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Iterator
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from game.ato.iflightroster import IFlightRoster
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.squadrons import Squadron, Pilot
|
from game.squadrons import Squadron, Pilot
|
||||||
|
|
||||||
|
|
||||||
class FlightRoster(IFlightRoster):
|
class FlightRoster:
|
||||||
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
|
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
|
||||||
self.squadron = squadron
|
self.squadron = squadron
|
||||||
self.pilots: list[Optional[Pilot]] = []
|
self.pilots: list[Optional[Pilot]] = []
|
||||||
self.resize(initial_size)
|
self.resize(initial_size)
|
||||||
|
|
||||||
def iter_pilots(self) -> Iterator[Pilot | None]:
|
|
||||||
yield from self.pilots
|
|
||||||
|
|
||||||
def pilot_at(self, idx: int) -> Pilot | None:
|
|
||||||
return self.pilots[idx]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def max_size(self) -> int:
|
def max_size(self) -> int:
|
||||||
return len(self.pilots)
|
return len(self.pilots)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def player_count(self) -> int:
|
||||||
|
return len([p for p in self.pilots if p is not None and p.player])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def missing_pilots(self) -> int:
|
||||||
|
return len([p for p in self.pilots if p is None])
|
||||||
|
|
||||||
def resize(self, new_size: int) -> None:
|
def resize(self, new_size: int) -> None:
|
||||||
if self.max_size > new_size:
|
if self.max_size > new_size:
|
||||||
self.squadron.return_pilots(
|
self.squadron.return_pilots(
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.flightplan.waypointactions.waypointaction import WaypointAction
|
|
||||||
|
|
||||||
|
|
||||||
class ActionState:
|
|
||||||
def __init__(self, action: WaypointAction) -> None:
|
|
||||||
self.action = action
|
|
||||||
self._finished = False
|
|
||||||
|
|
||||||
def describe(self) -> str:
|
|
||||||
return self.action.describe()
|
|
||||||
|
|
||||||
def finish(self) -> None:
|
|
||||||
self._finished = True
|
|
||||||
|
|
||||||
def is_finished(self) -> bool:
|
|
||||||
return self._finished
|
|
||||||
|
|
||||||
def on_game_tick(self, time: datetime, duration: timedelta) -> timedelta:
|
|
||||||
return self.action.update_state(self, time, duration)
|
|
||||||
@@ -21,14 +21,8 @@ class FlightState(ABC):
|
|||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.avoid_further_combat = False
|
self.avoid_further_combat = False
|
||||||
|
|
||||||
def initialize(self, now: datetime) -> None:
|
def reinitialize(self, now: datetime) -> None:
|
||||||
from game.ato.flightstate import Uninitialized, WaitingForStart
|
from game.ato.flightstate import WaitingForStart
|
||||||
|
|
||||||
# Flight objects are created with Uninitialized state. However when the simulation runs
|
|
||||||
# the flight state changes and may be serialized. We only want to initialize the state
|
|
||||||
# for newly created flights and not ones deserialized from a save file.
|
|
||||||
if type(self.flight.state) != Uninitialized:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.flight.flight_plan.startup_time() <= now:
|
if self.flight.flight_plan.startup_time() <= now:
|
||||||
self._set_active_flight_state(now)
|
self._set_active_flight_state(now)
|
||||||
@@ -63,12 +57,14 @@ class FlightState(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cancelable(self) -> bool: ...
|
def cancelable(self) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def on_game_tick(
|
def on_game_tick(
|
||||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||||
) -> None: ...
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def in_flight(self) -> bool:
|
def in_flight(self) -> bool:
|
||||||
@@ -99,14 +95,17 @@ class FlightState(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_waiting_for_start(self) -> bool: ...
|
def is_waiting_for_start(self) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def estimate_position(self) -> Point: ...
|
def estimate_position(self) -> Point:
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def spawn_type(self) -> StartType: ...
|
def spawn_type(self) -> StartType:
|
||||||
|
...
|
||||||
|
|
||||||
def a2a_commit_region(self) -> Optional[ThreatPoly]:
|
def a2a_commit_region(self) -> Optional[ThreatPoly]:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import deque
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
from game.ato.flightstate import Completed
|
from game.ato.flightstate import Completed
|
||||||
from game.ato.flightstate.actionstate import ActionState
|
|
||||||
from game.ato.flightstate.flightstate import FlightState
|
from game.ato.flightstate.flightstate import FlightState
|
||||||
from game.ato.flightwaypoint import FlightWaypoint
|
from game.ato.flightwaypoint import FlightWaypoint
|
||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
from game.ato.starttype import StartType
|
from game.ato.starttype import StartType
|
||||||
from game.settings.settings import FastForwardStopCondition
|
|
||||||
from game.utils import Distance, LBS_TO_KG, Speed, pairwise
|
from game.utils import Distance, LBS_TO_KG, Speed, pairwise
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -41,15 +37,6 @@ class InFlight(FlightState, ABC):
|
|||||||
self.total_time_to_next_waypoint = self.travel_time_between_waypoints()
|
self.total_time_to_next_waypoint = self.travel_time_between_waypoints()
|
||||||
self.elapsed_time = timedelta()
|
self.elapsed_time = timedelta()
|
||||||
self.current_waypoint_elapsed = False
|
self.current_waypoint_elapsed = False
|
||||||
self.pending_actions: deque[ActionState] = deque(
|
|
||||||
ActionState(a) for a in self.current_waypoint.actions
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_action(self) -> ActionState | None:
|
|
||||||
if self.pending_actions:
|
|
||||||
return self.pending_actions[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cancelable(self) -> bool:
|
def cancelable(self) -> bool:
|
||||||
@@ -64,18 +51,29 @@ class InFlight(FlightState, ABC):
|
|||||||
return index <= self.waypoint_index
|
return index <= self.waypoint_index
|
||||||
|
|
||||||
def travel_time_between_waypoints(self) -> timedelta:
|
def travel_time_between_waypoints(self) -> timedelta:
|
||||||
return self.flight.flight_plan.travel_time_between_waypoints(
|
travel_time = self.flight.flight_plan.travel_time_between_waypoints(
|
||||||
self.current_waypoint, self.next_waypoint
|
self.current_waypoint, self.next_waypoint
|
||||||
)
|
)
|
||||||
|
if self.current_waypoint.waypoint_type is FlightWaypointType.LOITER:
|
||||||
|
# Loiter time is already built into travel_time_between_waypoints. If we're
|
||||||
|
# at a loiter point but still a regular InFlight (Loiter overrides this
|
||||||
|
# method) that means we're traveling from the loiter point but no longer
|
||||||
|
# loitering.
|
||||||
|
assert self.flight.flight_plan.is_loiter(self.flight.flight_plan)
|
||||||
|
travel_time -= self.flight.flight_plan.hold_duration
|
||||||
|
return travel_time
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def estimate_position(self) -> Point: ...
|
def estimate_position(self) -> Point:
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def estimate_altitude(self) -> tuple[Distance, str]: ...
|
def estimate_altitude(self) -> tuple[Distance, str]:
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def estimate_speed(self) -> Speed: ...
|
def estimate_speed(self) -> Speed:
|
||||||
|
...
|
||||||
|
|
||||||
def estimate_fuel_at_current_waypoint(self) -> float:
|
def estimate_fuel_at_current_waypoint(self) -> float:
|
||||||
initial_fuel = super().estimate_fuel()
|
initial_fuel = super().estimate_fuel()
|
||||||
@@ -90,6 +88,7 @@ class InFlight(FlightState, ABC):
|
|||||||
return initial_fuel
|
return initial_fuel
|
||||||
|
|
||||||
def next_waypoint_state(self) -> FlightState:
|
def next_waypoint_state(self) -> FlightState:
|
||||||
|
from .loiter import Loiter
|
||||||
from .racetrack import RaceTrack
|
from .racetrack import RaceTrack
|
||||||
from .navigating import Navigating
|
from .navigating import Navigating
|
||||||
|
|
||||||
@@ -98,6 +97,8 @@ class InFlight(FlightState, ABC):
|
|||||||
return Completed(self.flight, self.settings)
|
return Completed(self.flight, self.settings)
|
||||||
if self.next_waypoint.waypoint_type is FlightWaypointType.PATROL_TRACK:
|
if self.next_waypoint.waypoint_type is FlightWaypointType.PATROL_TRACK:
|
||||||
return RaceTrack(self.flight, self.settings, new_index)
|
return RaceTrack(self.flight, self.settings, new_index)
|
||||||
|
if self.next_waypoint.waypoint_type is FlightWaypointType.LOITER:
|
||||||
|
return Loiter(self.flight, self.settings, new_index)
|
||||||
return Navigating(self.flight, self.settings, new_index)
|
return Navigating(self.flight, self.settings, new_index)
|
||||||
|
|
||||||
def advance_to_next_waypoint(self) -> FlightState:
|
def advance_to_next_waypoint(self) -> FlightState:
|
||||||
@@ -109,13 +110,6 @@ class InFlight(FlightState, ABC):
|
|||||||
def on_game_tick(
|
def on_game_tick(
|
||||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||||
) -> None:
|
) -> None:
|
||||||
while (action := self.current_action) is not None:
|
|
||||||
duration = action.on_game_tick(time, duration)
|
|
||||||
if action.is_finished():
|
|
||||||
self.pending_actions.popleft()
|
|
||||||
if duration <= timedelta():
|
|
||||||
return
|
|
||||||
|
|
||||||
self.elapsed_time += duration
|
self.elapsed_time += duration
|
||||||
if self.elapsed_time > self.total_time_to_next_waypoint:
|
if self.elapsed_time > self.total_time_to_next_waypoint:
|
||||||
new_state = self.advance_to_next_waypoint()
|
new_state = self.advance_to_next_waypoint()
|
||||||
@@ -167,16 +161,10 @@ class InFlight(FlightState, ABC):
|
|||||||
def spawn_type(self) -> StartType:
|
def spawn_type(self) -> StartType:
|
||||||
return StartType.IN_FLIGHT
|
return StartType.IN_FLIGHT
|
||||||
|
|
||||||
def should_halt_sim(self) -> bool:
|
@property
|
||||||
if (
|
def description(self) -> str:
|
||||||
self.flight.client_count > 0
|
if self.has_aborted:
|
||||||
and self.settings.fast_forward_stop_condition
|
abort = "(Aborted) "
|
||||||
== FastForwardStopCondition.PLAYER_AT_IP
|
else:
|
||||||
and self.is_at_ip
|
abort = ""
|
||||||
):
|
return f"{abort}Flying to {self.next_waypoint.name}"
|
||||||
logging.info(
|
|
||||||
f"Interrupting simulation because {self.flight} has players and has "
|
|
||||||
"reached IP"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|||||||
46
game/ato/flightstate/loiter.py
Normal file
46
game/ato/flightstate/loiter.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from dcs import Point
|
||||||
|
|
||||||
|
from game.ato.flightstate import FlightState, InFlight
|
||||||
|
from game.ato.flightstate.navigating import Navigating
|
||||||
|
from game.utils import Distance, Speed
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.ato.flight import Flight
|
||||||
|
from game.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
class Loiter(InFlight):
|
||||||
|
def __init__(self, flight: Flight, settings: Settings, waypoint_index: int) -> None:
|
||||||
|
assert flight.flight_plan.is_loiter(flight.flight_plan)
|
||||||
|
self.hold_duration = flight.flight_plan.hold_duration
|
||||||
|
super().__init__(flight, settings, waypoint_index)
|
||||||
|
|
||||||
|
def estimate_position(self) -> Point:
|
||||||
|
return self.current_waypoint.position
|
||||||
|
|
||||||
|
def estimate_altitude(self) -> tuple[Distance, str]:
|
||||||
|
return self.current_waypoint.alt, self.current_waypoint.alt_type
|
||||||
|
|
||||||
|
def estimate_speed(self) -> Speed:
|
||||||
|
return self.flight.unit_type.preferred_patrol_speed(self.estimate_altitude()[0])
|
||||||
|
|
||||||
|
def estimate_fuel(self) -> float:
|
||||||
|
# TODO: Estimate loiter consumption per minute?
|
||||||
|
return self.estimate_fuel_at_current_waypoint()
|
||||||
|
|
||||||
|
def next_waypoint_state(self) -> FlightState:
|
||||||
|
# Do not automatically advance to the next waypoint. Just proceed from the
|
||||||
|
# current one with the normal flying state.
|
||||||
|
return Navigating(self.flight, self.settings, self.waypoint_index)
|
||||||
|
|
||||||
|
def travel_time_between_waypoints(self) -> timedelta:
|
||||||
|
return self.hold_duration
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str:
|
||||||
|
return f"Loitering for {self.hold_duration - self.elapsed_time}"
|
||||||
@@ -29,11 +29,6 @@ class Navigating(InFlight):
|
|||||||
events.update_flight_position(self.flight, self.estimate_position())
|
events.update_flight_position(self.flight, self.estimate_position())
|
||||||
|
|
||||||
def progress(self) -> float:
|
def progress(self) -> float:
|
||||||
# if next waypoint is very close, assume we reach it immediately to avoid divide
|
|
||||||
# by zero error
|
|
||||||
if self.total_time_to_next_waypoint.total_seconds() < 1:
|
|
||||||
return 1.0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
self.elapsed_time.total_seconds()
|
self.elapsed_time.total_seconds()
|
||||||
/ self.total_time_to_next_waypoint.total_seconds()
|
/ self.total_time_to_next_waypoint.total_seconds()
|
||||||
@@ -85,14 +80,3 @@ class Navigating(InFlight):
|
|||||||
@property
|
@property
|
||||||
def spawn_type(self) -> StartType:
|
def spawn_type(self) -> StartType:
|
||||||
return StartType.IN_FLIGHT
|
return StartType.IN_FLIGHT
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self) -> str:
|
|
||||||
if (action := self.current_action) is not None:
|
|
||||||
return action.describe()
|
|
||||||
|
|
||||||
if self.has_aborted:
|
|
||||||
abort = "(Aborted) "
|
|
||||||
else:
|
|
||||||
abort = ""
|
|
||||||
return f"{abort}Flying to {self.next_waypoint.name}"
|
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ from .atdeparture import AtDeparture
|
|||||||
from .taxi import Taxi
|
from .taxi import Taxi
|
||||||
from ..starttype import StartType
|
from ..starttype import StartType
|
||||||
|
|
||||||
from game.settings.settings import FastForwardStopCondition
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
@@ -40,8 +37,7 @@ class StartUp(AtDeparture):
|
|||||||
def should_halt_sim(self) -> bool:
|
def should_halt_sim(self) -> bool:
|
||||||
if (
|
if (
|
||||||
self.flight.client_count > 0
|
self.flight.client_count > 0
|
||||||
and self.settings.fast_forward_stop_condition
|
and self.settings.player_mission_interrupts_sim_at is StartType.COLD
|
||||||
== FastForwardStopCondition.PLAYER_STARTUP
|
|
||||||
):
|
):
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Interrupting simulation because {self.flight} has players and has "
|
f"Interrupting simulation because {self.flight} has players and has "
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ from .navigating import Navigating
|
|||||||
from ..starttype import StartType
|
from ..starttype import StartType
|
||||||
from ...utils import LBS_TO_KG
|
from ...utils import LBS_TO_KG
|
||||||
|
|
||||||
from game.settings.settings import FastForwardStopCondition
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
@@ -47,8 +45,7 @@ class Takeoff(AtDeparture):
|
|||||||
def should_halt_sim(self) -> bool:
|
def should_halt_sim(self) -> bool:
|
||||||
if (
|
if (
|
||||||
self.flight.client_count > 0
|
self.flight.client_count > 0
|
||||||
and self.settings.fast_forward_stop_condition
|
and self.settings.player_mission_interrupts_sim_at is StartType.RUNWAY
|
||||||
== FastForwardStopCondition.PLAYER_TAKEOFF
|
|
||||||
):
|
):
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Interrupting simulation because {self.flight} has players and has "
|
f"Interrupting simulation because {self.flight} has players and has "
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ from .atdeparture import AtDeparture
|
|||||||
from .takeoff import Takeoff
|
from .takeoff import Takeoff
|
||||||
from ..starttype import StartType
|
from ..starttype import StartType
|
||||||
|
|
||||||
from game.settings.settings import FastForwardStopCondition
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
@@ -39,8 +37,7 @@ class Taxi(AtDeparture):
|
|||||||
def should_halt_sim(self) -> bool:
|
def should_halt_sim(self) -> bool:
|
||||||
if (
|
if (
|
||||||
self.flight.client_count > 0
|
self.flight.client_count > 0
|
||||||
and self.settings.fast_forward_stop_condition
|
and self.settings.player_mission_interrupts_sim_at is StartType.WARM
|
||||||
== FastForwardStopCondition.PLAYER_TAXI
|
|
||||||
):
|
):
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Interrupting simulation because {self.flight} has players and has "
|
f"Interrupting simulation because {self.flight} has players and has "
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Uninitialized(FlightState):
|
|||||||
def on_game_tick(
|
def on_game_tick(
|
||||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||||
) -> None:
|
) -> None:
|
||||||
self.initialize(time)
|
self.reinitialize(time)
|
||||||
self.flight.state.on_game_tick(events, time, duration)
|
self.flight.state.on_game_tick(events, time, duration)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ class FlightType(Enum):
|
|||||||
REFUELING = "Refueling"
|
REFUELING = "Refueling"
|
||||||
FERRY = "Ferry"
|
FERRY = "Ferry"
|
||||||
AIR_ASSAULT = "Air Assault"
|
AIR_ASSAULT = "Air Assault"
|
||||||
IDLE = "Idle"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.value
|
return self.value
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ from typing import Literal, TYPE_CHECKING
|
|||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
from game.flightplan.waypointactions.waypointaction import WaypointAction
|
|
||||||
from game.flightplan.waypointoptions.waypointoption import WaypointOption
|
|
||||||
from game.theater.theatergroup import TheaterUnit
|
from game.theater.theatergroup import TheaterUnit
|
||||||
from game.utils import Distance, meters
|
from game.utils import Distance, meters
|
||||||
|
|
||||||
@@ -41,11 +39,6 @@ class FlightWaypoint:
|
|||||||
# The minimum amount of fuel remaining at this waypoint in pounds.
|
# The minimum amount of fuel remaining at this waypoint in pounds.
|
||||||
min_fuel: float | None = None
|
min_fuel: float | None = None
|
||||||
|
|
||||||
wants_escort: bool = False
|
|
||||||
|
|
||||||
actions: list[WaypointAction] = field(default_factory=list)
|
|
||||||
options: dict[str, WaypointOption] = field(default_factory=dict)
|
|
||||||
|
|
||||||
# These are set very late by the air conflict generator (part of mission
|
# These are set very late by the air conflict generator (part of mission
|
||||||
# generation). We do it late so that we don't need to propagate changes
|
# generation). We do it late so that we don't need to propagate changes
|
||||||
# to waypoint times whenever the player alters the package TOT or the
|
# to waypoint times whenever the player alters the package TOT or the
|
||||||
@@ -53,12 +46,6 @@ class FlightWaypoint:
|
|||||||
tot: datetime | None = None
|
tot: datetime | None = None
|
||||||
departure_time: datetime | None = None
|
departure_time: datetime | None = None
|
||||||
|
|
||||||
def add_action(self, action: WaypointAction) -> None:
|
|
||||||
self.actions.append(action)
|
|
||||||
|
|
||||||
def set_option(self, option: WaypointOption) -> None:
|
|
||||||
self.options[option.id()] = option
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def x(self) -> float:
|
def x(self) -> float:
|
||||||
return self.position.x
|
return self.position.x
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class FlightWaypointType(IntEnum):
|
|||||||
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
|
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
|
||||||
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
|
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
|
||||||
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
|
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
|
||||||
CAS = 8 # Unused.
|
CAS = 8 # Should do CAS there
|
||||||
EGRESS = 9 # Should stop attack
|
EGRESS = 9 # Should stop attack
|
||||||
DESCENT_POINT = 10 # Should start descending to pattern alt
|
DESCENT_POINT = 10 # Should start descending to pattern alt
|
||||||
LANDING_POINT = 11 # Should land there
|
LANDING_POINT = 11 # Should land there
|
||||||
@@ -50,4 +50,3 @@ class FlightWaypointType(IntEnum):
|
|||||||
CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type
|
CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type
|
||||||
INGRESS_AIR_ASSAULT = 31
|
INGRESS_AIR_ASSAULT = 31
|
||||||
RECOVERY_TANKER = 32
|
RECOVERY_TANKER = 32
|
||||||
INGRESS_ANTI_SHIP = 33
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Optional, TYPE_CHECKING, Iterator
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.squadrons import Pilot
|
|
||||||
|
|
||||||
|
|
||||||
class IFlightRoster(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def iter_pilots(self) -> Iterator[Pilot | None]: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def pilot_at(self, idx: int) -> Pilot | None: ...
|
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def max_size(self) -> int: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def resize(self, new_size: int) -> None: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def clear(self) -> None: ...
|
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from typing import Iterator, Mapping, Optional, TYPE_CHECKING, Type, Any
|
from typing import Iterator, Mapping, Optional, TYPE_CHECKING, Type
|
||||||
|
|
||||||
from dcs.unittype import FlyingType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
from game.data.weapons import Pylon, Weapon, WeaponType
|
from game.data.weapons import Pylon, Weapon, WeaponType
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.factions.faction import Faction
|
|
||||||
|
|
||||||
from .flighttype import FlightType
|
from .flighttype import FlightType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -38,11 +35,6 @@ class Loadout:
|
|||||||
def derive_custom(self, name: str) -> Loadout:
|
def derive_custom(self, name: str) -> Loadout:
|
||||||
return Loadout(name, self.pylons, self.date, is_custom=True)
|
return Loadout(name, self.pylons, self.date, is_custom=True)
|
||||||
|
|
||||||
def clone(self) -> Loadout:
|
|
||||||
return Loadout(
|
|
||||||
self.name, dict(self.pylons), copy.deepcopy(self.date), self.is_custom
|
|
||||||
)
|
|
||||||
|
|
||||||
def has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
def has_weapon_of_type(self, weapon_type: WeaponType) -> bool:
|
||||||
for weapon in self.pylons.values():
|
for weapon in self.pylons.values():
|
||||||
if weapon is not None and weapon.weapon_group.type is weapon_type:
|
if weapon is not None and weapon.weapon_group.type is weapon_type:
|
||||||
@@ -54,7 +46,6 @@ class Loadout:
|
|||||||
weapon: Weapon,
|
weapon: Weapon,
|
||||||
pylon: Pylon,
|
pylon: Pylon,
|
||||||
date: datetime.date,
|
date: datetime.date,
|
||||||
faction: Faction,
|
|
||||||
skip_types: Optional[Iterable[WeaponType]] = None,
|
skip_types: Optional[Iterable[WeaponType]] = None,
|
||||||
) -> Optional[Weapon]:
|
) -> Optional[Weapon]:
|
||||||
if skip_types is None:
|
if skip_types is None:
|
||||||
@@ -62,16 +53,14 @@ class Loadout:
|
|||||||
for fallback in weapon.fallbacks:
|
for fallback in weapon.fallbacks:
|
||||||
if not pylon.can_equip(fallback):
|
if not pylon.can_equip(fallback):
|
||||||
continue
|
continue
|
||||||
if not fallback.available_on(date, faction):
|
if not fallback.available_on(date):
|
||||||
continue
|
continue
|
||||||
if fallback.weapon_group.type in skip_types:
|
if fallback.weapon_group.type in skip_types:
|
||||||
continue
|
continue
|
||||||
return fallback
|
return fallback
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def degrade_for_date(
|
def degrade_for_date(self, unit_type: AircraftType, date: datetime.date) -> Loadout:
|
||||||
self, unit_type: AircraftType, date: datetime.date, faction: Faction
|
|
||||||
) -> Loadout:
|
|
||||||
if self.date is not None and self.date <= date:
|
if self.date is not None and self.date <= date:
|
||||||
return Loadout(self.name, self.pylons, self.date, self.is_custom)
|
return Loadout(self.name, self.pylons, self.date, self.is_custom)
|
||||||
|
|
||||||
@@ -80,9 +69,9 @@ class Loadout:
|
|||||||
if weapon is None:
|
if weapon is None:
|
||||||
del new_pylons[pylon_number]
|
del new_pylons[pylon_number]
|
||||||
continue
|
continue
|
||||||
if not weapon.available_on(date, faction):
|
if not weapon.available_on(date):
|
||||||
pylon = Pylon.for_aircraft(unit_type, pylon_number)
|
pylon = Pylon.for_aircraft(unit_type, pylon_number)
|
||||||
fallback = self._fallback_for(weapon, pylon, date, faction)
|
fallback = self._fallback_for(weapon, pylon, date)
|
||||||
if fallback is None:
|
if fallback is None:
|
||||||
del new_pylons[pylon_number]
|
del new_pylons[pylon_number]
|
||||||
else:
|
else:
|
||||||
@@ -94,11 +83,11 @@ class Loadout:
|
|||||||
# If the loadout was chosen explicitly by the user, assume they know what
|
# If the loadout was chosen explicitly by the user, assume they know what
|
||||||
# they're doing. They may be coordinating buddy-lase.
|
# they're doing. They may be coordinating buddy-lase.
|
||||||
if not loadout.is_custom:
|
if not loadout.is_custom:
|
||||||
loadout.replace_lgbs_if_no_tgp(unit_type, date, faction)
|
loadout.replace_lgbs_if_no_tgp(unit_type, date)
|
||||||
return loadout
|
return loadout
|
||||||
|
|
||||||
def replace_lgbs_if_no_tgp(
|
def replace_lgbs_if_no_tgp(
|
||||||
self, unit_type: AircraftType, date: datetime.date, faction: Faction
|
self, unit_type: AircraftType, date: datetime.date
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.has_weapon_of_type(WeaponType.TGP):
|
if self.has_weapon_of_type(WeaponType.TGP):
|
||||||
return
|
return
|
||||||
@@ -111,7 +100,7 @@ class Loadout:
|
|||||||
if weapon is not None and weapon.weapon_group.type is WeaponType.LGB:
|
if weapon is not None and weapon.weapon_group.type is WeaponType.LGB:
|
||||||
pylon = Pylon.for_aircraft(unit_type, pylon_number)
|
pylon = Pylon.for_aircraft(unit_type, pylon_number)
|
||||||
fallback = self._fallback_for(
|
fallback = self._fallback_for(
|
||||||
weapon, pylon, date, faction, skip_types={WeaponType.LGB}
|
weapon, pylon, date, skip_types={WeaponType.LGB}
|
||||||
)
|
)
|
||||||
if fallback is None:
|
if fallback is None:
|
||||||
del new_pylons[pylon_number]
|
del new_pylons[pylon_number]
|
||||||
@@ -119,24 +108,6 @@ class Loadout:
|
|||||||
new_pylons[pylon_number] = fallback
|
new_pylons[pylon_number] = fallback
|
||||||
self.pylons = new_pylons
|
self.pylons = new_pylons
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def convert_dcs_loadout_to_pylon_map(
|
|
||||||
cls, pylons: dict[int, dict[str, Any]]
|
|
||||||
) -> dict[int, Weapon | None]:
|
|
||||||
return {
|
|
||||||
p["num"]: Weapon.with_clsid(p["CLSID"])
|
|
||||||
for p in pylons.values()
|
|
||||||
# When unloading incompatible pylons (for example, some of the
|
|
||||||
# Mosquito's pylons cannot be loaded when other pylons are carrying
|
|
||||||
# rockets), DCS sometimes equips the empty string rather than
|
|
||||||
# unsetting the pylon. An unset pylon and the empty string appear to
|
|
||||||
# have identical behavior, and it's annoying to deal with weapons
|
|
||||||
# that pydcs doesn't know about, so just clear those pylons rather
|
|
||||||
# than explicitly handling "".
|
|
||||||
# https://github.com/dcs-liberation/dcs_liberation/issues/3171
|
|
||||||
if p["CLSID"] != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def iter_for(cls, flight: Flight) -> Iterator[Loadout]:
|
def iter_for(cls, flight: Flight) -> Iterator[Loadout]:
|
||||||
return cls.iter_for_aircraft(flight.unit_type)
|
return cls.iter_for_aircraft(flight.unit_type)
|
||||||
@@ -154,15 +125,14 @@ class Loadout:
|
|||||||
payloads = aircraft.dcs_unit_type.load_payloads()
|
payloads = aircraft.dcs_unit_type.load_payloads()
|
||||||
for payload in payloads.values():
|
for payload in payloads.values():
|
||||||
name = payload["name"]
|
name = payload["name"]
|
||||||
|
pylons = payload["pylons"]
|
||||||
try:
|
try:
|
||||||
pylon_assignments = cls.convert_dcs_loadout_to_pylon_map(
|
pylon_assignments = {
|
||||||
payload["pylons"]
|
p["num"]: Weapon.with_clsid(p["CLSID"]) for p in pylons.values()
|
||||||
)
|
}
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logging.exception(
|
logging.exception(
|
||||||
"Ignoring %s loadout with invalid weapons: %s",
|
"Ignoring %s loadout with invalid weapons: %s", aircraft.name, name
|
||||||
aircraft.variant_id,
|
|
||||||
name,
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -234,25 +204,7 @@ class Loadout:
|
|||||||
payload = dcs_unit_type.loadout_by_name(name)
|
payload = dcs_unit_type.loadout_by_name(name)
|
||||||
if payload is not None:
|
if payload is not None:
|
||||||
try:
|
try:
|
||||||
# Pydcs returns the data in a different format for loadout_by_name()
|
pylons = {i: Weapon.with_clsid(d["clsid"]) for i, d in payload}
|
||||||
# than it does for load_payloads(), for some reason. Convert this
|
|
||||||
# result to match the other so that we can reuse
|
|
||||||
# convert_dcs_loadout_to_pylon_map.
|
|
||||||
#
|
|
||||||
# loadout_by_name() returns a list of pairs, with the first item
|
|
||||||
# being the pylon index and the second being a dict with a single
|
|
||||||
# clsid key.
|
|
||||||
#
|
|
||||||
# Each element of load_payloads() pylons is a dict of dicts with
|
|
||||||
# both the CLSID key (yes, different case from the other API!) and a
|
|
||||||
# num key for the pylon index. The outer dict is a mapping for a lua
|
|
||||||
# table, so its keys are just indexes.
|
|
||||||
pylons = cls.convert_dcs_loadout_to_pylon_map(
|
|
||||||
{
|
|
||||||
i: {"num": n, "CLSID": p["clsid"]}
|
|
||||||
for i, (n, p) in enumerate(payload)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logging.exception(
|
logging.exception(
|
||||||
"Ignoring %s loadout with invalid weapons: %s",
|
"Ignoring %s loadout with invalid weapons: %s",
|
||||||
|
|||||||
@@ -6,11 +6,8 @@ from typing import TYPE_CHECKING
|
|||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
from game.ato.flightplans.waypointbuilder import WaypointBuilder
|
||||||
from game.flightplan import JoinZoneGeometry
|
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
|
||||||
from game.flightplan.ipsolver import IpSolver
|
|
||||||
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
|
||||||
from game.persistence.paths import waypoint_debug_directory
|
|
||||||
from game.utils import dcs_to_shapely_point
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.ato import Package
|
from game.ato import Package
|
||||||
@@ -25,28 +22,15 @@ class PackageWaypoints:
|
|||||||
refuel: Point
|
refuel: Point
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(
|
def create(package: Package, coalition: Coalition) -> PackageWaypoints:
|
||||||
package: Package, coalition: Coalition, dump_debug_info: bool
|
|
||||||
) -> PackageWaypoints:
|
|
||||||
origin = package.departure_closest_to_target()
|
origin = package.departure_closest_to_target()
|
||||||
|
|
||||||
# Start by picking the best IP for the attack.
|
# Start by picking the best IP for the attack.
|
||||||
ip_solver = IpSolver(
|
ingress_point = IpZoneGeometry(
|
||||||
dcs_to_shapely_point(origin.position),
|
package.target.position,
|
||||||
dcs_to_shapely_point(package.target.position),
|
origin.position,
|
||||||
coalition.doctrine,
|
coalition,
|
||||||
coalition.opponent.threat_zone.all,
|
).find_best_ip()
|
||||||
)
|
|
||||||
ip_solver.set_debug_properties(
|
|
||||||
waypoint_debug_directory() / "IP", coalition.game.theater.terrain
|
|
||||||
)
|
|
||||||
ingress_point_shapely = ip_solver.solve()
|
|
||||||
if dump_debug_info:
|
|
||||||
ip_solver.dump_debug_info()
|
|
||||||
|
|
||||||
ingress_point = origin.position.new_in_same_map(
|
|
||||||
ingress_point_shapely.x, ingress_point_shapely.y
|
|
||||||
)
|
|
||||||
|
|
||||||
join_point = JoinZoneGeometry(
|
join_point = JoinZoneGeometry(
|
||||||
package.target.position,
|
package.target.position,
|
||||||
|
|||||||
14
game/ato/task.py
Normal file
14
game/ato/task.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from game.ato import FlightType
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Task:
|
||||||
|
"""The main task of a flight or package."""
|
||||||
|
|
||||||
|
#: The type of task.
|
||||||
|
task_type: FlightType
|
||||||
|
|
||||||
|
#: The location of the objective.
|
||||||
|
location: str
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from game.utils import Distance, SPEED_OF_SOUND_AT_SEA_LEVEL, Speed, mach
|
from dcs.mapping import Point
|
||||||
|
|
||||||
|
from game.utils import (
|
||||||
|
Distance,
|
||||||
|
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||||
|
Speed,
|
||||||
|
mach,
|
||||||
|
meters,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .flight import Flight
|
from .flight import Flight
|
||||||
@@ -18,9 +26,6 @@ class GroundSpeed:
|
|||||||
# on fuel, but mission speed will be fast enough to keep the flight
|
# on fuel, but mission speed will be fast enough to keep the flight
|
||||||
# safer.
|
# safer.
|
||||||
|
|
||||||
if flight.squadron.aircraft.cruise_speed is not None:
|
|
||||||
return mach(flight.squadron.aircraft.cruise_speed.mach(), altitude)
|
|
||||||
|
|
||||||
# DCS's max speed is in kph at 0 MSL.
|
# DCS's max speed is in kph at 0 MSL.
|
||||||
max_speed = flight.unit_type.max_speed
|
max_speed = flight.unit_type.max_speed
|
||||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
||||||
@@ -37,6 +42,14 @@ class GroundSpeed:
|
|||||||
return mach(cruise_mach, altitude)
|
return mach(cruise_mach, altitude)
|
||||||
|
|
||||||
|
|
||||||
|
class TravelTime:
|
||||||
|
@staticmethod
|
||||||
|
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
||||||
|
error_factor = 1.05
|
||||||
|
distance = meters(a.distance_to_point(b))
|
||||||
|
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Most if not all of this should move into FlightPlan.
|
# TODO: Most if not all of this should move into FlightPlan.
|
||||||
class TotEstimator:
|
class TotEstimator:
|
||||||
def __init__(self, package: Package) -> None:
|
def __init__(self, package: Package) -> None:
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""Support for working with DCS group callsigns."""
|
"""Support for working with DCS group callsigns."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
from abc import ABC
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import StrEnum
|
|
||||||
|
|
||||||
from collections import deque
|
|
||||||
from typing import Any, List, Optional
|
|
||||||
|
|
||||||
from dcs.country import Country
|
|
||||||
from dcs.countries import countries_by_name
|
|
||||||
|
|
||||||
from game.ato.flight import Flight
|
|
||||||
from game.ato.flighttype import FlightType
|
|
||||||
|
|
||||||
|
|
||||||
MAX_GROUP_ID = 99
|
|
||||||
|
|
||||||
|
|
||||||
class CallsignCategory(StrEnum):
|
|
||||||
AIR = "Air"
|
|
||||||
TANKERS = "Tankers"
|
|
||||||
AWACS = "AWACS"
|
|
||||||
GROUND_UNITS = "GroundUnits"
|
|
||||||
HELIPADS = "Helipad"
|
|
||||||
GRASS_AIRFIELDS = "GrassAirfield"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Callsign:
|
|
||||||
name: Optional[
|
|
||||||
str
|
|
||||||
] # Callsign name e.g. "Enfield" for western callsigns. None for eastern callsigns.
|
|
||||||
group_id: int # ID of the group e.g. 2 in Enfield-2-3 for western callsigns. First two digits of eastern callsigns.
|
|
||||||
unit_id: int # ID of the unit e.g. 3 in Enfield-2-3 for western callsigns. Last digit of eastern callsigns.
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
if self.group_id < 1 or self.group_id > MAX_GROUP_ID:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid group ID {self.group_id}. Group IDs have to be between 1 and {MAX_GROUP_ID}."
|
|
||||||
)
|
|
||||||
if self.unit_id < 1 or self.unit_id > 9:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid unit ID {self.unit_id}. Unit IDs have to be between 1 and 9."
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
if self.name is not None:
|
|
||||||
return f"{self.name}{self.group_id}{self.unit_id}"
|
|
||||||
else:
|
|
||||||
return str(self.group_id * 10 + self.unit_id)
|
|
||||||
|
|
||||||
def lead_callsign(self) -> Callsign:
|
|
||||||
return Callsign(self.name, self.group_id, 1)
|
|
||||||
|
|
||||||
def unit_callsign(self, unit_id: int) -> Callsign:
|
|
||||||
return Callsign(self.name, self.group_id, unit_id)
|
|
||||||
|
|
||||||
def group_name(self) -> str:
|
|
||||||
if self.name is not None:
|
|
||||||
return f"{self.name}-{self.group_id}"
|
|
||||||
else:
|
|
||||||
return str(self.lead_callsign())
|
|
||||||
|
|
||||||
def pydcs_dict(self, country: str) -> dict[Any, Any]:
|
|
||||||
country_obj = countries_by_name[country]()
|
|
||||||
for category in CallsignCategory:
|
|
||||||
if category in country_obj.callsign:
|
|
||||||
for index, name in enumerate(country_obj.callsign[category]):
|
|
||||||
if name == self.name:
|
|
||||||
return {
|
|
||||||
"name": str(self),
|
|
||||||
1: index + 1,
|
|
||||||
2: self.group_id,
|
|
||||||
3: self.unit_id,
|
|
||||||
}
|
|
||||||
raise ValueError(f"Could not find callsign {name} in {country}.")
|
|
||||||
|
|
||||||
|
|
||||||
class WesternGroupIdRegistry:
|
|
||||||
|
|
||||||
def __init__(self, country: Country, max_group_id: int = MAX_GROUP_ID):
|
|
||||||
self._names: dict[str, deque[int]] = {}
|
|
||||||
for category in CallsignCategory:
|
|
||||||
if category in country.callsign:
|
|
||||||
for name in country.callsign[category]:
|
|
||||||
self._names[name] = deque()
|
|
||||||
self._max_group_id = max_group_id
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
for name in self._names:
|
|
||||||
self._names[name] = deque()
|
|
||||||
for i in range(
|
|
||||||
self._max_group_id, 0, -1
|
|
||||||
): # Put group IDs on FIFO queue so 1 gets popped first
|
|
||||||
self._names[name].appendleft(i)
|
|
||||||
|
|
||||||
def alloc_group_id(self, name: str) -> int:
|
|
||||||
return self._names[name].popleft()
|
|
||||||
|
|
||||||
def release_group_id(self, callsign: Callsign) -> None:
|
|
||||||
if callsign.name is None:
|
|
||||||
raise ValueError("Releasing eastern callsign")
|
|
||||||
self._names[callsign.name].appendleft(callsign.group_id)
|
|
||||||
|
|
||||||
|
|
||||||
class EasternGroupIdRegistry:
|
|
||||||
|
|
||||||
def __init__(self, max_group_id: int = MAX_GROUP_ID):
|
|
||||||
self._max_group_id = max_group_id
|
|
||||||
self._queue: deque[int] = deque()
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self._queue = deque()
|
|
||||||
for i in range(
|
|
||||||
self._max_group_id, 0, -1
|
|
||||||
): # Put group IDs on FIFO queue so 1 gets popped first
|
|
||||||
self._queue.appendleft(i)
|
|
||||||
|
|
||||||
def alloc_group_id(self) -> int:
|
|
||||||
return self._queue.popleft()
|
|
||||||
|
|
||||||
def release_group_id(self, callsign: Callsign) -> None:
|
|
||||||
self._queue.appendleft(callsign.group_id)
|
|
||||||
|
|
||||||
|
|
||||||
class RoundRobinNameAllocator:
|
|
||||||
|
|
||||||
def __init__(self, names: List[str]):
|
|
||||||
self.names = names
|
|
||||||
self._index = 0
|
|
||||||
|
|
||||||
def allocate(self) -> str:
|
|
||||||
this_index = self._index
|
|
||||||
if this_index == len(self.names) - 1:
|
|
||||||
self._index = 0
|
|
||||||
else:
|
|
||||||
self._index += 1
|
|
||||||
return self.names[this_index]
|
|
||||||
|
|
||||||
|
|
||||||
class FlightTypeNameAllocator:
|
|
||||||
def __init__(self, names: List[str]):
|
|
||||||
self.names = names
|
|
||||||
|
|
||||||
def allocate(self, flight: Flight) -> str:
|
|
||||||
index = self.FLIGHT_TYPE_LOOKUP.get(flight.flight_type, 0)
|
|
||||||
return self.names[index]
|
|
||||||
|
|
||||||
FLIGHT_TYPE_LOOKUP: dict[FlightType, int] = {
|
|
||||||
FlightType.TARCAP: 1,
|
|
||||||
FlightType.BARCAP: 1,
|
|
||||||
FlightType.INTERCEPTION: 1,
|
|
||||||
FlightType.SWEEP: 1,
|
|
||||||
FlightType.CAS: 2,
|
|
||||||
FlightType.ANTISHIP: 2,
|
|
||||||
FlightType.BAI: 2,
|
|
||||||
FlightType.STRIKE: 3,
|
|
||||||
FlightType.OCA_RUNWAY: 3,
|
|
||||||
FlightType.OCA_AIRCRAFT: 3,
|
|
||||||
FlightType.SEAD: 4,
|
|
||||||
FlightType.DEAD: 4,
|
|
||||||
FlightType.ESCORT: 5,
|
|
||||||
FlightType.AIR_ASSAULT: 6,
|
|
||||||
FlightType.TRANSPORT: 7,
|
|
||||||
FlightType.FERRY: 7,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class WesternFlightCallsignGenerator:
|
|
||||||
"""Generate western callsign for lead unit in a group"""
|
|
||||||
|
|
||||||
def __init__(self, country: str) -> None:
|
|
||||||
self._country = countries_by_name[country]()
|
|
||||||
self._group_id_registry = WesternGroupIdRegistry(self._country)
|
|
||||||
self._awacs_name_allocator = None
|
|
||||||
self._tankers_name_allocator = None
|
|
||||||
|
|
||||||
if CallsignCategory.AWACS in self._country.callsign:
|
|
||||||
self._awacs_name_allocator = RoundRobinNameAllocator(
|
|
||||||
self._country.callsign[CallsignCategory.AWACS]
|
|
||||||
)
|
|
||||||
if CallsignCategory.TANKERS in self._country.callsign:
|
|
||||||
self._tankers_name_allocator = RoundRobinNameAllocator(
|
|
||||||
self._country.callsign[CallsignCategory.TANKERS]
|
|
||||||
)
|
|
||||||
self._air_name_allocator = FlightTypeNameAllocator(
|
|
||||||
self._country.callsign[CallsignCategory.AIR]
|
|
||||||
)
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self._group_id_registry.reset()
|
|
||||||
|
|
||||||
def alloc_callsign(self, flight: Flight) -> Callsign:
|
|
||||||
if flight.flight_type == FlightType.AEWC:
|
|
||||||
if self._awacs_name_allocator is None:
|
|
||||||
raise ValueError(f"{self._country.name} does not have AWACs callsigns")
|
|
||||||
name = self._awacs_name_allocator.allocate()
|
|
||||||
elif flight.flight_type == FlightType.REFUELING:
|
|
||||||
if self._tankers_name_allocator is None:
|
|
||||||
raise ValueError(f"{self._country.name} does not have tanker callsigns")
|
|
||||||
name = self._tankers_name_allocator.allocate()
|
|
||||||
else:
|
|
||||||
name = self._air_name_allocator.allocate(flight)
|
|
||||||
group_id = self._group_id_registry.alloc_group_id(name)
|
|
||||||
return Callsign(name, group_id, 1)
|
|
||||||
|
|
||||||
def release_callsign(self, callsign: Callsign) -> None:
|
|
||||||
self._group_id_registry.release_group_id(callsign)
|
|
||||||
|
|
||||||
|
|
||||||
class EasternFlightCallsignGenerator:
|
|
||||||
"""Generate eastern callsign for lead unit in a group"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._group_id_registry = EasternGroupIdRegistry()
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self._group_id_registry.reset()
|
|
||||||
|
|
||||||
def alloc_callsign(self, flight: Flight) -> Callsign:
|
|
||||||
group_id = self._group_id_registry.alloc_group_id()
|
|
||||||
return Callsign(None, group_id, 1)
|
|
||||||
|
|
||||||
def release_callsign(self, callsign: Callsign) -> None:
|
|
||||||
self._group_id_registry.release_group_id(callsign)
|
|
||||||
|
|
||||||
|
|
||||||
class FlightCallsignGenerator:
|
|
||||||
|
|
||||||
def __init__(self, country: str):
|
|
||||||
self._use_western_callsigns = countries_by_name[country]().use_western_callsigns
|
|
||||||
self._generators: dict[
|
|
||||||
bool, WesternFlightCallsignGenerator | EasternFlightCallsignGenerator
|
|
||||||
] = {}
|
|
||||||
if self._use_western_callsigns:
|
|
||||||
self._generators[self._use_western_callsigns] = (
|
|
||||||
WesternFlightCallsignGenerator(country)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._generators[self._use_western_callsigns] = (
|
|
||||||
EasternFlightCallsignGenerator()
|
|
||||||
)
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
|
||||||
self._generators[self._use_western_callsigns].reset()
|
|
||||||
|
|
||||||
def alloc_callsign(self, flight: Flight) -> Callsign:
|
|
||||||
return self._generators[self._use_western_callsigns].alloc_callsign(flight)
|
|
||||||
|
|
||||||
def release_callsign(self, callsign: Callsign) -> None:
|
|
||||||
self._generators[self._use_western_callsigns].release_callsign(callsign)
|
|
||||||
@@ -17,7 +17,6 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
|
|||||||
from game.theater.theaterloader import TheaterLoader
|
from game.theater.theaterloader import TheaterLoader
|
||||||
from game.version import CAMPAIGN_FORMAT_VERSION
|
from game.version import CAMPAIGN_FORMAT_VERSION
|
||||||
from .campaignairwingconfig import CampaignAirWingConfig
|
from .campaignairwingconfig import CampaignAirWingConfig
|
||||||
from .controlpointconfig import ControlPointConfig
|
|
||||||
from .factionrecommendation import FactionRecommendation
|
from .factionrecommendation import FactionRecommendation
|
||||||
from .mizcampaignloader import MizCampaignLoader
|
from .mizcampaignloader import MizCampaignLoader
|
||||||
|
|
||||||
@@ -124,15 +123,7 @@ class Campaign:
|
|||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
with logged_duration("Importing miz data"):
|
with logged_duration("Importing miz data"):
|
||||||
MizCampaignLoader(
|
MizCampaignLoader(self.path.parent / miz, t).populate_theater()
|
||||||
self.path.parent / miz,
|
|
||||||
t,
|
|
||||||
dict(
|
|
||||||
ControlPointConfig.iter_from_data(
|
|
||||||
self.data.get("control_points", {})
|
|
||||||
)
|
|
||||||
),
|
|
||||||
).populate_theater()
|
|
||||||
|
|
||||||
# TODO: Move into MizCampaignLoader so this doesn't have unknown initialization
|
# TODO: Move into MizCampaignLoader so this doesn't have unknown initialization
|
||||||
# in ConflictTheater.
|
# in ConflictTheater.
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
from dcs import Point
|
|
||||||
from dcs.terrain import Airport
|
|
||||||
|
|
||||||
from game.campaignloader.controlpointconfig import ControlPointConfig
|
|
||||||
from game.theater import (
|
|
||||||
Airfield,
|
|
||||||
Carrier,
|
|
||||||
ConflictTheater,
|
|
||||||
ControlPoint,
|
|
||||||
Fob,
|
|
||||||
Lha,
|
|
||||||
OffMapSpawn,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ControlPointBuilder:
|
|
||||||
def __init__(
|
|
||||||
self, theater: ConflictTheater, configs: dict[str | int, ControlPointConfig]
|
|
||||||
) -> None:
|
|
||||||
self.theater = theater
|
|
||||||
self.config = configs
|
|
||||||
|
|
||||||
def create_airfield(self, airport: Airport) -> Airfield:
|
|
||||||
cp = Airfield(airport, self.theater, 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".
|
|
||||||
cp.captured_invert = airport.unlimited_aircrafts
|
|
||||||
|
|
||||||
self._apply_config(airport.id, cp)
|
|
||||||
return cp
|
|
||||||
|
|
||||||
def create_fob(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: Point,
|
|
||||||
theater: ConflictTheater,
|
|
||||||
starts_blue: bool,
|
|
||||||
captured_invert: bool,
|
|
||||||
) -> Fob:
|
|
||||||
cp = Fob(name, position, theater, starts_blue)
|
|
||||||
cp.captured_invert = captured_invert
|
|
||||||
self._apply_config(name, cp)
|
|
||||||
return cp
|
|
||||||
|
|
||||||
def create_carrier(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: Point,
|
|
||||||
theater: ConflictTheater,
|
|
||||||
starts_blue: bool,
|
|
||||||
captured_invert: bool,
|
|
||||||
) -> Carrier:
|
|
||||||
cp = Carrier(name, position, theater, starts_blue)
|
|
||||||
cp.captured_invert = captured_invert
|
|
||||||
self._apply_config(name, cp)
|
|
||||||
return cp
|
|
||||||
|
|
||||||
def create_lha(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: Point,
|
|
||||||
theater: ConflictTheater,
|
|
||||||
starts_blue: bool,
|
|
||||||
captured_invert: bool,
|
|
||||||
) -> Lha:
|
|
||||||
cp = Lha(name, position, theater, starts_blue)
|
|
||||||
cp.captured_invert = captured_invert
|
|
||||||
self._apply_config(name, cp)
|
|
||||||
return cp
|
|
||||||
|
|
||||||
def create_off_map(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: Point,
|
|
||||||
theater: ConflictTheater,
|
|
||||||
starts_blue: bool,
|
|
||||||
captured_invert: bool,
|
|
||||||
) -> OffMapSpawn:
|
|
||||||
cp = OffMapSpawn(name, position, theater, starts_blue)
|
|
||||||
cp.captured_invert = captured_invert
|
|
||||||
self._apply_config(name, cp)
|
|
||||||
return cp
|
|
||||||
|
|
||||||
def _apply_config(self, cp_id: str | int, control_point: ControlPoint) -> None:
|
|
||||||
config = self.config.get(cp_id)
|
|
||||||
if config is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
control_point.ferry_only = config.ferry_only
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Iterator
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ControlPointConfig:
|
|
||||||
ferry_only: bool
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_data(data: dict[str, Any]) -> ControlPointConfig:
|
|
||||||
return ControlPointConfig(ferry_only=data.get("ferry_only", False))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def iter_from_data(
|
|
||||||
data: dict[str | int, Any]
|
|
||||||
) -> Iterator[tuple[str | int, ControlPointConfig]]:
|
|
||||||
for name_or_id, cp_data in data.items():
|
|
||||||
yield name_or_id, ControlPointConfig.from_data(cp_data)
|
|
||||||
@@ -29,6 +29,7 @@ class DefaultSquadronAssigner:
|
|||||||
self.coalition.player
|
self.coalition.player
|
||||||
):
|
):
|
||||||
for squadron_config in self.config.by_location[control_point]:
|
for squadron_config in self.config.by_location[control_point]:
|
||||||
|
|
||||||
squadron_def = self.override_squadron_defaults(
|
squadron_def = self.override_squadron_defaults(
|
||||||
self.find_squadron_for(squadron_config, control_point),
|
self.find_squadron_for(squadron_config, control_point),
|
||||||
squadron_config,
|
squadron_config,
|
||||||
@@ -161,6 +162,7 @@ class DefaultSquadronAssigner:
|
|||||||
def override_squadron_defaults(
|
def override_squadron_defaults(
|
||||||
squadron_def: Optional[SquadronDef], config: SquadronConfig
|
squadron_def: Optional[SquadronDef], config: SquadronConfig
|
||||||
) -> Optional[SquadronDef]:
|
) -> Optional[SquadronDef]:
|
||||||
|
|
||||||
if squadron_def is None:
|
if squadron_def is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ class FactionRecommendation(ABC):
|
|||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def register_campaign_specific_faction(self, factions: Factions) -> None: ...
|
def register_campaign_specific_faction(self, factions: Factions) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_faction(self, factions: Factions) -> Faction: ...
|
def get_faction(self, factions: Factions) -> Faction:
|
||||||
|
...
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_field(
|
def from_field(
|
||||||
|
|||||||
@@ -12,14 +12,20 @@ from dcs.country import Country
|
|||||||
from dcs.planes import F_15C
|
from dcs.planes import F_15C
|
||||||
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
|
from dcs.ships import HandyWind, LHA_Tarawa, Stennis, USS_Arleigh_Burke_IIa
|
||||||
from dcs.statics import Fortification, Warehouse
|
from dcs.statics import Fortification, Warehouse
|
||||||
|
from dcs.terrain import Airport
|
||||||
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
|
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
|
||||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||||
|
|
||||||
from game.campaignloader.controlpointbuilder import ControlPointBuilder
|
|
||||||
from game.campaignloader.controlpointconfig import ControlPointConfig
|
|
||||||
from game.profiling import logged_duration
|
from game.profiling import logged_duration
|
||||||
from game.scenery_group import SceneryGroup
|
from game.scenery_group import SceneryGroup
|
||||||
from game.theater.controlpoint import ControlPoint
|
from game.theater.controlpoint import (
|
||||||
|
Airfield,
|
||||||
|
Carrier,
|
||||||
|
ControlPoint,
|
||||||
|
Fob,
|
||||||
|
Lha,
|
||||||
|
OffMapSpawn,
|
||||||
|
)
|
||||||
from game.theater.presetlocation import PresetLocation
|
from game.theater.presetlocation import PresetLocation
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -61,8 +67,6 @@ class MizCampaignLoader:
|
|||||||
AirDefence.Hawk_ln.id,
|
AirDefence.Hawk_ln.id,
|
||||||
AirDefence.S_75M_Volhov.id,
|
AirDefence.S_75M_Volhov.id,
|
||||||
AirDefence.X_5p73_s_125_ln.id,
|
AirDefence.X_5p73_s_125_ln.id,
|
||||||
AirDefence.NASAMS_LN_B.id,
|
|
||||||
AirDefence.NASAMS_LN_C.id,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SHORT_RANGE_SAM_UNIT_TYPES = {
|
SHORT_RANGE_SAM_UNIT_TYPES = {
|
||||||
@@ -88,14 +92,8 @@ class MizCampaignLoader:
|
|||||||
|
|
||||||
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||||
self,
|
|
||||||
miz: Path,
|
|
||||||
theater: ConflictTheater,
|
|
||||||
control_point_configs: dict[str | int, ControlPointConfig],
|
|
||||||
) -> None:
|
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
self.control_point_builder = ControlPointBuilder(theater, control_point_configs)
|
|
||||||
self.mission = Mission()
|
self.mission = Mission()
|
||||||
with logged_duration("Loading miz"):
|
with logged_duration("Loading miz"):
|
||||||
self.mission.load_file(str(miz))
|
self.mission.load_file(str(miz))
|
||||||
@@ -107,6 +105,15 @@ class MizCampaignLoader:
|
|||||||
if self.mission.country(self.RED_COUNTRY.name) is None:
|
if self.mission.country(self.RED_COUNTRY.name) is None:
|
||||||
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
||||||
|
|
||||||
|
def control_point_from_airport(self, airport: Airport) -> ControlPoint:
|
||||||
|
cp = Airfield(airport, self.theater, 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".
|
||||||
|
cp.captured_invert = airport.unlimited_aircrafts
|
||||||
|
|
||||||
|
return cp
|
||||||
|
|
||||||
def country(self, blue: bool) -> Country:
|
def country(self, blue: bool) -> Country:
|
||||||
country = self.mission.country(
|
country = self.mission.country(
|
||||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
|
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
|
||||||
@@ -233,49 +240,36 @@ class MizCampaignLoader:
|
|||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def control_points(self) -> dict[UUID, ControlPoint]:
|
def control_points(self) -> dict[UUID, ControlPoint]:
|
||||||
control_points: dict[UUID, ControlPoint] = {}
|
control_points = {}
|
||||||
control_point: ControlPoint
|
|
||||||
for airport in self.mission.terrain.airport_list():
|
for airport in self.mission.terrain.airport_list():
|
||||||
if airport.is_blue() or airport.is_red():
|
if airport.is_blue() or airport.is_red():
|
||||||
control_point = self.control_point_builder.create_airfield(airport)
|
control_point = self.control_point_from_airport(airport)
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
for blue in (False, True):
|
for blue in (False, True):
|
||||||
for group in self.off_map_spawns(blue):
|
for group in self.off_map_spawns(blue):
|
||||||
control_point = self.control_point_builder.create_off_map(
|
control_point = OffMapSpawn(
|
||||||
str(group.name),
|
str(group.name), group.position, self.theater, starts_blue=blue
|
||||||
group.position,
|
|
||||||
self.theater,
|
|
||||||
starts_blue=blue,
|
|
||||||
captured_invert=group.late_activation,
|
|
||||||
)
|
)
|
||||||
|
control_point.captured_invert = group.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for ship in self.carriers(blue):
|
for ship in self.carriers(blue):
|
||||||
control_point = self.control_point_builder.create_carrier(
|
control_point = Carrier(
|
||||||
ship.name,
|
ship.name, ship.position, self.theater, starts_blue=blue
|
||||||
ship.position,
|
|
||||||
self.theater,
|
|
||||||
starts_blue=blue,
|
|
||||||
captured_invert=ship.late_activation,
|
|
||||||
)
|
)
|
||||||
|
control_point.captured_invert = ship.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for ship in self.lhas(blue):
|
for ship in self.lhas(blue):
|
||||||
control_point = self.control_point_builder.create_lha(
|
control_point = Lha(
|
||||||
ship.name,
|
ship.name, ship.position, self.theater, starts_blue=blue
|
||||||
ship.position,
|
|
||||||
self.theater,
|
|
||||||
starts_blue=blue,
|
|
||||||
captured_invert=ship.late_activation,
|
|
||||||
)
|
)
|
||||||
|
control_point.captured_invert = ship.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for fob in self.fobs(blue):
|
for fob in self.fobs(blue):
|
||||||
control_point = self.control_point_builder.create_fob(
|
control_point = Fob(
|
||||||
str(fob.name),
|
str(fob.name), fob.position, self.theater, starts_blue=blue
|
||||||
fob.position,
|
|
||||||
self.theater,
|
|
||||||
starts_blue=blue,
|
|
||||||
captured_invert=fob.late_activation,
|
|
||||||
)
|
)
|
||||||
|
control_point.captured_invert = fob.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
return control_points
|
return control_points
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from faker import Faker
|
|||||||
|
|
||||||
from game.armedforces.armedforces import ArmedForces
|
from game.armedforces.armedforces import ArmedForces
|
||||||
from game.ato.airtaaskingorder import AirTaskingOrder
|
from game.ato.airtaaskingorder import AirTaskingOrder
|
||||||
from game.callsigns.callsigngenerator import FlightCallsignGenerator
|
|
||||||
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
||||||
from game.commander import TheaterCommander
|
from game.commander import TheaterCommander
|
||||||
from game.commander.missionscheduler import MissionScheduler
|
from game.commander.missionscheduler import MissionScheduler
|
||||||
@@ -27,7 +26,6 @@ if TYPE_CHECKING:
|
|||||||
from .data.doctrine import Doctrine
|
from .data.doctrine import Doctrine
|
||||||
from .factions.faction import Faction
|
from .factions.faction import Faction
|
||||||
from .game import Game
|
from .game import Game
|
||||||
from .lasercodes import LaserCodeRegistry
|
|
||||||
from .sim import GameUpdateEvents
|
from .sim import GameUpdateEvents
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +45,6 @@ class Coalition:
|
|||||||
self.air_wing = AirWing(player, game, self.faction)
|
self.air_wing = AirWing(player, game, self.faction)
|
||||||
self.armed_forces = ArmedForces(self.faction)
|
self.armed_forces = ArmedForces(self.faction)
|
||||||
self.transfers = PendingTransfers(game, player)
|
self.transfers = PendingTransfers(game, player)
|
||||||
self.callsign_generator = FlightCallsignGenerator(faction.country)
|
|
||||||
|
|
||||||
# Late initialized because the two coalitions in the game are mutually
|
# Late initialized because the two coalitions in the game are mutually
|
||||||
# dependent, so must be both constructed before this property can be set.
|
# dependent, so must be both constructed before this property can be set.
|
||||||
@@ -93,10 +90,6 @@ class Coalition:
|
|||||||
assert self._navmesh is not None
|
assert self._navmesh is not None
|
||||||
return self._navmesh
|
return self._navmesh
|
||||||
|
|
||||||
@property
|
|
||||||
def laser_code_registry(self) -> LaserCodeRegistry:
|
|
||||||
return self.game.laser_code_registry
|
|
||||||
|
|
||||||
def __getstate__(self) -> dict[str, Any]:
|
def __getstate__(self) -> dict[str, Any]:
|
||||||
state = self.__dict__.copy()
|
state = self.__dict__.copy()
|
||||||
# Avoid persisting any volatile types that can be deterministically
|
# Avoid persisting any volatile types that can be deterministically
|
||||||
@@ -165,14 +158,12 @@ class Coalition:
|
|||||||
# is handled correctly.
|
# is handled correctly.
|
||||||
self.transfers.perform_transfers()
|
self.transfers.perform_transfers()
|
||||||
|
|
||||||
self.callsign_generator.reset()
|
def preinit_turn_0(self, squadrons_start_full: bool) -> None:
|
||||||
|
|
||||||
def preinit_turn_0(self) -> None:
|
|
||||||
"""Runs final Coalition initialization.
|
"""Runs final Coalition initialization.
|
||||||
|
|
||||||
Final initialization occurs before Game.initialize_turn runs for turn 0.
|
Final initialization occurs before Game.initialize_turn runs for turn 0.
|
||||||
"""
|
"""
|
||||||
self.air_wing.populate_for_turn_0()
|
self.air_wing.populate_for_turn_0(squadrons_start_full)
|
||||||
|
|
||||||
def initialize_turn(self, is_turn_0: bool) -> None:
|
def initialize_turn(self, is_turn_0: bool) -> None:
|
||||||
"""Processes coalition-specific turn initialization.
|
"""Processes coalition-specific turn initialization.
|
||||||
@@ -193,7 +184,7 @@ class Coalition:
|
|||||||
with logged_duration("Transport planning"):
|
with logged_duration("Transport planning"):
|
||||||
self.transfers.plan_transports(self.game.conditions.start_time)
|
self.transfers.plan_transports(self.game.conditions.start_time)
|
||||||
|
|
||||||
if not is_turn_0:
|
if not is_turn_0 or not self.game.settings.enable_squadron_aircraft_limits:
|
||||||
self.plan_missions(self.game.conditions.start_time)
|
self.plan_missions(self.game.conditions.start_time)
|
||||||
self.plan_procurement()
|
self.plan_procurement()
|
||||||
|
|
||||||
|
|||||||
@@ -47,17 +47,6 @@ class MissionScheduler:
|
|||||||
margin=5 * 60,
|
margin=5 * 60,
|
||||||
)
|
)
|
||||||
for package in self.coalition.ato.packages:
|
for package in self.coalition.ato.packages:
|
||||||
if package.time_over_target > datetime.min:
|
|
||||||
if package.primary_task in dca_types:
|
|
||||||
if (
|
|
||||||
package.mission_departure_time is not None
|
|
||||||
and package.mission_departure_time
|
|
||||||
> previous_cap_end_time[package.target]
|
|
||||||
):
|
|
||||||
previous_cap_end_time[package.target] = (
|
|
||||||
package.mission_departure_time
|
|
||||||
)
|
|
||||||
continue # If package already has TOT, leave it.
|
|
||||||
tot = TotEstimator(package).earliest_tot(now)
|
tot = TotEstimator(package).earliest_tot(now)
|
||||||
if package.primary_task in dca_types:
|
if package.primary_task in dca_types:
|
||||||
previous_end_time = previous_cap_end_time[package.target]
|
previous_end_time = previous_cap_end_time[package.target]
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from game.utils import meters, nautical_miles
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.transfers import CargoShip, Convoy
|
from game.transfers import CargoShip, Convoy
|
||||||
from game.threatzones import ThreatZones
|
|
||||||
|
|
||||||
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
||||||
|
|
||||||
@@ -194,36 +193,17 @@ class ObjectiveFinder:
|
|||||||
|
|
||||||
def farthest_friendly_control_point(self) -> ControlPoint:
|
def farthest_friendly_control_point(self) -> ControlPoint:
|
||||||
"""Finds the friendly control point that is farthest from any threats."""
|
"""Finds the friendly control point that is farthest from any threats."""
|
||||||
|
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
def find_farthest(
|
|
||||||
control_points: Iterator[ControlPoint],
|
|
||||||
threat_zones: ThreatZones,
|
|
||||||
consider_off_map_spawn: bool,
|
|
||||||
) -> ControlPoint | None:
|
|
||||||
farthest = None
|
farthest = None
|
||||||
max_distance = meters(0)
|
max_distance = meters(0)
|
||||||
for cp in control_points:
|
for cp in self.friendly_control_points():
|
||||||
if isinstance(cp, OffMapSpawn) and not consider_off_map_spawn:
|
if isinstance(cp, OffMapSpawn):
|
||||||
continue
|
continue
|
||||||
distance = threat_zones.distance_to_threat(cp.position)
|
distance = threat_zones.distance_to_threat(cp.position)
|
||||||
if distance > max_distance:
|
if distance > max_distance:
|
||||||
farthest = cp
|
farthest = cp
|
||||||
max_distance = distance
|
max_distance = distance
|
||||||
return farthest
|
|
||||||
|
|
||||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
|
||||||
|
|
||||||
farthest = find_farthest(
|
|
||||||
self.friendly_control_points(), threat_zones, consider_off_map_spawn=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# If there are only off-map spawn control points, fall back to the farthest amongst off map spawn points
|
|
||||||
if farthest is None:
|
|
||||||
farthest = find_farthest(
|
|
||||||
self.friendly_control_points(),
|
|
||||||
threat_zones,
|
|
||||||
consider_off_map_spawn=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if farthest is None:
|
if farthest is None:
|
||||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from game.callsigns.callsigngenerator import FlightCallsignGenerator
|
|
||||||
from game.theater import ControlPoint, MissionTarget, OffMapSpawn
|
from game.theater import ControlPoint, MissionTarget, OffMapSpawn
|
||||||
from game.utils import nautical_miles
|
from game.utils import nautical_miles
|
||||||
from ..ato.flight import Flight
|
from ..ato.flight import Flight
|
||||||
@@ -11,10 +10,9 @@ from ..ato.starttype import StartType
|
|||||||
from ..db.database import Database
|
from ..db.database import Database
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.ato.closestairfields import ClosestAirfields
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.lasercodes import LaserCodeRegistry
|
|
||||||
from game.squadrons.airwing import AirWing
|
from game.squadrons.airwing import AirWing
|
||||||
|
from game.ato.closestairfields import ClosestAirfields
|
||||||
from .missionproposals import ProposedFlight
|
from .missionproposals import ProposedFlight
|
||||||
|
|
||||||
|
|
||||||
@@ -26,8 +24,6 @@ class PackageBuilder:
|
|||||||
location: MissionTarget,
|
location: MissionTarget,
|
||||||
closest_airfields: ClosestAirfields,
|
closest_airfields: ClosestAirfields,
|
||||||
air_wing: AirWing,
|
air_wing: AirWing,
|
||||||
laser_code_registry: LaserCodeRegistry,
|
|
||||||
callsign_generator: FlightCallsignGenerator,
|
|
||||||
flight_db: Database[Flight],
|
flight_db: Database[Flight],
|
||||||
is_player: bool,
|
is_player: bool,
|
||||||
package_country: str,
|
package_country: str,
|
||||||
@@ -39,8 +35,6 @@ class PackageBuilder:
|
|||||||
self.package_country = package_country
|
self.package_country = package_country
|
||||||
self.package = Package(location, flight_db, auto_asap=asap)
|
self.package = Package(location, flight_db, auto_asap=asap)
|
||||||
self.air_wing = air_wing
|
self.air_wing = air_wing
|
||||||
self.laser_code_registry = laser_code_registry
|
|
||||||
self.callsign_generator = callsign_generator
|
|
||||||
self.start_type = start_type
|
self.start_type = start_type
|
||||||
|
|
||||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||||
@@ -69,12 +63,6 @@ class PackageBuilder:
|
|||||||
start_type,
|
start_type,
|
||||||
divert=self.find_divert_field(squadron.aircraft, squadron.location),
|
divert=self.find_divert_field(squadron.aircraft, squadron.location),
|
||||||
)
|
)
|
||||||
for member in flight.iter_members():
|
|
||||||
if member.is_player:
|
|
||||||
member.assign_tgp_laser_code(
|
|
||||||
self.laser_code_registry.alloc_laser_code()
|
|
||||||
)
|
|
||||||
flight.callsign = self.callsign_generator.alloc_callsign(flight)
|
|
||||||
self.package.add_flight(flight)
|
self.package.add_flight(flight)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -104,6 +92,4 @@ class PackageBuilder:
|
|||||||
"""Returns any planned flights to the inventory."""
|
"""Returns any planned flights to the inventory."""
|
||||||
flights = list(self.package.flights)
|
flights = list(self.package.flights)
|
||||||
for flight in flights:
|
for flight in flights:
|
||||||
if flight.callsign is not None:
|
|
||||||
self.callsign_generator.release_callsign(flight.callsign)
|
|
||||||
self.package.remove_flight(flight)
|
self.package.remove_flight(flight)
|
||||||
|
|||||||
@@ -141,8 +141,6 @@ class PackageFulfiller:
|
|||||||
mission.location,
|
mission.location,
|
||||||
ObjectiveDistanceCache.get_closest_airfields(mission.location),
|
ObjectiveDistanceCache.get_closest_airfields(mission.location),
|
||||||
self.air_wing,
|
self.air_wing,
|
||||||
self.coalition.laser_code_registry,
|
|
||||||
self.coalition.callsign_generator,
|
|
||||||
self.flight_db,
|
self.flight_db,
|
||||||
self.is_player,
|
self.is_player,
|
||||||
self.coalition.country_name,
|
self.coalition.country_name,
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def stance(self) -> CombatStance: ...
|
def stance(self) -> CombatStance:
|
||||||
|
...
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def management_allowed(state: TheaterState) -> bool:
|
def management_allowed(state: TheaterState) -> bool:
|
||||||
@@ -48,7 +49,8 @@ class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def have_sufficient_front_line_advantage(self) -> bool: ...
|
def have_sufficient_front_line_advantage(self) -> bool:
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ground_force_balance(self) -> float:
|
def ground_force_balance(self) -> float:
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
coalition.ato.add_package(self.package)
|
coalition.ato.add_package(self.package)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def propose_flights(self) -> None: ...
|
def propose_flights(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
def propose_flight(
|
def propose_flight(
|
||||||
self,
|
self,
|
||||||
@@ -117,9 +118,9 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
|||||||
target_ranges: list[
|
target_ranges: list[
|
||||||
tuple[Union[IadsGroundObject, NavalGroundObject], Distance]
|
tuple[Union[IadsGroundObject, NavalGroundObject], Distance]
|
||||||
] = []
|
] = []
|
||||||
all_iads: Iterator[Union[IadsGroundObject, NavalGroundObject]] = (
|
all_iads: Iterator[
|
||||||
itertools.chain(state.enemy_air_defenses, state.enemy_ships)
|
Union[IadsGroundObject, NavalGroundObject]
|
||||||
)
|
] = itertools.chain(state.enemy_air_defenses, state.enemy_ships)
|
||||||
for target in all_iads:
|
for target in all_iads:
|
||||||
distance = meters(target.distance_to(self.target))
|
distance = meters(target.distance_to(self.target))
|
||||||
if range_type is RangeType.Detection:
|
if range_type is RangeType.Detection:
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class TheaterCommanderTask(PrimitiveTask[TheaterState]):
|
class TheaterCommanderTask(PrimitiveTask[TheaterState]):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def execute(self, coalition: Coalition) -> None: ...
|
def execute(self, coalition: Coalition) -> None:
|
||||||
|
...
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ even though it is a primitive task used by many other tasks.
|
|||||||
|
|
||||||
https://en.wikipedia.org/wiki/Hierarchical_task_network
|
https://en.wikipedia.org/wiki/Hierarchical_task_network
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ from game.theater.theatergroundobject import (
|
|||||||
VehicleGroupGroundObject,
|
VehicleGroupGroundObject,
|
||||||
)
|
)
|
||||||
from game.threatzones import ThreatZones
|
from game.threatzones import ThreatZones
|
||||||
from game.ato.flighttype import FlightType
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@@ -78,7 +77,6 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
self.threatening_air_defenses.remove(target)
|
self.threatening_air_defenses.remove(target)
|
||||||
if target in self.detecting_air_defenses:
|
if target in self.detecting_air_defenses:
|
||||||
self.detecting_air_defenses.remove(target)
|
self.detecting_air_defenses.remove(target)
|
||||||
if target in self.enemy_air_defenses:
|
|
||||||
self.enemy_air_defenses.remove(target)
|
self.enemy_air_defenses.remove(target)
|
||||||
self._rebuild_threat_zones()
|
self._rebuild_threat_zones()
|
||||||
|
|
||||||
@@ -87,7 +85,6 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
self.threatening_air_defenses.remove(target)
|
self.threatening_air_defenses.remove(target)
|
||||||
if target in self.detecting_air_defenses:
|
if target in self.detecting_air_defenses:
|
||||||
self.detecting_air_defenses.remove(target)
|
self.detecting_air_defenses.remove(target)
|
||||||
if target in self.enemy_ships:
|
|
||||||
self.enemy_ships.remove(target)
|
self.enemy_ships.remove(target)
|
||||||
self._rebuild_threat_zones()
|
self._rebuild_threat_zones()
|
||||||
|
|
||||||
@@ -158,16 +155,21 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
tracer,
|
tracer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Plan enough rounds of CAP that the target has coverage over the expected
|
||||||
|
# mission duration.
|
||||||
|
mission_duration = game.settings.desired_player_mission_duration.total_seconds()
|
||||||
|
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
|
||||||
|
barcap_rounds = math.ceil(mission_duration / barcap_duration)
|
||||||
|
|
||||||
refueling_targets: list[MissionTarget] = []
|
refueling_targets: list[MissionTarget] = []
|
||||||
theater_refuling_point = finder.preferred_theater_refueling_control_point()
|
theater_refuling_point = finder.preferred_theater_refueling_control_point()
|
||||||
if theater_refuling_point is not None:
|
if theater_refuling_point is not None:
|
||||||
refueling_targets.append(theater_refuling_point)
|
refueling_targets.append(theater_refuling_point)
|
||||||
|
|
||||||
theater_state = TheaterState(
|
return TheaterState(
|
||||||
context=context,
|
context=context,
|
||||||
barcaps_needed={
|
barcaps_needed={
|
||||||
cp: cls._barcap_rounds(game, player, now, cp)
|
cp: barcap_rounds for cp in finder.vulnerable_control_points()
|
||||||
for cp in finder.vulnerable_control_points()
|
|
||||||
},
|
},
|
||||||
active_front_lines=list(finder.front_lines()),
|
active_front_lines=list(finder.front_lines()),
|
||||||
front_line_stances={f: None for f in finder.front_lines()},
|
front_line_stances={f: None for f in finder.front_lines()},
|
||||||
@@ -189,62 +191,3 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
enemy_barcaps=list(game.theater.control_points_for(not player)),
|
enemy_barcaps=list(game.theater.control_points_for(not player)),
|
||||||
threat_zones=game.threat_zone_for(not player),
|
threat_zones=game.threat_zone_for(not player),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Look through packages already planned in the ATO and eliminate from the
|
|
||||||
# list of targets.
|
|
||||||
for package in coalition.ato.packages:
|
|
||||||
if isinstance(package.target, NavalGroundObject):
|
|
||||||
theater_state.eliminate_ship(package.target)
|
|
||||||
if package.primary_task == FlightType.BAI and isinstance(
|
|
||||||
package.target, VehicleGroupGroundObject
|
|
||||||
):
|
|
||||||
theater_state.eliminate_battle_position(package.target)
|
|
||||||
if isinstance(package.target, IadsGroundObject):
|
|
||||||
theater_state.eliminate_air_defense(package.target)
|
|
||||||
if (
|
|
||||||
package.primary_task == FlightType.STRIKE
|
|
||||||
and isinstance(package.target, TheaterGroundObject)
|
|
||||||
and package.target in theater_state.strike_targets
|
|
||||||
):
|
|
||||||
theater_state.strike_targets.remove(package.target)
|
|
||||||
if package.primary_task == FlightType.AEWC:
|
|
||||||
# If a planned AEWC mission covers the target beyond the planned mission duration, it can safely be removed
|
|
||||||
if (
|
|
||||||
package.time_over_target + coalition.doctrine.aewc.duration
|
|
||||||
> now + game.settings.desired_player_mission_duration
|
|
||||||
) and package.target in theater_state.aewc_targets:
|
|
||||||
theater_state.aewc_targets.remove(package.target)
|
|
||||||
if (
|
|
||||||
package.primary_task
|
|
||||||
in (
|
|
||||||
FlightType.OCA_AIRCRAFT,
|
|
||||||
FlightType.OCA_RUNWAY,
|
|
||||||
)
|
|
||||||
and isinstance(package.target, ControlPoint)
|
|
||||||
and package.target in theater_state.oca_targets
|
|
||||||
):
|
|
||||||
theater_state.oca_targets.remove(package.target)
|
|
||||||
return theater_state
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _barcap_rounds(
|
|
||||||
cls, game: Game, player: bool, now: datetime, control_point: ControlPoint
|
|
||||||
) -> int:
|
|
||||||
"""Calculate number of additional rounds of CAP required to cover mission duration."""
|
|
||||||
coalition = game.coalition_for(player)
|
|
||||||
|
|
||||||
# Look through ATO for any existing planned CAP missions and calculate last planned CAP end
|
|
||||||
planned_cap_coverage_end_time = now
|
|
||||||
for package in coalition.ato.packages:
|
|
||||||
if package.target == control_point:
|
|
||||||
cap_end_time = (
|
|
||||||
package.time_over_target + coalition.doctrine.cap.duration
|
|
||||||
)
|
|
||||||
if cap_end_time > planned_cap_coverage_end_time:
|
|
||||||
planned_cap_coverage_end_time = cap_end_time
|
|
||||||
# When mission is expected to finish
|
|
||||||
mission_end_time = now + game.settings.desired_player_mission_duration
|
|
||||||
return math.ceil(
|
|
||||||
(mission_end_time - planned_cap_coverage_end_time).total_seconds()
|
|
||||||
/ coalition.doctrine.cap.duration.total_seconds()
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
import yaml
|
|
||||||
from typing import Any, ClassVar, Optional
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from dcs.task import OptAAMissileAttackRange
|
|
||||||
from game.data.units import UnitClass
|
from game.data.units import UnitClass
|
||||||
from game.utils import Distance, feet, nautical_miles
|
from game.utils import Distance, feet, nautical_miles
|
||||||
|
|
||||||
@@ -21,133 +15,16 @@ class GroundUnitProcurementRatios:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict[str, float]) -> GroundUnitProcurementRatios:
|
|
||||||
unit_class_enum_from_name = {unit.value: unit for unit in UnitClass}
|
|
||||||
r = {}
|
|
||||||
for unit_class in data:
|
|
||||||
if unit_class not in unit_class_enum_from_name:
|
|
||||||
raise ValueError(f"Could not find unit type {unit_class}")
|
|
||||||
r[unit_class_enum_from_name[unit_class]] = float(data[unit_class])
|
|
||||||
return GroundUnitProcurementRatios(r)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Helicopter:
|
|
||||||
#: The altitude used for combat section of a flight, overrides the base combat_altitude parameter for helos
|
|
||||||
combat_altitude: Distance
|
|
||||||
|
|
||||||
#: The altitude used for forming up a pacakge. Overrides the base rendezvous_altitude parameter for helos
|
|
||||||
rendezvous_altitude: Distance
|
|
||||||
|
|
||||||
#: Altitude of the nav points (cruise section) of air assault missions.
|
|
||||||
air_assault_nav_altitude: Distance
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict[str, Any]) -> Helicopter:
|
|
||||||
return Helicopter(
|
|
||||||
combat_altitude=feet(data["combat_altitude_ft_agl"]),
|
|
||||||
rendezvous_altitude=feet(data["rendezvous_altitude_ft_agl"]),
|
|
||||||
air_assault_nav_altitude=feet(data["air_assault_nav_altitude_ft_agl"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Aewc:
|
|
||||||
#: The duration that AEWC flights will remain on-station
|
|
||||||
duration: timedelta
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict[str, Any]) -> Aewc:
|
|
||||||
return Aewc(duration=timedelta(minutes=data["duration_minutes"]))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Cas:
|
|
||||||
#: The duration that CAP flights will remain on-station.
|
|
||||||
duration: timedelta
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict[str, Any]) -> Cas:
|
|
||||||
return Cas(duration=timedelta(minutes=data["duration_minutes"]))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Sweep:
|
|
||||||
#: Length of the sweep / patrol leg
|
|
||||||
distance: Distance
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict[str, Any]) -> Sweep:
|
|
||||||
return Sweep(
|
|
||||||
distance=nautical_miles(data["distance_nm"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Cap:
|
|
||||||
#: The duration that CAP flights will remain on-station.
|
|
||||||
duration: timedelta
|
|
||||||
|
|
||||||
#: The minimum length of the CAP race track.
|
|
||||||
min_track_length: Distance
|
|
||||||
|
|
||||||
#: The maximum length of the CAP race track.
|
|
||||||
max_track_length: Distance
|
|
||||||
|
|
||||||
#: The minimum distance between the defended position and the *end* of the
|
|
||||||
#: CAP race track.
|
|
||||||
min_distance_from_cp: Distance
|
|
||||||
|
|
||||||
#: The maximum distance between the defended position and the *end* of the
|
|
||||||
#: CAP race track.
|
|
||||||
max_distance_from_cp: Distance
|
|
||||||
|
|
||||||
#: The engagement range of CAP flights. Any enemy aircraft within this range
|
|
||||||
#: of the CAP's current position will be engaged by the CAP.
|
|
||||||
engagement_range: Distance
|
|
||||||
|
|
||||||
#: Defines the range of altitudes CAP racetracks are planned at.
|
|
||||||
min_patrol_altitude: Distance
|
|
||||||
max_patrol_altitude: Distance
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict[str, Any]) -> Cap:
|
|
||||||
return Cap(
|
|
||||||
duration=timedelta(minutes=data["duration_minutes"]),
|
|
||||||
min_track_length=nautical_miles(data["min_track_length_nm"]),
|
|
||||||
max_track_length=nautical_miles(data["max_track_length_nm"]),
|
|
||||||
min_distance_from_cp=nautical_miles(data["min_distance_from_cp_nm"]),
|
|
||||||
max_distance_from_cp=nautical_miles(data["max_distance_from_cp_nm"]),
|
|
||||||
engagement_range=nautical_miles(data["engagement_range_nm"]),
|
|
||||||
min_patrol_altitude=feet(data["min_patrol_altitude_ft_msl"]),
|
|
||||||
max_patrol_altitude=feet(data["max_patrol_altitude_ft_msl"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Tactics:
|
|
||||||
|
|
||||||
#: Air-to-air missile attack range options
|
|
||||||
air_to_air_missile_attack_range: Optional[OptAAMissileAttackRange.Values]
|
|
||||||
|
|
||||||
#: Air defence units evade ARMs
|
|
||||||
air_defence_evades_anti_radiation_missiles: bool
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(data: dict[str, Any]) -> Tactics:
|
|
||||||
return Tactics(
|
|
||||||
air_to_air_missile_attack_range=None,
|
|
||||||
air_defence_evades_anti_radiation_missiles=data.get(
|
|
||||||
"air_defence_evades_anti_radiation_missiles", False
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Doctrine:
|
class Doctrine:
|
||||||
#: Name of the doctrine, used to assign a doctrine in a faction.
|
cas: bool
|
||||||
name: str
|
cap: bool
|
||||||
|
sead: bool
|
||||||
|
strike: bool
|
||||||
|
antiship: bool
|
||||||
|
|
||||||
|
rendezvous_altitude: Distance
|
||||||
|
|
||||||
#: The minimum distance between the departure airfield and the hold point.
|
#: The minimum distance between the departure airfield and the hold point.
|
||||||
hold_distance: Distance
|
hold_distance: Distance
|
||||||
@@ -167,95 +44,146 @@ class Doctrine:
|
|||||||
#: target.
|
#: target.
|
||||||
min_ingress_distance: Distance
|
min_ingress_distance: Distance
|
||||||
|
|
||||||
#: The altitude used for combat section of a flight.
|
ingress_altitude: Distance
|
||||||
combat_altitude: Distance
|
|
||||||
|
|
||||||
#: The altitude used for forming up a pacakge.
|
min_patrol_altitude: Distance
|
||||||
rendezvous_altitude: Distance
|
max_patrol_altitude: Distance
|
||||||
|
pattern_altitude: Distance
|
||||||
|
|
||||||
|
#: The duration that CAP flights will remain on-station.
|
||||||
|
cap_duration: timedelta
|
||||||
|
|
||||||
|
#: The minimum length of the CAP race track.
|
||||||
|
cap_min_track_length: Distance
|
||||||
|
|
||||||
|
#: The maximum length of the CAP race track.
|
||||||
|
cap_max_track_length: Distance
|
||||||
|
|
||||||
|
#: The minimum distance between the defended position and the *end* of the
|
||||||
|
#: CAP race track.
|
||||||
|
cap_min_distance_from_cp: Distance
|
||||||
|
|
||||||
|
#: The maximum distance between the defended position and the *end* of the
|
||||||
|
#: CAP race track.
|
||||||
|
cap_max_distance_from_cp: Distance
|
||||||
|
|
||||||
|
#: The engagement range of CAP flights. Any enemy aircraft within this range
|
||||||
|
#: of the CAP's current position will be engaged by the CAP.
|
||||||
|
cap_engagement_range: Distance
|
||||||
|
|
||||||
|
cas_duration: timedelta
|
||||||
|
|
||||||
|
sweep_distance: Distance
|
||||||
|
|
||||||
#: Defines prioritization of ground unit purchases.
|
|
||||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||||
|
|
||||||
#: Helicopter specific doctrines.
|
|
||||||
helicopter: Helicopter
|
|
||||||
|
|
||||||
#: Doctrine for AEWC missions.
|
MODERN_DOCTRINE = Doctrine(
|
||||||
aewc: Aewc
|
cap=True,
|
||||||
|
cas=True,
|
||||||
#: Doctrine for CAS missions.
|
sead=True,
|
||||||
cas: Cas
|
strike=True,
|
||||||
|
antiship=True,
|
||||||
#: Doctrine for CAP missions.
|
rendezvous_altitude=feet(25000),
|
||||||
cap: Cap
|
hold_distance=nautical_miles(25),
|
||||||
|
push_distance=nautical_miles(20),
|
||||||
#: Doctrine for Fighter Sweep missions.
|
join_distance=nautical_miles(20),
|
||||||
sweep: Sweep
|
max_ingress_distance=nautical_miles(45),
|
||||||
|
min_ingress_distance=nautical_miles(10),
|
||||||
#: Tactics options
|
ingress_altitude=feet(20000),
|
||||||
tactics: Tactics
|
min_patrol_altitude=feet(15000),
|
||||||
|
max_patrol_altitude=feet(33000),
|
||||||
_by_name: ClassVar[dict[str, Doctrine]] = {}
|
pattern_altitude=feet(5000),
|
||||||
_loaded: ClassVar[bool] = False
|
cap_duration=timedelta(minutes=30),
|
||||||
|
cap_min_track_length=nautical_miles(15),
|
||||||
def resolve_combat_altitude(self, is_helo: bool = False) -> Distance:
|
cap_max_track_length=nautical_miles(40),
|
||||||
if is_helo:
|
cap_min_distance_from_cp=nautical_miles(10),
|
||||||
return self.helicopter.combat_altitude
|
cap_max_distance_from_cp=nautical_miles(40),
|
||||||
return self.combat_altitude
|
cap_engagement_range=nautical_miles(50),
|
||||||
|
cas_duration=timedelta(minutes=30),
|
||||||
def resolve_rendezvous_altitude(self, is_helo: bool = False) -> Distance:
|
sweep_distance=nautical_miles(60),
|
||||||
if is_helo:
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
return self.helicopter.rendezvous_altitude
|
{
|
||||||
return self.rendezvous_altitude
|
UnitClass.TANK: 3,
|
||||||
|
UnitClass.ATGM: 2,
|
||||||
@classmethod
|
UnitClass.APC: 2,
|
||||||
def register(cls, doctrine: Doctrine) -> None:
|
UnitClass.IFV: 3,
|
||||||
if doctrine.name in cls._by_name:
|
UnitClass.ARTILLERY: 1,
|
||||||
duplicate = cls._by_name[doctrine.name]
|
UnitClass.SHORAD: 2,
|
||||||
raise ValueError(f"Doctrine {doctrine.name} is already loaded")
|
UnitClass.RECON: 1,
|
||||||
cls._by_name[doctrine.name] = doctrine
|
}
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def named(cls, name: str) -> Doctrine:
|
|
||||||
if not cls._loaded:
|
|
||||||
cls.load_all()
|
|
||||||
return cls._by_name[name]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def all_doctrines(cls) -> list[Doctrine]:
|
|
||||||
if not cls._loaded:
|
|
||||||
cls.load_all()
|
|
||||||
return list(cls._by_name.values())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load_all(cls) -> None:
|
|
||||||
if cls._loaded:
|
|
||||||
return
|
|
||||||
for doctrine_file_path in Path("resources/doctrines").glob("**/*.yaml"):
|
|
||||||
with doctrine_file_path.open(encoding="utf8") as doctrine_file:
|
|
||||||
data = yaml.safe_load(doctrine_file)
|
|
||||||
cls.register(
|
|
||||||
Doctrine(
|
|
||||||
name=data["name"],
|
|
||||||
rendezvous_altitude=feet(data["rendezvous_altitude_ft_msl"]),
|
|
||||||
hold_distance=nautical_miles(data["hold_distance_nm"]),
|
|
||||||
push_distance=nautical_miles(data["push_distance_nm"]),
|
|
||||||
join_distance=nautical_miles(data["join_distance_nm"]),
|
|
||||||
max_ingress_distance=nautical_miles(
|
|
||||||
data["max_ingress_distance_nm"]
|
|
||||||
),
|
),
|
||||||
min_ingress_distance=nautical_miles(
|
|
||||||
data["min_ingress_distance_nm"]
|
|
||||||
),
|
|
||||||
combat_altitude=feet(data["combat_altitude_ft_msl"]),
|
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios.from_dict(
|
|
||||||
data["ground_unit_procurement_ratios"]
|
|
||||||
),
|
|
||||||
helicopter=Helicopter.from_dict(data["helicopter"]),
|
|
||||||
aewc=Aewc.from_dict(data["aewc"]),
|
|
||||||
cas=Cas.from_dict(data["cas"]),
|
|
||||||
cap=Cap.from_dict(data["cap"]),
|
|
||||||
sweep=Sweep.from_dict(data["sweep"]),
|
|
||||||
tactics=Tactics.from_dict(data.get("tactics", {})),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
COLDWAR_DOCTRINE = Doctrine(
|
||||||
|
cap=True,
|
||||||
|
cas=True,
|
||||||
|
sead=True,
|
||||||
|
strike=True,
|
||||||
|
antiship=True,
|
||||||
|
rendezvous_altitude=feet(22000),
|
||||||
|
hold_distance=nautical_miles(15),
|
||||||
|
push_distance=nautical_miles(10),
|
||||||
|
join_distance=nautical_miles(10),
|
||||||
|
max_ingress_distance=nautical_miles(30),
|
||||||
|
min_ingress_distance=nautical_miles(10),
|
||||||
|
ingress_altitude=feet(18000),
|
||||||
|
min_patrol_altitude=feet(10000),
|
||||||
|
max_patrol_altitude=feet(24000),
|
||||||
|
pattern_altitude=feet(5000),
|
||||||
|
cap_duration=timedelta(minutes=30),
|
||||||
|
cap_min_track_length=nautical_miles(12),
|
||||||
|
cap_max_track_length=nautical_miles(24),
|
||||||
|
cap_min_distance_from_cp=nautical_miles(8),
|
||||||
|
cap_max_distance_from_cp=nautical_miles(25),
|
||||||
|
cap_engagement_range=nautical_miles(35),
|
||||||
|
cas_duration=timedelta(minutes=30),
|
||||||
|
sweep_distance=nautical_miles(40),
|
||||||
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
|
{
|
||||||
|
UnitClass.TANK: 4,
|
||||||
|
UnitClass.ATGM: 2,
|
||||||
|
UnitClass.APC: 3,
|
||||||
|
UnitClass.IFV: 2,
|
||||||
|
UnitClass.ARTILLERY: 1,
|
||||||
|
UnitClass.SHORAD: 2,
|
||||||
|
UnitClass.RECON: 1,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
WWII_DOCTRINE = Doctrine(
|
||||||
|
cap=True,
|
||||||
|
cas=True,
|
||||||
|
sead=False,
|
||||||
|
strike=True,
|
||||||
|
antiship=True,
|
||||||
|
hold_distance=nautical_miles(10),
|
||||||
|
push_distance=nautical_miles(5),
|
||||||
|
join_distance=nautical_miles(5),
|
||||||
|
rendezvous_altitude=feet(10000),
|
||||||
|
max_ingress_distance=nautical_miles(7),
|
||||||
|
min_ingress_distance=nautical_miles(5),
|
||||||
|
ingress_altitude=feet(8000),
|
||||||
|
min_patrol_altitude=feet(4000),
|
||||||
|
max_patrol_altitude=feet(15000),
|
||||||
|
pattern_altitude=feet(5000),
|
||||||
|
cap_duration=timedelta(minutes=30),
|
||||||
|
cap_min_track_length=nautical_miles(8),
|
||||||
|
cap_max_track_length=nautical_miles(18),
|
||||||
|
cap_min_distance_from_cp=nautical_miles(0),
|
||||||
|
cap_max_distance_from_cp=nautical_miles(5),
|
||||||
|
cap_engagement_range=nautical_miles(20),
|
||||||
|
cas_duration=timedelta(minutes=30),
|
||||||
|
sweep_distance=nautical_miles(10),
|
||||||
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
|
{
|
||||||
|
UnitClass.TANK: 3,
|
||||||
|
UnitClass.ATGM: 3,
|
||||||
|
UnitClass.APC: 3,
|
||||||
|
UnitClass.ARTILLERY: 1,
|
||||||
|
UnitClass.SHORAD: 3,
|
||||||
|
UnitClass.RECON: 1,
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
cls._loaded = True
|
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ from pathlib import Path
|
|||||||
from typing import Iterator, Optional, Any, ClassVar
|
from typing import Iterator, Optional, Any, ClassVar
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from dcs.flyingunit import FlyingUnit
|
from dcs.unitgroup import FlyingGroup
|
||||||
from dcs.weapons_data import weapon_ids
|
from dcs.weapons_data import weapon_ids
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.factions.faction import Faction
|
|
||||||
|
|
||||||
|
|
||||||
PydcsWeapon = Any
|
PydcsWeapon = Any
|
||||||
PydcsWeaponAssignment = tuple[int, PydcsWeapon]
|
PydcsWeaponAssignment = tuple[int, PydcsWeapon]
|
||||||
@@ -79,12 +77,8 @@ class Weapon:
|
|||||||
WeaponGroup.load_all()
|
WeaponGroup.load_all()
|
||||||
cls._loaded = True
|
cls._loaded = True
|
||||||
|
|
||||||
def available_on(self, date: datetime.date, faction: Faction) -> bool:
|
def available_on(self, date: datetime.date) -> bool:
|
||||||
introduction_year = self.weapon_group.introduction_year
|
introduction_year = self.weapon_group.introduction_year
|
||||||
if self.weapon_group.name in faction.weapons_introduction_year_overrides:
|
|
||||||
introduction_year = faction.weapons_introduction_year_overrides[
|
|
||||||
self.weapon_group.name
|
|
||||||
]
|
|
||||||
if introduction_year is None:
|
if introduction_year is None:
|
||||||
return True
|
return True
|
||||||
return date >= datetime.date(introduction_year, 1, 1)
|
return date >= datetime.date(introduction_year, 1, 1)
|
||||||
@@ -241,17 +235,17 @@ class Pylon:
|
|||||||
# configuration.
|
# configuration.
|
||||||
return weapon in self.allowed or weapon.clsid == "<CLEAN>"
|
return weapon in self.allowed or weapon.clsid == "<CLEAN>"
|
||||||
|
|
||||||
def equip(self, unit: FlyingUnit, weapon: Weapon) -> None:
|
def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None:
|
||||||
if not self.can_equip(weapon):
|
if not self.can_equip(weapon):
|
||||||
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
|
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
|
||||||
unit.load_pylon(self.make_pydcs_assignment(weapon), self.number)
|
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
|
||||||
|
|
||||||
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
|
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
|
||||||
return self.number, weapon.pydcs_data
|
return self.number, weapon.pydcs_data
|
||||||
|
|
||||||
def available_on(self, date: datetime.date, faction: Faction) -> Iterator[Weapon]:
|
def available_on(self, date: datetime.date) -> Iterator[Weapon]:
|
||||||
for weapon in self.allowed:
|
for weapon in self.allowed:
|
||||||
if weapon.available_on(date, faction):
|
if weapon.available_on(date):
|
||||||
yield weapon
|
yield weapon
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user