mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
commit
c69e5e05a3
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before filing, please search the issue tracker to see if the issue has already been reported.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Additional information**
|
||||
|
||||
We will usually need more information for debugging. Include as much of the following as you are able:
|
||||
|
||||
- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`).
|
||||
- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`).
|
||||
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are locaed in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
|
||||
|
||||
**Version information (please complete the following information):**
|
||||
- DCS Liberation [e.g. 2.3.1]:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before filing, please search the issue tracker to see if this feature has already been requested.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ resources/payloads/*.lua
|
||||
venv
|
||||
logs.txt
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
a.py
|
||||
resources/tools/a.miz
|
||||
|
||||
165
LICENSE
Normal file
165
LICENSE
Normal file
@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
10
README.md
10
README.md
@ -21,6 +21,16 @@ It is an external program that generates full and complex DCS missions and manag
|
||||
|
||||
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
|
||||
|
||||
To download preview builds of the next version of DCS Liberation, see https://github.com/Khopa/dcs_liberation/wiki/Preview-builds.
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/Khopa/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Our plans for future releases can be found on our [Projects page](https://github.com/Khopa/dcs_liberation/projects). Each planned release has a Project, and the page for that project has columns for to do, in progress, and done. Items in the Done column are in the [preview build](https://github.com/Khopa/dcs_liberation/wiki/Preview-builds) for that release. Items in the To do column are planned to be added to that release.
|
||||
|
||||
## Resources
|
||||
|
||||
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/)
|
||||
|
||||
81
changelog.md
81
changelog.md
@ -1,7 +1,86 @@
|
||||
# 2.4.0
|
||||
|
||||
Saves from 2.3 are not compatible with 2.4.
|
||||
|
||||
## Highlights
|
||||
|
||||
* Improved flight plan generation to avoid loitering in or traveling through threatened areas when practical.
|
||||
* Improved AI aircraft purchasing behavior.
|
||||
* Era-restricted weapons (work in progress).
|
||||
* Tons of UI polish.
|
||||
* Rebalanced economy to keep opfor competitive over the course of the game.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats.
|
||||
* **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical.
|
||||
* **[Flight Planner]** Flight plans along front lines now ensure that the race track start is closer to the departure airfield than the race track end.
|
||||
* **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy.
|
||||
* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety.
|
||||
* **[Campaign AI]** Opfor now sells off odd aircraft since they're unlikely to be used.
|
||||
* **[Campaign AI]** Multiple rounds of CAP will be planned (roughly 90 minutes of coverage). Default starting budget has increased to account for the increased need for aircraft.
|
||||
* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior.
|
||||
* **[Mission Generator]** Default start type can now be chosen in the settings. This replaces the non-functional "AI Parking Start" option. **Selecting any type other than cold will break OCA/Aircraft missions.**
|
||||
* **[Cheat Menu]** Added ability to toggle base capture and frontline advance/retreat cheats.
|
||||
* **[Skynet]** Updated to 2.0.1.
|
||||
* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany.
|
||||
* **[Hercules]** Updated the Hercules Cargo list file.
|
||||
* **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns.
|
||||
* **[Balance]** Units now retreat from captured bases when able. Units with no retreat path will be captured and sold.
|
||||
* **[Economy]** FOBs generate only $10M per turn (previously $20M like airbases).
|
||||
* **[Economy]** Carriers and off-map spawns generate no income (previously $20M like airbases).
|
||||
* **[Economy]** Sales of aircraft and ground vehicles can now be cancelled before the next turn begins.
|
||||
* **[UI]** Multi-SAM objectives now show threat and detection rings per group.
|
||||
* **[UI]** New icon for AA sites with no active threat.
|
||||
* **[UI]** Unit names are now prettier and more accurate, and can now be set per-country for added historical flavour.
|
||||
* **[UI]** Default loadout is now shown for flights with no custom loadout selected.
|
||||
* **[UI]** Aircraft for a new flight are now only selectable if they match the task type for that flight.
|
||||
* **[UI]** WIP - There is now a unit info button for each unit in the recruitment list, that should help newer players learn what each unit does.
|
||||
* **[UI]** Docs for time-on-target and creating new theaters/factions/loadouts are now linked in the UI at the appropriate places.
|
||||
* **[UI]** ASAP is now a checkbox rather than a button. Enabling this will disable the TOT selector but changes to the package structure will automatically re-ASAP the package.
|
||||
* **[UI]** Arrival airfield is now shown in the flight list if it differs from the departure airfield.
|
||||
* **[UI]** Start type can now be selected when creating a flight.
|
||||
* **[UI]** Arrival and divert airfields can be edited after the flight is created.
|
||||
* **[Factions]** Added option for date-based loadout restriction. Active radar homing missiles are handled, patches welcome for the other thousand weapons.
|
||||
* **[Factions]** Added Poland 2010 faction.
|
||||
* **[Factions]** Added Greece 2005 faction.
|
||||
* **[Factions]** Added Iran 1988 faction.
|
||||
* **[Units]** Support for E-2 Hawkeye, SH-60B Seahawk, S-3B Viking (thanks to awinterquest) and SpGH Dana - these are now being used by appropriate factions.
|
||||
* **[Culling]** Missile sites are no longer culled.
|
||||
* **[Campaigns]** Added campaign "Black Sea Lite" by Starfire
|
||||
* **[Campaigns]** Added campaign "Exercise Vegas Nerve" by Starfire
|
||||
* **[New game Wizard]** The theater page is now the first page of the campaign wizard, recommended factions will be selected automatically on the faction selection page
|
||||
* **[New game Wizard]** Added information text about the selected campaign performance.
|
||||
* **[Mod Support]** Added support for High Digit SAMs mod 1.4.0
|
||||
* **[Mod Support]** Added SAMs sites generator : KS19Generator, SA10BGenerator, SA12Generator, SA17Generator, SA20Generator, SA20BGenerator, SA23Generator
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Hercules]** Updated the default Hercules radio frequency.
|
||||
* **[Economy]** Pending unit orders at captured bases will be refunded.
|
||||
* **[UI]** Carrier group SAM threat rings now move with the carrier.
|
||||
* **[UI]** Base intel menu no longer compresses text, and is now scrollable.
|
||||
* **[UI]** Edit Flight window is now dynamically sized to adapt to the width of waypoint names, so they no longer get truncated.
|
||||
* **[UI]** Budget income display is now rounded to 2 decimal places.
|
||||
* **[UI]** Fixed incorrect income per turn displayed for strike target tooltip.
|
||||
* **[Factions]** USA with C-130 faction now links to the required mod.
|
||||
* **[Campaign]** Fixed issue where destroyed buildings would sometimes not count as destroyed and thus respawn.
|
||||
* **[Campaign]** Fixed issue where destroyed runways were not registered.
|
||||
* **[Units]** J-11A is no longer spawned with empty loadout.
|
||||
* **[Units]** F-14B is no longer spawned with empty loadout for fighter sweep tasks.
|
||||
* **[Units]** Pyotr Velikiy cruiser has been removed for now as it's nearly unkillable.
|
||||
* **[Units]** Submarines have been removed for now as they aren't wholly functional.
|
||||
* **[Units]** Fixed "FACTION ERROR : Unable to find OliverHazardPerryGroupGenerator in pydcs" error at startup.
|
||||
* **[Mission Generator]** Fixed a bug where units set to Aggressive stance sometimes did not move.
|
||||
* **[Mission Generator]** Flyover points for OCA/Aircraft missions are now generated correctly.
|
||||
* **[Flight Planner]** Fixed not being able to create custom waypoints for buildings.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against SAMs.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against FOB structures.
|
||||
|
||||
# 2.3.4
|
||||
|
||||
## Fixes:
|
||||
* **[Mission Generator]** Mission generator would crash when generating fire missions for destroyed SCUD sites - fixed
|
||||
[Mission Generator] Mission generator would crash when generating fire missions for destroyed SCUD sites - fixed
|
||||
|
||||
# 2.3.3
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from game.utils import nm_to_meter, feet_to_meter
|
||||
from game.utils import Distance, feet, nautical_miles
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -12,31 +12,43 @@ class Doctrine:
|
||||
strike: bool
|
||||
antiship: bool
|
||||
|
||||
strike_max_range: int
|
||||
sead_max_range: int
|
||||
rendezvous_altitude: Distance
|
||||
hold_distance: Distance
|
||||
push_distance: Distance
|
||||
join_distance: Distance
|
||||
split_distance: Distance
|
||||
ingress_egress_distance: Distance
|
||||
ingress_altitude: Distance
|
||||
egress_altitude: Distance
|
||||
|
||||
rendezvous_altitude: int
|
||||
hold_distance: int
|
||||
push_distance: int
|
||||
join_distance: int
|
||||
split_distance: int
|
||||
ingress_egress_distance: int
|
||||
ingress_altitude: int
|
||||
egress_altitude: int
|
||||
|
||||
min_patrol_altitude: int
|
||||
max_patrol_altitude: int
|
||||
pattern_altitude: int
|
||||
min_patrol_altitude: Distance
|
||||
max_patrol_altitude: Distance
|
||||
pattern_altitude: Distance
|
||||
|
||||
#: The duration that CAP flights will remain on-station.
|
||||
cap_duration: timedelta
|
||||
cap_min_track_length: int
|
||||
cap_max_track_length: int
|
||||
cap_min_distance_from_cp: int
|
||||
cap_max_distance_from_cp: int
|
||||
|
||||
#: 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: int
|
||||
sweep_distance: Distance
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
@ -45,26 +57,25 @@ MODERN_DOCTRINE = Doctrine(
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
rendezvous_altitude=feet_to_meter(25000),
|
||||
hold_distance=nm_to_meter(15),
|
||||
push_distance=nm_to_meter(20),
|
||||
join_distance=nm_to_meter(20),
|
||||
split_distance=nm_to_meter(20),
|
||||
ingress_egress_distance=nm_to_meter(45),
|
||||
ingress_altitude=feet_to_meter(20000),
|
||||
egress_altitude=feet_to_meter(20000),
|
||||
min_patrol_altitude=feet_to_meter(15000),
|
||||
max_patrol_altitude=feet_to_meter(33000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
rendezvous_altitude=feet(25000),
|
||||
hold_distance=nautical_miles(15),
|
||||
push_distance=nautical_miles(20),
|
||||
join_distance=nautical_miles(20),
|
||||
split_distance=nautical_miles(20),
|
||||
ingress_egress_distance=nautical_miles(45),
|
||||
ingress_altitude=feet(20000),
|
||||
egress_altitude=feet(20000),
|
||||
min_patrol_altitude=feet(15000),
|
||||
max_patrol_altitude=feet(33000),
|
||||
pattern_altitude=feet(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nm_to_meter(15),
|
||||
cap_max_track_length=nm_to_meter(40),
|
||||
cap_min_distance_from_cp=nm_to_meter(10),
|
||||
cap_max_distance_from_cp=nm_to_meter(40),
|
||||
cap_min_track_length=nautical_miles(15),
|
||||
cap_max_track_length=nautical_miles(40),
|
||||
cap_min_distance_from_cp=nautical_miles(10),
|
||||
cap_max_distance_from_cp=nautical_miles(40),
|
||||
cap_engagement_range=nautical_miles(50),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nm_to_meter(60),
|
||||
sweep_distance=nautical_miles(60),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
@ -73,26 +84,25 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
rendezvous_altitude=feet_to_meter(22000),
|
||||
hold_distance=nm_to_meter(10),
|
||||
push_distance=nm_to_meter(10),
|
||||
join_distance=nm_to_meter(10),
|
||||
split_distance=nm_to_meter(10),
|
||||
ingress_egress_distance=nm_to_meter(30),
|
||||
ingress_altitude=feet_to_meter(18000),
|
||||
egress_altitude=feet_to_meter(18000),
|
||||
min_patrol_altitude=feet_to_meter(10000),
|
||||
max_patrol_altitude=feet_to_meter(24000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
rendezvous_altitude=feet(22000),
|
||||
hold_distance=nautical_miles(10),
|
||||
push_distance=nautical_miles(10),
|
||||
join_distance=nautical_miles(10),
|
||||
split_distance=nautical_miles(10),
|
||||
ingress_egress_distance=nautical_miles(30),
|
||||
ingress_altitude=feet(18000),
|
||||
egress_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=nm_to_meter(12),
|
||||
cap_max_track_length=nm_to_meter(24),
|
||||
cap_min_distance_from_cp=nm_to_meter(8),
|
||||
cap_max_distance_from_cp=nm_to_meter(25),
|
||||
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=nm_to_meter(40),
|
||||
sweep_distance=nautical_miles(40),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
@ -101,24 +111,23 @@ WWII_DOCTRINE = Doctrine(
|
||||
sead=False,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
strike_max_range=1500000,
|
||||
sead_max_range=1500000,
|
||||
hold_distance=nm_to_meter(5),
|
||||
push_distance=nm_to_meter(5),
|
||||
join_distance=nm_to_meter(5),
|
||||
split_distance=nm_to_meter(5),
|
||||
rendezvous_altitude=feet_to_meter(10000),
|
||||
ingress_egress_distance=nm_to_meter(7),
|
||||
ingress_altitude=feet_to_meter(8000),
|
||||
egress_altitude=feet_to_meter(8000),
|
||||
min_patrol_altitude=feet_to_meter(4000),
|
||||
max_patrol_altitude=feet_to_meter(15000),
|
||||
pattern_altitude=feet_to_meter(5000),
|
||||
hold_distance=nautical_miles(5),
|
||||
push_distance=nautical_miles(5),
|
||||
join_distance=nautical_miles(5),
|
||||
split_distance=nautical_miles(5),
|
||||
rendezvous_altitude=feet(10000),
|
||||
ingress_egress_distance=nautical_miles(7),
|
||||
ingress_altitude=feet(8000),
|
||||
egress_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=nm_to_meter(8),
|
||||
cap_max_track_length=nm_to_meter(18),
|
||||
cap_min_distance_from_cp=nm_to_meter(0),
|
||||
cap_max_distance_from_cp=nm_to_meter(5),
|
||||
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=nm_to_meter(10),
|
||||
sweep_distance=nautical_miles(10),
|
||||
)
|
||||
|
||||
326
game/data/weapons.py
Normal file
326
game/data/weapons.py
Normal file
@ -0,0 +1,326 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.unittype import FlyingType
|
||||
from dcs.weapons_data import Weapons, weapon_ids
|
||||
|
||||
|
||||
PydcsWeapon = Dict[str, Union[int, str]]
|
||||
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Weapon:
|
||||
"""Wraps a pydcs weapon dict in a hashable type."""
|
||||
|
||||
cls_id: str
|
||||
name: str
|
||||
weight: int
|
||||
|
||||
def available_on(self, date: datetime.date) -> bool:
|
||||
introduction_year = WEAPON_INTRODUCTION_YEARS.get(self)
|
||||
if introduction_year is None:
|
||||
logging.warning(
|
||||
f"No introduction year for {self}, assuming always available")
|
||||
return True
|
||||
return date >= datetime.date(introduction_year, 1, 1)
|
||||
|
||||
@property
|
||||
def as_pydcs(self) -> PydcsWeapon:
|
||||
return {
|
||||
"clsid": self.cls_id,
|
||||
"name": self.name,
|
||||
"weight": self.weight,
|
||||
}
|
||||
|
||||
@property
|
||||
def fallbacks(self) -> Iterator[Weapon]:
|
||||
yield self
|
||||
fallback = WEAPON_FALLBACK_MAP[self]
|
||||
if fallback is not None:
|
||||
yield from fallback.fallbacks
|
||||
|
||||
@classmethod
|
||||
def from_pydcs(cls, weapon_data: PydcsWeapon) -> Weapon:
|
||||
return cls(
|
||||
cast(str, weapon_data["clsid"]),
|
||||
cast(str, weapon_data["name"]),
|
||||
cast(int, weapon_data["weight"])
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_clsid(cls, clsid: str) -> Optional[Weapon]:
|
||||
data = weapon_ids.get(clsid)
|
||||
if data is None:
|
||||
return None
|
||||
return cls.from_pydcs(data)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pylon:
|
||||
number: int
|
||||
allowed: Set[Weapon]
|
||||
|
||||
def can_equip(self, weapon: Weapon) -> bool:
|
||||
return weapon in self.allowed
|
||||
|
||||
def equip(self, group: FlyingGroup, weapon: Weapon) -> None:
|
||||
if not self.can_equip(weapon):
|
||||
raise ValueError(f"Pylon {self.number} cannot equip {weapon.name}")
|
||||
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
|
||||
|
||||
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
|
||||
return self.number, weapon.as_pydcs
|
||||
|
||||
def available_on(self, date: datetime.date) -> Iterator[Weapon]:
|
||||
for weapon in self.allowed:
|
||||
if weapon.available_on(date):
|
||||
yield weapon
|
||||
|
||||
@classmethod
|
||||
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
|
||||
# In pydcs these are all arbitrary inner classes of the aircraft type.
|
||||
# The only way to identify them is by their name.
|
||||
pylons = [v for v in aircraft.__dict__.values() if
|
||||
inspect.isclass(v) and v.__name__.startswith("Pylon")]
|
||||
|
||||
# And that Pylon class has members with irrelevant names that have
|
||||
# values of (pylon number, allowed weapon).
|
||||
allowed = set()
|
||||
for pylon in pylons:
|
||||
for key, value in pylon.__dict__.items():
|
||||
if key.startswith("__"):
|
||||
continue
|
||||
pylon_number, weapon = value
|
||||
if pylon_number != number:
|
||||
continue
|
||||
allowed.add(Weapon.from_pydcs(weapon))
|
||||
|
||||
return cls(number, allowed)
|
||||
|
||||
@classmethod
|
||||
def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
|
||||
for pylon in sorted(list(aircraft.pylons)):
|
||||
yield cls.for_aircraft(aircraft, pylon)
|
||||
|
||||
|
||||
_WEAPON_FALLBACKS = [
|
||||
# AIM-120C
|
||||
(Weapons.AIM_120C, Weapons.AIM_120B),
|
||||
(Weapons.LAU_115___AIM_120C, Weapons.LAU_115___AIM_120B),
|
||||
(Weapons.LAU_115_2_LAU_127_AIM_120C, Weapons.LAU_115_2_LAU_127_AIM_120B),
|
||||
|
||||
# AIM-120B
|
||||
(Weapons.AIM_120B, Weapons.AIM_7MH),
|
||||
(Weapons.LAU_115___AIM_120B, Weapons.LAU_115C_AIM_7MH),
|
||||
(Weapons.LAU_115_2_LAU_127_AIM_120B, Weapons.LAU_115C_AIM_7MH),
|
||||
|
||||
# AIM-7MH
|
||||
(Weapons.AIM_7MH, Weapons.AIM_7M),
|
||||
(Weapons.AIM_7MH_, Weapons.AIM_7M_),
|
||||
(Weapons.AIM_7MH__, Weapons.AIM_7M__),
|
||||
(Weapons.LAU_115C_AIM_7MH, Weapons.LAU_115___AIM_7M),
|
||||
|
||||
# AIM-7M
|
||||
(Weapons.AIM_7M, Weapons.AIM_7F),
|
||||
(Weapons.AIM_7M_, None),
|
||||
(Weapons.AIM_7M__, None),
|
||||
(Weapons.LAU_115___AIM_7M, Weapons.LAU_115C_AIM_7F),
|
||||
|
||||
# AIM-7F
|
||||
(Weapons.AIM_7F, Weapons.AIM_7E),
|
||||
(Weapons.AIM_7F_, Weapons.AIM_7E),
|
||||
(Weapons.AIM_7F__, Weapons.AIM_7E),
|
||||
(Weapons.LAU_115C_AIM_7F, Weapons.LAU_115C_AIM_7E),
|
||||
|
||||
# AIM-7E
|
||||
(Weapons.AIM_7E, Weapons.AIM_9X_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_115C_AIM_7E, Weapons.LAU_115_LAU_127_AIM_9X),
|
||||
|
||||
# AIM-9X
|
||||
(Weapons.AIM_9X_Sidewinder_IR_AAM, Weapons.AIM_9P5_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_115_LAU_127_AIM_9X, Weapons.LAU_115_LAU_127_AIM_9M),
|
||||
(Weapons.LAU_115_2_LAU_127_AIM_9X, Weapons.LAU_115_2_LAU_127_AIM_9M),
|
||||
(Weapons.LAU_127_AIM_9X, Weapons.LAU_127_AIM_9M),
|
||||
|
||||
# AIM-9P5
|
||||
(Weapons.AIM_9P5_Sidewinder_IR_AAM, Weapons.AIM_9P_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM),
|
||||
|
||||
# AIM-9P
|
||||
(Weapons.AIM_9P_Sidewinder_IR_AAM, Weapons.AIM_9M_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM),
|
||||
|
||||
# AIM-9M
|
||||
(Weapons.AIM_9M_Sidewinder_IR_AAM, Weapons.AIM_9L_Sidewinder_IR_AAM),
|
||||
(Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM, Weapons.LAU_7_AIM_9L),
|
||||
|
||||
# AIM-9L
|
||||
(Weapons.AIM_9L_Sidewinder_IR_AAM, None),
|
||||
(Weapons.LAU_7_AIM_9L, None),
|
||||
|
||||
# AIM-54C Mk47
|
||||
(Weapons.AIM_54C_Mk47, Weapons.AIM_54A_Mk60),
|
||||
(Weapons.AIM_54C_Mk47_, Weapons.AIM_54A_Mk60_),
|
||||
(Weapons.AIM_54C_Mk47__, Weapons.AIM_54A_Mk60__),
|
||||
|
||||
# AIM-54A Mk60
|
||||
(Weapons.AIM_54A_Mk60, Weapons.AIM_54A_Mk47),
|
||||
(Weapons.AIM_54A_Mk60_, Weapons.AIM_54A_Mk47_),
|
||||
(Weapons.AIM_54A_Mk60__, Weapons.AIM_54A_Mk47__),
|
||||
|
||||
# R-27 (AA-10 Alamo)
|
||||
(Weapons.R_27ER, Weapons.R_27R),
|
||||
(Weapons.R_27ET, Weapons.R_27T),
|
||||
|
||||
# R-77 (AA-12)
|
||||
(Weapons.R_77, Weapons.R_27ER),
|
||||
(Weapons.R_77_, Weapons.R_27ER),
|
||||
|
||||
# R-73 (AA-11)
|
||||
(Weapons.R_73, Weapons.R_60M),
|
||||
(Weapons.R_73_, Weapons.R_60M_),
|
||||
|
||||
# GBU-38 (JDAM)
|
||||
(Weapons.GBU_38, Weapons.GBU_12),
|
||||
(Weapons.GBU_38_16, Weapons.MK_82_28), # B1-B only
|
||||
(Weapons._2_GBU_38_, Weapons._2_GBU_12),
|
||||
(Weapons._2_GBU_38, Weapons._2_GBU_12),
|
||||
(Weapons._3_GBU_38, Weapons._3_GBU_12),
|
||||
(Weapons.BRU_55___2_x_GBU_38, Weapons.BRU_33___2_x_GBU_12),
|
||||
(Weapons.BRU_57___2_x_GBU_38, Weapons.BRU_33___2_x_GBU_12),
|
||||
|
||||
# AGM-154A (JSOW)
|
||||
(Weapons.AGM_154A, Weapons.GBU_12),
|
||||
(Weapons.BRU_55___2_x_AGM_154A, Weapons.BRU_33___2_x_GBU_12),
|
||||
(Weapons.BRU_57___2_x_AGM_154A, Weapons.BRU_33___2_x_GBU_12),
|
||||
|
||||
# AGM-154C (JSOW)
|
||||
(Weapons.AGM_154C, Weapons.GBU_12),
|
||||
(Weapons.AGM_154C_4, Weapons.MK_82_28), # B1-B only
|
||||
(Weapons.BRU_55___2_x_AGM_154C, Weapons.BRU_33___2_x_GBU_12),
|
||||
|
||||
# AGM-84E (SLAM)
|
||||
(Weapons.AGM_84E, Weapons.LAU_117_AGM_65F),
|
||||
|
||||
# CBU-97
|
||||
(Weapons.CBU_97, Weapons.GBU_12),
|
||||
(Weapons.TER_9A___2_x_CBU_97, Weapons.TER_9A___2_x_GBU_12),
|
||||
(Weapons.TER_9A___2_x_CBU_97_, Weapons.TER_9A___2_x_GBU_12),
|
||||
(Weapons.TER_9A___3_x_CBU_97, Weapons.TER_9A___2_x_GBU_12),
|
||||
|
||||
]
|
||||
|
||||
WEAPON_FALLBACK_MAP: Dict[Weapon, Optional[Weapon]] = defaultdict(
|
||||
lambda: cast(Optional[Weapon], None),
|
||||
((Weapon.from_pydcs(a), b if b is None else Weapon.from_pydcs(b))
|
||||
for a, b in _WEAPON_FALLBACKS))
|
||||
|
||||
|
||||
WEAPON_INTRODUCTION_YEARS = {
|
||||
# AIM-120C
|
||||
Weapon.from_pydcs(Weapons.AIM_120C): 1996,
|
||||
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120C): 1996,
|
||||
Weapon.from_pydcs(Weapons.LAU_115___AIM_120C): 1996,
|
||||
|
||||
# AIM-120B
|
||||
Weapon.from_pydcs(Weapons.AIM_120B): 1994,
|
||||
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_120B): 1994,
|
||||
Weapon.from_pydcs(Weapons.LAU_115___AIM_120B): 1994,
|
||||
|
||||
# AIM-7MH
|
||||
Weapon.from_pydcs(Weapons.AIM_7MH): 1987,
|
||||
Weapon.from_pydcs(Weapons.AIM_7MH_): 1987,
|
||||
Weapon.from_pydcs(Weapons.AIM_7MH__): 1987,
|
||||
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7MH): 1987,
|
||||
|
||||
# AIM-7M
|
||||
Weapon.from_pydcs(Weapons.AIM_7M): 1982,
|
||||
Weapon.from_pydcs(Weapons.AIM_7M_): 1982,
|
||||
Weapon.from_pydcs(Weapons.AIM_7M__): 1982,
|
||||
Weapon.from_pydcs(Weapons.LAU_115___AIM_7M): 1982,
|
||||
|
||||
# AIM-7F
|
||||
Weapon.from_pydcs(Weapons.AIM_7F): 1976,
|
||||
Weapon.from_pydcs(Weapons.AIM_7F_): 1976,
|
||||
Weapon.from_pydcs(Weapons.AIM_7F__): 1976,
|
||||
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7F): 1976,
|
||||
|
||||
# AIM-7E
|
||||
Weapon.from_pydcs(Weapons.AIM_7E): 1963,
|
||||
Weapon.from_pydcs(Weapons.LAU_115C_AIM_7E): 1963,
|
||||
|
||||
# AIM-9X
|
||||
Weapon.from_pydcs(Weapons.AIM_9X_Sidewinder_IR_AAM): 2003,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9X_Sidewinder_IR_AAM): 2003,
|
||||
Weapon.from_pydcs(Weapons.LAU_115_LAU_127_AIM_9X): 2003,
|
||||
Weapon.from_pydcs(Weapons.LAU_115_2_LAU_127_AIM_9X): 2003,
|
||||
Weapon.from_pydcs(Weapons.LAU_127_AIM_9X): 2003,
|
||||
|
||||
# AIM-9P5
|
||||
Weapon.from_pydcs(Weapons.AIM_9P5_Sidewinder_IR_AAM): 1963,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9P5_Sidewinder_IR_AAM): 1963,
|
||||
|
||||
# AIM-9P
|
||||
Weapon.from_pydcs(Weapons.AIM_9P_Sidewinder_IR_AAM): 1978,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9P_Sidewinder_IR_AAM): 1978,
|
||||
|
||||
# AIM-9M
|
||||
Weapon.from_pydcs(Weapons.AIM_9M_Sidewinder_IR_AAM): 1983,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9M_Sidewinder_IR_AAM): 1983,
|
||||
|
||||
# AIM-9L
|
||||
Weapon.from_pydcs(Weapons.AIM_9L_Sidewinder_IR_AAM): 1977,
|
||||
Weapon.from_pydcs(Weapons.LAU_7_AIM_9L): 1977,
|
||||
|
||||
# AIM-54C-Mk47
|
||||
Weapon.from_pydcs(Weapons.AIM_54C_Mk47): 1986,
|
||||
Weapon.from_pydcs(Weapons.AIM_54C_Mk47_): 1986,
|
||||
Weapon.from_pydcs(Weapons.AIM_54C_Mk47__): 1986,
|
||||
Weapon.from_pydcs(Weapons.AIM_54C): 1986, # this weapon id is unused (legacy F-14A)
|
||||
|
||||
# R-77 (AA-12)
|
||||
Weapon.from_pydcs(Weapons.R_77): 2002,
|
||||
Weapon.from_pydcs(Weapons.R_77_): 2002,
|
||||
|
||||
# R-73 (AA-11)
|
||||
Weapon.from_pydcs(Weapons.R_73): 1989,
|
||||
Weapon.from_pydcs(Weapons.R_73_): 1989,
|
||||
|
||||
# GBU-38 (JDAM)
|
||||
Weapon.from_pydcs(Weapons.GBU_38): 1998,
|
||||
Weapon.from_pydcs(Weapons.GBU_38_16): 1998, # B1-B only
|
||||
Weapon.from_pydcs(Weapons._2_GBU_38_): 1998,
|
||||
Weapon.from_pydcs(Weapons._2_GBU_38): 1998,
|
||||
Weapon.from_pydcs(Weapons._3_GBU_38): 1998,
|
||||
Weapon.from_pydcs(Weapons.BRU_55___2_x_GBU_38): 1998,
|
||||
Weapon.from_pydcs(Weapons.BRU_57___2_x_GBU_38): 1998,
|
||||
|
||||
# AGM-154A (JSOW)
|
||||
Weapon.from_pydcs(Weapons.AGM_154A): 1999,
|
||||
Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154A): 1999,
|
||||
Weapon.from_pydcs(Weapons.BRU_57___2_x_AGM_154A): 1999,
|
||||
|
||||
# AGM-154C (JSOW)
|
||||
Weapon.from_pydcs(Weapons.AGM_154C): 2005,
|
||||
Weapon.from_pydcs(Weapons.AGM_154C_4): 2005, # B1-B only
|
||||
Weapon.from_pydcs(Weapons.BRU_55___2_x_AGM_154C): 2005,
|
||||
|
||||
# AGM-84E
|
||||
Weapon.from_pydcs(Weapons.AGM_84E): 1990,
|
||||
|
||||
# CBU-97
|
||||
Weapon.from_pydcs(Weapons.CBU_97): 1995,
|
||||
Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97): 1995,
|
||||
Weapon.from_pydcs(Weapons.TER_9A___2_x_CBU_97_): 1995,
|
||||
Weapon.from_pydcs(Weapons.TER_9A___3_x_CBU_97): 1995
|
||||
|
||||
}
|
||||
204
game/db.py
204
game/db.py
@ -1,6 +1,8 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Tuple, Type, Union
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from dcs.countries import country_dict
|
||||
from dcs.helicopters import (
|
||||
@ -17,6 +19,7 @@ from dcs.helicopters import (
|
||||
SA342M,
|
||||
SA342Minigun,
|
||||
SA342Mistral,
|
||||
SH_60B,
|
||||
UH_1H,
|
||||
UH_60A,
|
||||
helicopter_map,
|
||||
@ -40,12 +43,14 @@ from dcs.planes import (
|
||||
C_101CC,
|
||||
C_130,
|
||||
E_3A,
|
||||
E_2C,
|
||||
FA_18C_hornet,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_117A,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_111F,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_16A,
|
||||
@ -61,6 +66,7 @@ from dcs.planes import (
|
||||
Ju_88A4,
|
||||
KC130,
|
||||
KC_135,
|
||||
KC135MPRS,
|
||||
KJ_2000,
|
||||
L_39C,
|
||||
L_39ZA,
|
||||
@ -84,6 +90,7 @@ from dcs.planes import (
|
||||
P_51D_30_NA,
|
||||
PlaneType,
|
||||
RQ_1A_Predator,
|
||||
S_3B,
|
||||
S_3B_Tanker,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
@ -130,6 +137,7 @@ from dcs.task import (
|
||||
CargoTransportation,
|
||||
Embarking,
|
||||
Escort,
|
||||
FighterSweep,
|
||||
GroundAttack,
|
||||
Intercept,
|
||||
MainTask,
|
||||
@ -157,6 +165,7 @@ from dcs.vehicles import (
|
||||
)
|
||||
|
||||
import pydcs_extensions.frenchpack.frenchpack as frenchpack
|
||||
import pydcs_extensions.highdigitsams.highdigitsams as highdigitsams
|
||||
# PATCH pydcs data with MODS
|
||||
from game.factions.faction_loader import FactionLoader
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
@ -166,6 +175,8 @@ from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
UNITINFOTEXT_PATH = Path("./resources/units/unit_info_text.json")
|
||||
|
||||
plane_map["A-4E-C"] = A_4E_C
|
||||
plane_map["MB-339PAN"] = MB_339PAN
|
||||
plane_map["Rafale_M"] = Rafale_M
|
||||
@ -209,6 +220,49 @@ vehicle_map["Toyota_vert"] = frenchpack.DIM__TOYOTA_GREEN
|
||||
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
|
||||
vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
|
||||
|
||||
vehicle_map[highdigitsams.AAA_SON_9_Fire_Can.id] = highdigitsams.AAA_SON_9_Fire_Can
|
||||
vehicle_map[highdigitsams.AAA_100mm_KS_19.id] = highdigitsams.AAA_100mm_KS_19
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_54K6_CP.id] = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN.id] = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE
|
||||
vehicle_map[highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE.id] = highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_30N6_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR.id] = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6.id] = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck.id] = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E.id] = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E.id] = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE.id] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE
|
||||
vehicle_map[highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE.id] = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE
|
||||
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2
|
||||
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck.id] = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck
|
||||
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2
|
||||
vehicle_map[highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2.id] = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S457_CP.id] = highdigitsams.SAM_SA_12_S_300V_9S457_CP
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9A82_LN.id] = highdigitsams.SAM_SA_12_S_300V_9A82_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9A83_LN.id] = highdigitsams.SAM_SA_12_S_300V_9A83_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S15_SR.id] = highdigitsams.SAM_SA_12_S_300V_9S15_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S19_SR.id] = highdigitsams.SAM_SA_12_S_300V_9S19_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_12_S_300V_9S32_TR.id] = highdigitsams.SAM_SA_12_S_300V_9S32_TR
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP.id] = highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR.id] = highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR.id] = highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR.id] = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN.id] = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN.id] = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN
|
||||
vehicle_map[highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2.id] = highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2
|
||||
vehicle_map[highdigitsams.SAM_SA_2__V759__LN_SM_90.id] = highdigitsams.SAM_SA_2__V759__LN_SM_90
|
||||
vehicle_map[highdigitsams.SAM_HQ_2_LN_SM_90.id] = highdigitsams.SAM_HQ_2_LN_SM_90
|
||||
vehicle_map[highdigitsams.SAM_SA_3__V_601P__LN_5P73.id] = highdigitsams.SAM_SA_3__V_601P__LN_5P73
|
||||
vehicle_map[highdigitsams.SAM_SA_24_Igla_S_manpad.id] = highdigitsams.SAM_SA_24_Igla_S_manpad
|
||||
vehicle_map[highdigitsams.SAM_SA_14_Strela_3_manpad.id] = highdigitsams.SAM_SA_14_Strela_3_manpad
|
||||
vehicle_map[highdigitsams.Polyana_D4M1_C2_node.id] = highdigitsams.Polyana_D4M1_C2_node
|
||||
vehicle_map[highdigitsams._34Ya6E_Gazetchik_E_decoy.id] = highdigitsams._34Ya6E_Gazetchik_E_decoy
|
||||
|
||||
"""
|
||||
---------- BEGINNING OF CONFIGURATION SECTION
|
||||
"""
|
||||
@ -302,6 +356,7 @@ PRICES = {
|
||||
A_10A: 16,
|
||||
A_10C: 22,
|
||||
A_10C_2: 24,
|
||||
S_3B: 10,
|
||||
|
||||
# heli
|
||||
Ka_50: 13,
|
||||
@ -317,6 +372,7 @@ PRICES = {
|
||||
AH_64A: 24,
|
||||
AH_64D: 30,
|
||||
OH_58D: 6,
|
||||
SH_60B: 6,
|
||||
|
||||
# Bombers
|
||||
B_52H: 35,
|
||||
@ -325,6 +381,7 @@ PRICES = {
|
||||
Tu_160: 50,
|
||||
Tu_22M3: 40,
|
||||
Tu_95MS: 35,
|
||||
F_111F: 21,
|
||||
|
||||
# special
|
||||
IL_76MD: 30,
|
||||
@ -335,10 +392,12 @@ PRICES = {
|
||||
IL_78M: 25,
|
||||
KC_135: 25,
|
||||
KC130: 25,
|
||||
KC135MPRS: 25,
|
||||
|
||||
A_50: 50,
|
||||
KJ_2000: 50,
|
||||
E_3A: 50,
|
||||
E_2C: 50,
|
||||
C_130: 25,
|
||||
Hercules: 25,
|
||||
|
||||
@ -410,6 +469,7 @@ PRICES = {
|
||||
Artillery.MLRS_9K57_Uragan_BM_27: 50,
|
||||
Artillery.MLRS_9A52_Smerch: 40,
|
||||
Artillery._2B11_mortar: 4,
|
||||
Artillery.SpGH_Dana: 26,
|
||||
|
||||
Unarmed.Transport_UAZ_469: 3,
|
||||
Unarmed.Transport_Ural_375: 3,
|
||||
@ -459,14 +519,14 @@ PRICES = {
|
||||
AirDefence.SAM_SA_19_Tunguska_2S6: 30,
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25: 20,
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73: 6,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C: 22,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22,
|
||||
|
||||
AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30,
|
||||
AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25,
|
||||
AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28,
|
||||
AirDefence.SAM_SA_8_Osa_9A33: 28,
|
||||
AirDefence.SAM_SA_15_Tor_9A331: 40,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 16,
|
||||
AirDefence.SAM_SA_9_Strela_1_9P31: 12,
|
||||
AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25,
|
||||
AirDefence.SAM_SA_8_Osa_LD_9T217: 22,
|
||||
AirDefence.SAM_Patriot_AMG_AN_MRC_137: 35,
|
||||
AirDefence.SAM_Patriot_ECS_AN_MSQ_104: 30,
|
||||
@ -481,7 +541,6 @@ PRICES = {
|
||||
AirDefence.SAM_Patriot_EPP_III: 15,
|
||||
AirDefence.SAM_Patriot_ICC: 18,
|
||||
AirDefence.SAM_Roland_ADS: 12,
|
||||
AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18,
|
||||
AirDefence.Stinger_MANPADS: 6,
|
||||
AirDefence.SAM_Stinger_comm_dsr: 4,
|
||||
AirDefence.SAM_Stinger_comm: 4,
|
||||
@ -499,11 +558,8 @@ PRICES = {
|
||||
AirDefence.SAM_SA_18_Igla_S_comm: 8,
|
||||
AirDefence.EWR_1L13: 30,
|
||||
AirDefence.SAM_SA_6_Kub_STR_9S91: 22,
|
||||
AirDefence.SAM_SA_10_S_300PS_TR_30N6: 24,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_5N66M: 30,
|
||||
|
||||
AirDefence.EWR_55G6: 30,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_64H6E: 30,
|
||||
AirDefence.SAM_SA_11_Buk_SR_9S18M1: 28,
|
||||
AirDefence.CP_9S80M1_Sborka: 10,
|
||||
AirDefence.SAM_Hawk_TR_AN_MPQ_46: 14,
|
||||
AirDefence.SAM_Hawk_SR_AN_MPQ_50: 18,
|
||||
@ -564,6 +620,57 @@ PRICES = {
|
||||
frenchpack.DIM__TOYOTA_DESERT: 2,
|
||||
frenchpack.DIM__KAMIKAZE: 6,
|
||||
|
||||
# SA-10
|
||||
AirDefence.SAM_SA_10_S_300PS_CP_54K6: 18,
|
||||
AirDefence.SAM_SA_10_S_300PS_TR_30N6: 24,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_5N66M: 30,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_64H6E: 30,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C: 22,
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85D: 22,
|
||||
|
||||
# High digit sams mod
|
||||
highdigitsams.AAA_SON_9_Fire_Can: 8,
|
||||
highdigitsams.AAA_100mm_KS_19: 10,
|
||||
|
||||
highdigitsams.SAM_SA_10B_S_300PS_54K6_CP: 20,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN: 24,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN: 24,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE: 24,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE: 24,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_30N6_TR: 26,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR: 26,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR: 32,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR: 32,
|
||||
|
||||
highdigitsams.SAM_SA_12_S_300V_9S457_CP: 22,
|
||||
highdigitsams.SAM_SA_12_S_300V_9A82_LN: 26,
|
||||
highdigitsams.SAM_SA_12_S_300V_9A83_LN: 26,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S15_SR: 34,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S19_SR: 34,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S32_TR: 28,
|
||||
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6: 26,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E: 30,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck: 32,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E: 38,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E: 38,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE: 28,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE: 28,
|
||||
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2: 27,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck: 33,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2: 40,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2: 30,
|
||||
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP: 30,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR: 45,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR: 45,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR: 35,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN: 32,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN: 32,
|
||||
|
||||
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2: 40,
|
||||
|
||||
}
|
||||
|
||||
"""
|
||||
@ -574,6 +681,7 @@ Following tasks are present:
|
||||
* CAS - CAS aircraft
|
||||
* Transport - transport aircraft (used as targets in intercept operations)
|
||||
* AWACS - awacs
|
||||
* AntishipStrike - units that will engage shipping
|
||||
* PinpointStrike - armor that will engage in ground war
|
||||
* AirDefense - AA units
|
||||
* Reconnaissance - units that will be used as targets in destroy insurgents operations
|
||||
@ -633,6 +741,7 @@ UNIT_BY_TASK = {
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
B_17G,
|
||||
F_111F,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
@ -655,6 +764,7 @@ UNIT_BY_TASK = {
|
||||
RQ_1A_Predator,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
S_3B,
|
||||
SA342L,
|
||||
SA342M,
|
||||
SA342Minigun,
|
||||
@ -669,7 +779,8 @@ UNIT_BY_TASK = {
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
UH_1H,
|
||||
UH_1H,
|
||||
SH_60B,
|
||||
WingLoong_I,
|
||||
Hercules
|
||||
],
|
||||
@ -685,8 +796,14 @@ UNIT_BY_TASK = {
|
||||
KC_135,
|
||||
KC130,
|
||||
S_3B_Tanker,
|
||||
KC135MPRS,
|
||||
],
|
||||
AWACS: [
|
||||
E_3A,
|
||||
E_2C,
|
||||
A_50,
|
||||
KJ_2000
|
||||
],
|
||||
AWACS: [E_3A, A_50, KJ_2000],
|
||||
PinpointStrike: [
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_MTLB,
|
||||
@ -839,6 +956,7 @@ UNIT_BY_TASK = {
|
||||
Artillery.MLRS_BM_21_Grad,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27,
|
||||
Artillery.MLRS_9A52_Smerch,
|
||||
Artillery.SpGH_Dana,
|
||||
Artillery.M12_GMC,
|
||||
Artillery.Sturmpanzer_IV_Brummbär,
|
||||
|
||||
@ -998,7 +1116,33 @@ COMMON_OVERRIDE = {
|
||||
AntishipStrike: "ANTISHIP",
|
||||
GroundAttack: "STRIKE",
|
||||
Escort: "CAP",
|
||||
RunwayAttack: "RUNWAY_ATTACK"
|
||||
RunwayAttack: "RUNWAY_ATTACK",
|
||||
FighterSweep: "CAP"
|
||||
}
|
||||
|
||||
"""
|
||||
This is a list of mappings from the FlightType of a Flight to the type of payload defined in the
|
||||
resources/payloads/UNIT_TYPE.lua file. A Flight has no concept of a PyDCS task, so COMMON_OVERRIDE cannot be
|
||||
used here. This is used in the payload editor, for setting the default loadout of an object.
|
||||
The left element is the FlightType name, and the right element is a tuple containing what is used in the lua file.
|
||||
Some aircraft differ from the standard loadout names, so those have been included here too.
|
||||
The priority goes from first to last - the first element in the tuple will be tried first, then the second, etc.
|
||||
"""
|
||||
|
||||
EXPANDED_TASK_PAYLOAD_OVERRIDE = {
|
||||
"TARCAP": ("CAP HEAVY", "CAP"),
|
||||
"BARCAP": ("CAP HEAVY", "CAP"),
|
||||
"CAS": ("CAS MAVERICK F", "CAS"),
|
||||
"INTERCEPTION": ("CAP HEAVY", "CAP"),
|
||||
"STRIKE": ("STRIKE",),
|
||||
"ANTISHIP": ("ANTISHIP",),
|
||||
"SEAD": ("SEAD",),
|
||||
"DEAD": ("SEAD",),
|
||||
"ESCORT": ("CAP HEAVY", "CAP"),
|
||||
"BAI": ( "BAI", "CAS MAVERICK F", "CAS"),
|
||||
"SWEEP": ("CAP HEAVY", "CAP"),
|
||||
"OCA_RUNWAY": ("RUNWAY_ATTACK","RUNWAY_STRIKE","STRIKE"),
|
||||
"OCA_AIRCRAFT": ("OCA","CAS MAVERICK F", "CAS")
|
||||
}
|
||||
|
||||
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
@ -1016,6 +1160,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
AntishipStrike: "ANTISHIP",
|
||||
GroundAttack: "STRIKE",
|
||||
Escort: "CAP HEAVY",
|
||||
FighterSweep: "CAP HEAVY",
|
||||
},
|
||||
F_A_18C: {
|
||||
CAP: "CAP HEAVY",
|
||||
@ -1026,6 +1171,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
AntishipStrike: "ANTISHIP",
|
||||
GroundAttack: "STRIKE",
|
||||
Escort: "CAP HEAVY",
|
||||
FighterSweep: "CAP HEAVY",
|
||||
},
|
||||
Tu_160: {
|
||||
PinpointStrike: "Kh-65*12",
|
||||
@ -1041,6 +1187,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
F_14A_135_GR: COMMON_OVERRIDE,
|
||||
F_14B: COMMON_OVERRIDE,
|
||||
F_15C: COMMON_OVERRIDE,
|
||||
F_111F: COMMON_OVERRIDE,
|
||||
F_22A: COMMON_OVERRIDE,
|
||||
F_16C_50: COMMON_OVERRIDE,
|
||||
JF_17: COMMON_OVERRIDE,
|
||||
@ -1066,6 +1213,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
Tornado_IDS: COMMON_OVERRIDE,
|
||||
Mirage_2000_5: COMMON_OVERRIDE,
|
||||
MiG_31: COMMON_OVERRIDE,
|
||||
S_3B: COMMON_OVERRIDE,
|
||||
SA342M: COMMON_OVERRIDE,
|
||||
SA342L: COMMON_OVERRIDE,
|
||||
SA342Mistral: COMMON_OVERRIDE,
|
||||
@ -1103,6 +1251,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
|
||||
AH_1W: COMMON_OVERRIDE,
|
||||
AH_64D: COMMON_OVERRIDE,
|
||||
AH_64A: COMMON_OVERRIDE,
|
||||
SH_60B: COMMON_OVERRIDE,
|
||||
Hercules: COMMON_OVERRIDE,
|
||||
|
||||
Su_25TM: {
|
||||
@ -1170,9 +1319,6 @@ REWARDS = {
|
||||
"derrick": 8
|
||||
}
|
||||
|
||||
# Base post-turn bonus value
|
||||
PLAYER_BUDGET_BASE = 20
|
||||
|
||||
CARRIER_CAPABLE = [
|
||||
FA_18C_hornet,
|
||||
F_14A_135_GR,
|
||||
@ -1181,6 +1327,7 @@ CARRIER_CAPABLE = [
|
||||
Su_33,
|
||||
A_4E_C,
|
||||
Rafale_M,
|
||||
S_3B,
|
||||
|
||||
UH_1H,
|
||||
Mi_8MT,
|
||||
@ -1188,6 +1335,7 @@ CARRIER_CAPABLE = [
|
||||
AH_1W,
|
||||
OH_58D,
|
||||
UH_60A,
|
||||
SH_60B,
|
||||
|
||||
SA342L,
|
||||
SA342M,
|
||||
@ -1204,6 +1352,7 @@ LHA_CAPABLE = [
|
||||
AH_1W,
|
||||
OH_58D,
|
||||
UH_60A,
|
||||
SH_60B,
|
||||
|
||||
SA342L,
|
||||
SA342M,
|
||||
@ -1308,6 +1457,33 @@ def unit_type_name(unit_type) -> str:
|
||||
def unit_type_name_2(unit_type) -> str:
|
||||
return unit_type.name and unit_type.name or unit_type.id
|
||||
|
||||
def unit_get_expanded_info(country_name: str, unit_type, request_type: str) -> str:
|
||||
original_name = unit_type.name and unit_type.name or unit_type.id
|
||||
default_value = None
|
||||
faction_value = None
|
||||
with UNITINFOTEXT_PATH.open("r", encoding="utf-8") as fdata:
|
||||
data = json.load(fdata, encoding="utf-8")
|
||||
type_exists = data.get(original_name)
|
||||
if type_exists is not None:
|
||||
for faction in type_exists:
|
||||
if default_value is None:
|
||||
default_exists = faction.get("default")
|
||||
if default_exists is not None:
|
||||
default_value = default_exists.get(request_type)
|
||||
if faction_value is None:
|
||||
faction_exists = faction.get(country_name)
|
||||
if faction_exists is not None:
|
||||
faction_value = faction_exists.get(request_type)
|
||||
if default_value is None:
|
||||
if request_type == "text":
|
||||
return "WIP - This unit doesn't have any description text yet."
|
||||
if request_type == "name":
|
||||
return original_name
|
||||
else:
|
||||
return "Unknown"
|
||||
if faction_value is None:
|
||||
return default_value
|
||||
return faction_value
|
||||
|
||||
def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
if name in vehicle_map:
|
||||
|
||||
@ -197,6 +197,11 @@ class Debriefing:
|
||||
continue
|
||||
|
||||
building = self.unit_map.building_or_fortification(unit_name)
|
||||
# Try appending object to the name, because we do this for building statics.
|
||||
if building is None:
|
||||
building = self.unit_map.building_or_fortification(
|
||||
f"{unit_name} object"
|
||||
)
|
||||
if building is not None:
|
||||
if building.ground_object.control_point.captured:
|
||||
losses.player_buildings.append(building)
|
||||
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from typing import Dict, List, TYPE_CHECKING, Type
|
||||
from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import Task
|
||||
@ -15,16 +15,13 @@ from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen import AirTaskingOrder
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from ..db import PRICES
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
|
||||
|
||||
DIFFICULTY_LOG_BASE = 1.1
|
||||
EVENT_DEPARTURE_MAX_DISTANCE = 340000
|
||||
|
||||
|
||||
MINOR_DEFEAT_INFLUENCE = 0.1
|
||||
DEFEAT_INFLUENCE = 0.3
|
||||
STRONG_DEFEAT_INFLUENCE = 0.5
|
||||
@ -39,7 +36,6 @@ class Event:
|
||||
from_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
difficulty = 1 # type: int
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
self.game = game
|
||||
@ -57,9 +53,6 @@ class Event:
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
return []
|
||||
|
||||
def bonus(self) -> int:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
|
||||
def generate(self) -> UnitMap:
|
||||
Operation.prepare(self.game)
|
||||
unit_map = Operation.generate()
|
||||
@ -303,9 +296,6 @@ class Event:
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
def skip(self):
|
||||
pass
|
||||
|
||||
def redeploy_units(self, cp):
|
||||
""""
|
||||
Auto redeploy units to newly captured base
|
||||
@ -347,36 +337,63 @@ class Event:
|
||||
logging.info(info.text)
|
||||
|
||||
|
||||
class UnitsDeliveryEvent(Event):
|
||||
|
||||
informational = True
|
||||
|
||||
def __init__(self, attacker_name: str, defender_name: str,
|
||||
from_cp: ControlPoint, to_cp: ControlPoint,
|
||||
game: Game) -> None:
|
||||
super(UnitsDeliveryEvent, self).__init__(game=game,
|
||||
location=to_cp.position,
|
||||
from_cp=from_cp,
|
||||
target_cp=to_cp,
|
||||
attacker_name=attacker_name,
|
||||
defender_name=defender_name)
|
||||
class UnitsDeliveryEvent:
|
||||
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.to_cp = control_point
|
||||
self.units: Dict[Type[UnitType], int] = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Pending delivery to {}".format(self.to_cp)
|
||||
|
||||
def deliver(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
def order(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) + v
|
||||
|
||||
def skip(self) -> None:
|
||||
for k, v in self.units.items():
|
||||
if self.to_cp.captured:
|
||||
name = "Ally "
|
||||
else:
|
||||
name = "Enemy "
|
||||
self.game.message(
|
||||
f"{name} reinforcements: {k.id} x {v} at {self.to_cp.name}")
|
||||
def sell(self, units: Dict[Type[UnitType], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) - v
|
||||
|
||||
self.to_cp.base.commision_units(self.units)
|
||||
def consume_each_order(self) -> Iterator[Tuple[Type[UnitType], int]]:
|
||||
while self.units:
|
||||
yield self.units.popitem()
|
||||
|
||||
def refund_all(self, game: Game) -> None:
|
||||
for unit_type, count in self.consume_each_order():
|
||||
try:
|
||||
price = PRICES[unit_type]
|
||||
except KeyError:
|
||||
logging.error(f"Could not refund {unit_type.id}, price unknown")
|
||||
continue
|
||||
|
||||
logging.info(
|
||||
f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
|
||||
game.adjust_budget(price * count, player=self.to_cp.captured)
|
||||
|
||||
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
|
||||
pending_units = self.units.get(unit_type)
|
||||
if pending_units is None:
|
||||
pending_units = 0
|
||||
current_units = self.to_cp.base.total_units_of_type(unit_type)
|
||||
return pending_units + current_units
|
||||
|
||||
def process(self, game: Game) -> None:
|
||||
bought_units: Dict[Type[UnitType], int] = {}
|
||||
sold_units: Dict[Type[UnitType], int] = {}
|
||||
for unit_type, count in self.units.items():
|
||||
coalition = "Ally" if self.to_cp.captured else "Enemy"
|
||||
aircraft = unit_type.id
|
||||
name = self.to_cp.name
|
||||
if count >= 0:
|
||||
bought_units[unit_type] = count
|
||||
game.message(
|
||||
f"{coalition} reinforcements: {aircraft} x {count} at {name}")
|
||||
else:
|
||||
sold_units[unit_type] = -count
|
||||
game.message(
|
||||
f"{coalition} sold: {aircraft} x {-count} at {name}")
|
||||
self.to_cp.base.commision_units(bought_units)
|
||||
self.to_cp.base.commit_losses(sold_units)
|
||||
self.units = {}
|
||||
bought_units = {}
|
||||
sold_units = {}
|
||||
|
||||
107
game/game.py
107
game/game.py
@ -4,7 +4,7 @@ import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
@ -28,9 +28,12 @@ from .event.frontlineattack import FrontlineAttackEvent
|
||||
from .factions.faction import Faction
|
||||
from .income import Income
|
||||
from .infos.information import Information
|
||||
from .navmesh import NavMesh
|
||||
from .procurement import ProcurementAi
|
||||
from .settings import Settings
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
from .theater import ConflictTheater, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import MissileSiteGroundObject
|
||||
from .threatzones import ThreatZones
|
||||
from .unitmap import UnitMap
|
||||
from .weather import Conditions, TimeOfDay
|
||||
|
||||
@ -67,16 +70,18 @@ AWACS_BUDGET_COST = 4
|
||||
# Bonus multiplier logarithm base
|
||||
PLAYER_BUDGET_IMPORTANCE_LOG = 2
|
||||
|
||||
|
||||
class TurnState(Enum):
|
||||
WIN = 0
|
||||
LOSS = 1
|
||||
CONTINUE = 2
|
||||
|
||||
|
||||
class Game:
|
||||
def __init__(self, player_name: str, enemy_name: str,
|
||||
theater: ConflictTheater, start_date: datetime,
|
||||
settings: Settings, player_budget: int,
|
||||
enemy_budget: int) -> None:
|
||||
settings: Settings, player_budget: float,
|
||||
enemy_budget: float) -> None:
|
||||
self.settings = settings
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
@ -108,9 +113,6 @@ class Game:
|
||||
self.theater.controlpoints
|
||||
)
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
self.on_load()
|
||||
@ -126,6 +128,21 @@ class Game:
|
||||
|
||||
self.plan_procurement(blue_planner, red_planner)
|
||||
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
del state["blue_threat_zone"]
|
||||
del state["red_threat_zone"]
|
||||
del state["blue_navmesh"]
|
||||
del state["red_navmesh"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
|
||||
def generate_conditions(self) -> Conditions:
|
||||
return Conditions.generate(self.theater, self.date,
|
||||
self.current_turn_time_of_day, self.settings)
|
||||
@ -151,6 +168,11 @@ class Game:
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
if player:
|
||||
return self.player_faction
|
||||
return self.enemy_faction
|
||||
|
||||
def _roll(self, prob, mult):
|
||||
if self.settings.version == "dev":
|
||||
# always generate all events for dev
|
||||
@ -167,6 +189,12 @@ class Game:
|
||||
front_line.control_point_a,
|
||||
front_line.control_point_b)
|
||||
|
||||
def adjust_budget(self, amount: float, player: bool) -> None:
|
||||
if player:
|
||||
self.budget += amount
|
||||
else:
|
||||
self.enemy_budget += amount
|
||||
|
||||
def process_player_income(self):
|
||||
self.budget += Income(self, player=True).total
|
||||
|
||||
@ -176,15 +204,6 @@ class Game:
|
||||
self.enemy_budget = 0
|
||||
self.enemy_budget += Income(self, player=False).total
|
||||
|
||||
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
|
||||
event = UnitsDeliveryEvent(attacker_name=self.player_name,
|
||||
defender_name=self.player_name,
|
||||
from_cp=to_cp,
|
||||
to_cp=to_cp,
|
||||
game=self)
|
||||
self.events.append(event)
|
||||
return event
|
||||
|
||||
def initiate_event(self, event: Event) -> UnitMap:
|
||||
#assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
@ -193,8 +212,6 @@ class Game:
|
||||
def finish_event(self, event: Event, debriefing: Debriefing):
|
||||
logging.info("Finishing event {}".format(event))
|
||||
event.commit(debriefing)
|
||||
self.budget += int(event.bonus() *
|
||||
self.settings.player_income_multiplier)
|
||||
|
||||
if event in self.events:
|
||||
self.events.remove(event)
|
||||
@ -211,22 +228,15 @@ class Game:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
|
||||
self.turn += 1
|
||||
|
||||
for event in self.events:
|
||||
if self.settings.version == "dev":
|
||||
# don't damage player CPs in by skipping in dev mode
|
||||
if isinstance(event, UnitsDeliveryEvent):
|
||||
event.skip()
|
||||
else:
|
||||
event.skip()
|
||||
|
||||
for control_point in self.theater.controlpoints:
|
||||
control_point.process_turn()
|
||||
control_point.process_turn(self)
|
||||
|
||||
self.process_enemy_income()
|
||||
|
||||
@ -264,7 +274,6 @@ class Game:
|
||||
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
cp.pending_unit_deliveries = self.units_delivery_event(cp)
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Check for win or loss condition
|
||||
@ -274,6 +283,7 @@ class Game:
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
self.ground_planners = {}
|
||||
self.blue_ato.clear()
|
||||
self.red_ato.clear()
|
||||
@ -294,13 +304,20 @@ class Game:
|
||||
|
||||
def plan_procurement(self, blue_planner: CoalitionMissionPlanner,
|
||||
red_planner: CoalitionMissionPlanner) -> None:
|
||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
||||
# gets much more of the budget that turn. Otherwise budget (after
|
||||
# repairs) is split evenly between air and ground. For the default
|
||||
# starting budget of 2000 this gives 600 to ground forces and 1400 to
|
||||
# aircraft.
|
||||
ground_portion = 0.3 if self.turn == 0 else 0.5
|
||||
self.budget = ProcurementAi(
|
||||
self,
|
||||
for_player=True,
|
||||
faction=self.player_faction,
|
||||
manage_runways=self.settings.automate_runway_repair,
|
||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||
front_line_budget_share=ground_portion
|
||||
).spend_budget(self.budget, blue_planner.procurement_requests)
|
||||
|
||||
self.enemy_budget = ProcurementAi(
|
||||
@ -309,7 +326,8 @@ class Game:
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True
|
||||
manage_aircraft=True,
|
||||
front_line_budget_share=ground_portion
|
||||
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
@ -337,6 +355,24 @@ class Game:
|
||||
self.current_group_id += 1
|
||||
return self.current_group_id
|
||||
|
||||
def compute_threat_zones(self) -> None:
|
||||
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
|
||||
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
|
||||
self.blue_navmesh = NavMesh.from_threat_zones(self.red_threat_zone,
|
||||
self.theater)
|
||||
self.red_navmesh = NavMesh.from_threat_zones(self.blue_threat_zone,
|
||||
self.theater)
|
||||
|
||||
def threat_zone_for(self, player: bool) -> ThreatZones:
|
||||
if player:
|
||||
return self.blue_threat_zone
|
||||
return self.red_threat_zone
|
||||
|
||||
def navmesh_for(self, player: bool) -> NavMesh:
|
||||
if player:
|
||||
return self.blue_navmesh
|
||||
return self.red_navmesh
|
||||
|
||||
def compute_conflicts_position(self):
|
||||
"""
|
||||
Compute the current conflict center position(s), mainly used for culling calculation
|
||||
@ -353,9 +389,14 @@ class Game:
|
||||
points.append(front_line.control_point_a.position)
|
||||
points.append(front_line.control_point_b.position)
|
||||
|
||||
# If do_not_cull_carrier is enabled, add carriers as culling point
|
||||
if self.settings.perf_do_not_cull_carrier:
|
||||
for cp in self.theater.controlpoints:
|
||||
for cp in self.theater.controlpoints:
|
||||
# Don't cull missile sites - their range is long enough to make them
|
||||
# easily culled despite being a threat.
|
||||
for tgo in cp.ground_objects:
|
||||
if isinstance(tgo, MissileSiteGroundObject):
|
||||
points.append(cp.position)
|
||||
# If do_not_cull_carrier is enabled, add carriers as culling point
|
||||
if self.settings.perf_do_not_cull_carrier:
|
||||
if cp.is_carrier or cp.is_lha:
|
||||
points.append(cp.position)
|
||||
|
||||
|
||||
@ -3,8 +3,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.db import PLAYER_BUDGET_BASE, REWARDS
|
||||
from game.theater import ControlPoint
|
||||
from game.db import REWARDS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -22,12 +21,6 @@ class BuildingIncome:
|
||||
return self.number * self.income_per_building
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ControlPointIncome:
|
||||
control_point: ControlPoint
|
||||
income: int
|
||||
|
||||
|
||||
class Income:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
if player:
|
||||
@ -37,12 +30,10 @@ class Income:
|
||||
self.control_points = []
|
||||
self.buildings = []
|
||||
|
||||
self.income_per_base = PLAYER_BUDGET_BASE if player else 0
|
||||
|
||||
names = set()
|
||||
for cp in game.theater.control_points_for(player):
|
||||
self.control_points.append(
|
||||
ControlPointIncome(cp, self.income_per_base))
|
||||
if cp.income_per_turn:
|
||||
self.control_points.append(cp)
|
||||
for tgo in cp.ground_objects:
|
||||
names.add(tgo.obj_name)
|
||||
|
||||
@ -58,7 +49,7 @@ class Income:
|
||||
self.buildings.append(BuildingIncome(name, category, count,
|
||||
REWARDS[category]))
|
||||
|
||||
self.from_bases = sum(cp.income for cp in self.control_points)
|
||||
self.from_bases = sum(cp.income_per_turn for cp in self.control_points)
|
||||
self.total_buildings = sum(b.income for b in self.buildings)
|
||||
self.total = ((self.total_buildings + self.from_bases) *
|
||||
self.multiplier)
|
||||
|
||||
270
game/navmesh.py
Normal file
270
game/navmesh.py
Normal file
@ -0,0 +1,270 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
MultiPolygon,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
box,
|
||||
)
|
||||
from shapely.ops import nearest_points, triangulate
|
||||
|
||||
from game.theater import ConflictTheater
|
||||
from game.threatzones import ThreatZones
|
||||
from game.utils import nautical_miles
|
||||
|
||||
|
||||
class NavMeshPoly:
|
||||
def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None:
|
||||
self.ident = ident
|
||||
self.poly = poly
|
||||
self.threatened = threatened
|
||||
self.neighbors: Dict[NavMeshPoly, Union[LineString, ShapelyPoint]] = {}
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, NavMeshPoly):
|
||||
return False
|
||||
return self.ident == other.ident
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return self.ident
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NavPoint:
|
||||
point: ShapelyPoint
|
||||
poly: NavMeshPoly
|
||||
|
||||
@property
|
||||
def world_point(self) -> Point:
|
||||
return Point(self.point.x, self.point.y)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.poly.ident)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if id(self) == id(other):
|
||||
return True
|
||||
|
||||
if not isinstance(other, NavPoint):
|
||||
return False
|
||||
|
||||
if not self.point.almost_equals(other.point):
|
||||
return False
|
||||
|
||||
return self.poly == other.poly
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.point} in {self.poly.ident}"
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class FrontierNode:
|
||||
cost: float
|
||||
point: NavPoint = field(compare=False)
|
||||
|
||||
|
||||
class NavFrontier:
|
||||
def __init__(self) -> None:
|
||||
self.nodes: List[FrontierNode] = []
|
||||
|
||||
def push(self, poly: NavPoint, cost: float) -> None:
|
||||
heapq.heappush(self.nodes, FrontierNode(cost, poly))
|
||||
|
||||
def pop(self) -> Optional[NavPoint]:
|
||||
try:
|
||||
return heapq.heappop(self.nodes).point
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
class NavMesh:
|
||||
def __init__(self, polys: List[NavMeshPoly]) -> None:
|
||||
self.polys = polys
|
||||
|
||||
def localize(self, point: Point) -> Optional[NavMeshPoly]:
|
||||
# This is a naive implementation but it's O(n). Runs at about 10k
|
||||
# lookups a second on a 5950X. Flights usually have 5-10 waypoints, so
|
||||
# that's 1k-2k flights before we lose a full second to localization as a
|
||||
# part of flight plan creation.
|
||||
#
|
||||
# Can improve the algorithm later if needed, but that seems unnecessary
|
||||
# currently.
|
||||
p = ShapelyPoint(point.x, point.y)
|
||||
for navpoly in self.polys:
|
||||
if navpoly.poly.contains(p):
|
||||
return navpoly
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def travel_cost(a: NavPoint, b: NavPoint) -> float:
|
||||
modifier = 1.0
|
||||
if a.poly.threatened:
|
||||
modifier = 3.0
|
||||
return a.point.distance(b.point) * modifier
|
||||
|
||||
def travel_heuristic(self, a: NavPoint, b: NavPoint) -> float:
|
||||
return self.travel_cost(a, b)
|
||||
|
||||
@staticmethod
|
||||
def reconstruct_path(came_from: Dict[NavPoint, Optional[NavPoint]],
|
||||
origin: NavPoint,
|
||||
destination: NavPoint) -> List[Point]:
|
||||
current = destination
|
||||
path: List[Point] = []
|
||||
while current != origin:
|
||||
path.append(current.world_point)
|
||||
previous = came_from[current]
|
||||
if previous is None:
|
||||
raise RuntimeError(
|
||||
f"Could not reconstruct path to {destination} from {origin}"
|
||||
)
|
||||
current = previous
|
||||
path.append(origin.world_point)
|
||||
path.reverse()
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def dcs_to_shapely_point(point: Point) -> ShapelyPoint:
|
||||
return ShapelyPoint(point.x, point.y)
|
||||
|
||||
def shortest_path(self, origin: Point, destination: Point) -> List[Point]:
|
||||
origin_poly = self.localize(origin)
|
||||
if origin_poly is None:
|
||||
raise ValueError(f"Origin point {origin} is outside the navmesh")
|
||||
destination_poly = self.localize(destination)
|
||||
if destination_poly is None:
|
||||
raise ValueError(
|
||||
f"Origin point {destination} is outside the navmesh")
|
||||
|
||||
return self._shortest_path(
|
||||
NavPoint(self.dcs_to_shapely_point(origin), origin_poly),
|
||||
NavPoint(self.dcs_to_shapely_point(destination), destination_poly)
|
||||
)
|
||||
|
||||
def _shortest_path(self, origin: NavPoint,
|
||||
destination: NavPoint) -> List[Point]:
|
||||
# Adapted from
|
||||
# https://www.redblobgames.com/pathfinding/a-star/implementation.py.
|
||||
frontier = NavFrontier()
|
||||
frontier.push(origin, 0.0)
|
||||
came_from: Dict[NavPoint, Optional[NavPoint]] = {origin: None}
|
||||
|
||||
best_known: Dict[NavPoint, float] = defaultdict(lambda: math.inf)
|
||||
best_known[origin] = 0.0
|
||||
|
||||
while (current := frontier.pop()) is not None:
|
||||
if current == destination:
|
||||
break
|
||||
|
||||
if current.poly == destination.poly:
|
||||
# Made it to the correct nav poly. Add the leg from the border
|
||||
# to the target.
|
||||
cost = best_known[current] + self.travel_cost(
|
||||
current, destination
|
||||
)
|
||||
if cost < best_known[destination]:
|
||||
best_known[destination] = cost
|
||||
estimated = cost
|
||||
frontier.push(destination, estimated)
|
||||
came_from[destination] = current
|
||||
|
||||
for neighbor, boundary in current.poly.neighbors.items():
|
||||
previous = came_from[current]
|
||||
if previous is not None and previous.poly == neighbor:
|
||||
# Don't backtrack.
|
||||
continue
|
||||
if previous is None and current != origin:
|
||||
raise RuntimeError
|
||||
_, neighbor_point = nearest_points(current.point, boundary)
|
||||
neighbor_nav = NavPoint(neighbor_point, neighbor)
|
||||
cost = best_known[current] + self.travel_cost(
|
||||
current, neighbor_nav
|
||||
)
|
||||
if cost < best_known[neighbor_nav]:
|
||||
best_known[neighbor_nav] = cost
|
||||
estimated = cost + self.travel_heuristic(
|
||||
neighbor_nav, destination
|
||||
)
|
||||
frontier.push(neighbor_nav, estimated)
|
||||
came_from[neighbor_nav] = current
|
||||
|
||||
return self.reconstruct_path(came_from, origin, destination)
|
||||
|
||||
@staticmethod
|
||||
def map_bounds(theater: ConflictTheater) -> Polygon:
|
||||
points = []
|
||||
for cp in theater.controlpoints:
|
||||
points.append(ShapelyPoint(cp.position.x, cp.position.y))
|
||||
for tgo in cp.ground_objects:
|
||||
points.append(ShapelyPoint(tgo.position.x, tgo.position.y))
|
||||
# Needs to be a large enough boundary beyond the known points so that
|
||||
# threatened airbases at the map edges have room to retreat from the
|
||||
# threat without running off the navmesh.
|
||||
return box(*LineString(points).bounds).buffer(
|
||||
nautical_miles(100).meters, resolution=1)
|
||||
|
||||
@staticmethod
|
||||
def create_navpolys(polys: List[Polygon],
|
||||
threat_zones: ThreatZones) -> List[NavMeshPoly]:
|
||||
return [NavMeshPoly(i, p, threat_zones.threatened(p))
|
||||
for i, p in enumerate(polys)]
|
||||
|
||||
@staticmethod
|
||||
def associate_neighbors(polys: List[NavMeshPoly]) -> None:
|
||||
# Maps (rounded) points to polygons that have a vertex at that point.
|
||||
# The points are rounded to the nearest int so we can use them as dict
|
||||
# keys. This allows us to perform approximate neighbor lookups more
|
||||
# efficiently than comparing each poly to every other poly by finding
|
||||
# approximate neighbors before checking if the polys actually touch.
|
||||
points_map: Dict[Tuple[int, int], Set[NavMeshPoly]] = defaultdict(set)
|
||||
|
||||
for navpoly in polys:
|
||||
# The coordinates of the polygon's boundary are a sequence of
|
||||
# coordinates that define the polygon. The first point is repeated
|
||||
# at the end, so skip the last vertex.
|
||||
for x, y in navpoly.poly.boundary.coords[:-1]:
|
||||
point = (int(x), int(y))
|
||||
neighbors = {}
|
||||
for potential_neighbor in points_map[point]:
|
||||
intersection = navpoly.poly.intersection(
|
||||
potential_neighbor.poly)
|
||||
if not intersection.is_empty:
|
||||
potential_neighbor.neighbors[navpoly] = intersection
|
||||
neighbors[potential_neighbor] = intersection
|
||||
navpoly.neighbors.update(neighbors)
|
||||
points_map[point].add(navpoly)
|
||||
|
||||
@classmethod
|
||||
def from_threat_zones(cls, threat_zones: ThreatZones,
|
||||
theater: ConflictTheater) -> NavMesh:
|
||||
# Simplify the threat poly to reduce the number of nav zones. Increase
|
||||
# the size of the zone and then simplify it with the buffer size as the
|
||||
# error margin. This will create a simpler poly around the threat zone.
|
||||
buffer = nautical_miles(10).meters
|
||||
threat_poly = threat_zones.all.buffer(buffer).simplify(buffer)
|
||||
|
||||
# Threat zones can be disconnected. Create a list of threat zones.
|
||||
if isinstance(threat_poly, MultiPolygon):
|
||||
polys = list(threat_poly.geoms)
|
||||
else:
|
||||
polys = [threat_poly]
|
||||
|
||||
# Subtract the threat zones from the whole-map poly to build a navmesh
|
||||
# for the *safe* areas. Navigation within threatened regions is always
|
||||
# a straight line to the target or out of the threatened region.
|
||||
bounds = cls.map_bounds(theater)
|
||||
for poly in polys:
|
||||
bounds = bounds.difference(poly)
|
||||
|
||||
# Triangulate the safe-region to build the navmesh.
|
||||
navpolys = cls.create_navpolys(triangulate(bounds), threat_zones)
|
||||
cls.associate_neighbors(navpolys)
|
||||
return cls(navpolys)
|
||||
@ -26,12 +26,12 @@ from gen.environmentgen import EnvironmentGenerator
|
||||
from gen.forcedoptionsgen import ForcedOptionsGenerator
|
||||
from gen.groundobjectsgen import GroundObjectsGenerator
|
||||
from gen.kneeboard import KneeboardGenerator
|
||||
from gen.naming import namegen
|
||||
from gen.radios import RadioFrequency, RadioRegistry
|
||||
from gen.tacan import TacanRegistry
|
||||
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
|
||||
|
||||
from .. import db
|
||||
from ..debriefing import Debriefing
|
||||
from ..theater import Airfield
|
||||
from ..unitmap import UnitMap
|
||||
|
||||
@ -86,7 +86,7 @@ class Operation:
|
||||
cls.game.enemy_country,
|
||||
frontline.position
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def air_conflict(cls) -> Conflict:
|
||||
assert cls.game
|
||||
@ -103,7 +103,7 @@ class Operation:
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
mid_point
|
||||
mid_point
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -295,7 +295,7 @@ class Operation:
|
||||
heading=d["orientation"],
|
||||
dead=True,
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def generate(cls) -> UnitMap:
|
||||
"""Build the final Mission to be exported"""
|
||||
@ -349,7 +349,7 @@ class Operation:
|
||||
cls.jtacs,
|
||||
cls.airgen
|
||||
)
|
||||
|
||||
cls.reset_naming_ids()
|
||||
return cls.unit_map
|
||||
|
||||
@classmethod
|
||||
@ -411,6 +411,10 @@ class Operation:
|
||||
ground_conflict_gen.generate()
|
||||
cls.jtacs.extend(ground_conflict_gen.jtacs)
|
||||
|
||||
@classmethod
|
||||
def reset_naming_ids(cls):
|
||||
namegen.reset_numbers()
|
||||
|
||||
@classmethod
|
||||
def generate_lua(cls, airgen: AircraftConflictGenerator,
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
|
||||
@ -5,18 +5,16 @@ import random
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.task import CAP, CAS
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint, MissionTarget, TYPE_SHORAD
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
capable_aircraft_for_task,
|
||||
preferred_aircraft_for_task,
|
||||
)
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import Distance
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -25,36 +23,75 @@ if TYPE_CHECKING:
|
||||
@dataclass(frozen=True)
|
||||
class AircraftProcurementRequest:
|
||||
near: MissionTarget
|
||||
range: int
|
||||
range: Distance
|
||||
task_capability: FlightType
|
||||
number: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
task = self.task_capability.value
|
||||
distance = self.range.nautical_miles
|
||||
target = self.near.name
|
||||
return f"{self.number} ship {task} within {distance} nm of {target}"
|
||||
|
||||
|
||||
class ProcurementAi:
|
||||
def __init__(self, game: Game, for_player: bool, faction: Faction,
|
||||
manage_runways: bool, manage_front_line: bool,
|
||||
manage_aircraft: bool) -> None:
|
||||
manage_aircraft: bool, front_line_budget_share: float) -> None:
|
||||
if front_line_budget_share > 1.0:
|
||||
raise ValueError
|
||||
|
||||
self.game = game
|
||||
self.is_player = for_player
|
||||
self.faction = faction
|
||||
self.manage_runways = manage_runways
|
||||
self.manage_front_line = manage_front_line
|
||||
self.manage_aircraft = manage_aircraft
|
||||
self.front_line_budget_share = front_line_budget_share
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def spend_budget(
|
||||
self, budget: int,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> int:
|
||||
self, budget: float,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> float:
|
||||
if self.manage_runways:
|
||||
budget = self.repair_runways(budget)
|
||||
if self.manage_front_line:
|
||||
armor_budget = math.ceil(budget / 2)
|
||||
armor_budget = math.ceil(budget * self.front_line_budget_share)
|
||||
budget -= armor_budget
|
||||
budget += self.reinforce_front_line(armor_budget)
|
||||
|
||||
# Don't sell overstock aircraft until after we've bought runways and
|
||||
# front lines. Any budget we free up should be earmarked for aircraft.
|
||||
if not self.is_player:
|
||||
budget += self.sell_incomplete_squadrons()
|
||||
if self.manage_aircraft:
|
||||
budget = self.purchase_aircraft(budget, aircraft_requests)
|
||||
return budget
|
||||
|
||||
def repair_runways(self, budget: int) -> int:
|
||||
def sell_incomplete_squadrons(self) -> float:
|
||||
# Selling incomplete squadrons gives us more money to spend on the next
|
||||
# turn. This serves as a short term fix for
|
||||
# https://github.com/Khopa/dcs_liberation/issues/41.
|
||||
#
|
||||
# Only incomplete squadrons which are unlikely to get used will be sold
|
||||
# rather than all unused aircraft because the unused aircraft are what
|
||||
# make OCA strikes worthwhile.
|
||||
#
|
||||
# This option is only used by the AI since players cannot cancel sales
|
||||
# (https://github.com/Khopa/dcs_liberation/issues/365).
|
||||
total = 0.0
|
||||
for cp in self.game.theater.control_points_for(self.is_player):
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for aircraft, available in inventory.all_aircraft:
|
||||
# We only ever plan even groups, so the odd aircraft is unlikely
|
||||
# to get used.
|
||||
if available % 2 == 0:
|
||||
continue
|
||||
inventory.remove_aircraft(aircraft, 1)
|
||||
total += db.PRICES[aircraft]
|
||||
return total
|
||||
|
||||
def repair_runways(self, budget: float) -> float:
|
||||
for control_point in self.owned_points:
|
||||
if budget < db.RUNWAY_REPAIR_COST:
|
||||
break
|
||||
@ -74,7 +111,8 @@ class ProcurementAi:
|
||||
return budget
|
||||
|
||||
def random_affordable_ground_unit(
|
||||
self, budget: int, cp: ControlPoint) -> Optional[Type[VehicleType]]:
|
||||
self, budget: float,
|
||||
cp: ControlPoint) -> Optional[Type[VehicleType]]:
|
||||
affordable_units = [u for u in self.faction.frontline_units + self.faction.artillery_units if
|
||||
db.PRICES[u] <= budget]
|
||||
|
||||
@ -91,7 +129,7 @@ class ProcurementAi:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
|
||||
def reinforce_front_line(self, budget: int) -> int:
|
||||
def reinforce_front_line(self, budget: float) -> float:
|
||||
if not self.faction.frontline_units and not self.faction.artillery_units:
|
||||
return budget
|
||||
|
||||
@ -107,43 +145,38 @@ class ProcurementAi:
|
||||
break
|
||||
|
||||
budget -= db.PRICES[unit]
|
||||
assert cp.pending_unit_deliveries is not None
|
||||
cp.pending_unit_deliveries.deliver({unit: 1})
|
||||
cp.pending_unit_deliveries.order({unit: 1})
|
||||
|
||||
return budget
|
||||
|
||||
def _affordable_aircraft_of_types(
|
||||
self, types: List[Type[FlyingType]], airbase: ControlPoint,
|
||||
number: int, max_price: int) -> Optional[Type[FlyingType]]:
|
||||
unit_pool = [u for u in self.faction.aircrafts if u in types]
|
||||
affordable_units = [
|
||||
u for u in unit_pool
|
||||
if db.PRICES[u] * number <= max_price and airbase.can_operate(u)
|
||||
]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
number: int, max_price: float) -> Optional[Type[FlyingType]]:
|
||||
best_choice: Optional[Type[FlyingType]] = None
|
||||
for unit in [u for u in self.faction.aircrafts if u in types]:
|
||||
if db.PRICES[unit] * number > max_price:
|
||||
continue
|
||||
if not airbase.can_operate(unit):
|
||||
continue
|
||||
|
||||
# Affordable and compatible. To keep some variety, skip with a 50/50
|
||||
# chance. Might be a good idea to have the chance to skip based on
|
||||
# the price compared to the rest of the choices.
|
||||
best_choice = unit
|
||||
if random.choice([True, False]):
|
||||
break
|
||||
return best_choice
|
||||
|
||||
def affordable_aircraft_for(
|
||||
self, request: AircraftProcurementRequest,
|
||||
airbase: ControlPoint, budget: int) -> Optional[Type[FlyingType]]:
|
||||
aircraft = self._affordable_aircraft_of_types(
|
||||
preferred_aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
if aircraft is not None:
|
||||
return aircraft
|
||||
airbase: ControlPoint, budget: float) -> Optional[Type[FlyingType]]:
|
||||
return self._affordable_aircraft_of_types(
|
||||
capable_aircraft_for_task(request.task_capability),
|
||||
aircraft_for_task(request.task_capability),
|
||||
airbase, request.number, budget)
|
||||
|
||||
def purchase_aircraft(
|
||||
self, budget: int,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> int:
|
||||
unit_pool = [u for u in self.faction.aircrafts
|
||||
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
|
||||
if not unit_pool:
|
||||
return budget
|
||||
|
||||
self, budget: float,
|
||||
aircraft_requests: List[AircraftProcurementRequest]) -> float:
|
||||
for request in aircraft_requests:
|
||||
for airbase in self.best_airbases_for(request):
|
||||
unit = self.affordable_aircraft_for(request, airbase, budget)
|
||||
@ -156,8 +189,7 @@ class ProcurementAi:
|
||||
continue
|
||||
|
||||
budget -= db.PRICES[unit] * request.number
|
||||
assert airbase.pending_unit_deliveries is not None
|
||||
airbase.pending_unit_deliveries.deliver({unit: request.number})
|
||||
airbase.pending_unit_deliveries.order({unit: request.number})
|
||||
|
||||
return budget
|
||||
|
||||
@ -174,6 +206,7 @@ class ProcurementAi:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
request.near
|
||||
)
|
||||
threatened = []
|
||||
for cp in distance_cache.airfields_within(request.range):
|
||||
if not cp.is_friendly(self.is_player):
|
||||
continue
|
||||
@ -181,7 +214,10 @@ class ProcurementAi:
|
||||
continue
|
||||
if cp.unclaimed_parking(self.game) < request.number:
|
||||
continue
|
||||
if self.threat_zones.threatened(cp.position):
|
||||
threatened.append(cp)
|
||||
yield cp
|
||||
yield from threatened
|
||||
|
||||
def front_line_candidates(self) -> List[ControlPoint]:
|
||||
candidates = []
|
||||
@ -189,7 +225,7 @@ class ProcurementAi:
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
if cp.base.total_armor >= 30:
|
||||
if cp.expected_ground_units_next_turn.total >= 30:
|
||||
# Control point is already sufficiently defended.
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
@ -197,8 +233,23 @@ class ProcurementAi:
|
||||
candidates.append(cp)
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy them anywhere valid.
|
||||
candidates = [p for p in self.owned_points
|
||||
if p.can_deploy_ground_units]
|
||||
# Otherwise buy reserves, but don't exceed 10 reserve units per CP.
|
||||
# These units do not exist in the world until the CP becomes
|
||||
# connected to an active front line, at which point all these units
|
||||
# will suddenly appear at the gates of the newly captured CP.
|
||||
#
|
||||
# To avoid sudden overwhelming numbers of units we avoid buying
|
||||
# many.
|
||||
#
|
||||
# Also, do not bother buying units at bases that will never connect
|
||||
# to a front line.
|
||||
for cp in self.owned_points:
|
||||
if not cp.can_deploy_ground_units:
|
||||
continue
|
||||
if cp.expected_ground_units_next_turn.total >= 10:
|
||||
continue
|
||||
if cp.is_global:
|
||||
continue
|
||||
candidates.append(cp)
|
||||
|
||||
return candidates
|
||||
|
||||
@ -19,15 +19,17 @@ class Settings:
|
||||
supercarrier: bool = False
|
||||
generate_marks: bool = True
|
||||
manpads: bool = True
|
||||
cold_start: bool = False # Legacy parameter do not use
|
||||
version: Optional[str] = None
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
|
||||
default_start_type: str = "Cold"
|
||||
|
||||
# Campaign management
|
||||
automate_runway_repair: bool = False
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
restrict_weapons_by_date: bool = False
|
||||
|
||||
# Performance oriented
|
||||
perf_red_alert_state: bool = True
|
||||
@ -35,7 +37,6 @@ class Settings:
|
||||
perf_artillery: bool = True
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_ai_parking_start: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
|
||||
# Performance culling
|
||||
@ -48,6 +49,8 @@ class Settings:
|
||||
|
||||
# Cheating
|
||||
show_red_ato: bool = False
|
||||
enable_frontline_cheats: bool = False
|
||||
enable_base_capture_cheat: bool = False
|
||||
|
||||
never_delay_player_flights: bool = False
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ from dcs.unittype import FlyingType, UnitType, VehicleType
|
||||
from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
from game.db import PRICES
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
|
||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||
@ -37,6 +38,16 @@ class Base:
|
||||
def total_armor(self) -> int:
|
||||
return sum(self.armor.values())
|
||||
|
||||
@property
|
||||
def total_armor_value(self) -> int:
|
||||
total = 0
|
||||
for unit_type, count in self.armor.items():
|
||||
try:
|
||||
total += PRICES[unit_type] * count
|
||||
except KeyError:
|
||||
logging.exception(f"No price found for {unit_type.id}")
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_frontline_aa(self) -> int:
|
||||
return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD])
|
||||
|
||||
@ -55,7 +55,7 @@ from .controlpoint import (
|
||||
Fob,
|
||||
)
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from ..utils import nm_to_meter
|
||||
from ..utils import Distance, meters, nautical_miles
|
||||
|
||||
Numeric = Union[int, float]
|
||||
|
||||
@ -115,7 +115,7 @@ class MizCampaignLoader:
|
||||
AirDefence.SAM_SA_3_S_125_LN_5P73.id,
|
||||
}
|
||||
|
||||
BASE_DEFENSE_RADIUS = nm_to_meter(2)
|
||||
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
self.theater = theater
|
||||
@ -317,9 +317,9 @@ class MizCampaignLoader:
|
||||
self.control_points[origin.id])
|
||||
return front_lines
|
||||
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, int]:
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(group.position)
|
||||
distance = closest.position.distance_to_point(group.position)
|
||||
distance = meters(closest.position.distance_to_point(group.position))
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
@ -447,11 +447,11 @@ class ConflictTheater:
|
||||
if self.is_on_land(point):
|
||||
return False
|
||||
|
||||
for exclusion_zone in self.landmap[1]:
|
||||
for exclusion_zone in self.landmap.exclusion_zones:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
for sea in self.landmap[2]:
|
||||
for sea in self.landmap.sea_zones:
|
||||
if poly_contains(point.x, point.y, sea):
|
||||
return True
|
||||
|
||||
@ -462,14 +462,13 @@ class ConflictTheater:
|
||||
return True
|
||||
|
||||
is_point_included = False
|
||||
for inclusion_zone in self.landmap[0]:
|
||||
if poly_contains(point.x, point.y, inclusion_zone):
|
||||
is_point_included = True
|
||||
if poly_contains(point.x, point.y, self.landmap.inclusion_zones):
|
||||
is_point_included = True
|
||||
|
||||
if not is_point_included:
|
||||
return False
|
||||
|
||||
for exclusion_zone in self.landmap[1]:
|
||||
for exclusion_zone in self.landmap.exclusion_zones:
|
||||
if poly_contains(point.x, point.y, exclusion_zone):
|
||||
return False
|
||||
|
||||
@ -484,7 +483,7 @@ class ConflictTheater:
|
||||
nearest_points = []
|
||||
if not self.landmap:
|
||||
raise RuntimeError("Landmap not initialized")
|
||||
for inclusion_zone in self.landmap[0]:
|
||||
for inclusion_zone in self.landmap.inclusion_zones:
|
||||
nearest_pair = ops.nearest_points(point, inclusion_zone)
|
||||
nearest_points.append(nearest_pair[1])
|
||||
min_distance = point.distance(nearest_points[0]) # type: geometry.Point
|
||||
@ -528,6 +527,26 @@ class ConflictTheater:
|
||||
closest = control_point
|
||||
closest_distance = distance
|
||||
return closest
|
||||
|
||||
def closest_target(self, point: Point) -> MissionTarget:
|
||||
closest: MissionTarget = self.controlpoints[0]
|
||||
closest_distance = point.distance_to_point(closest.position)
|
||||
for control_point in self.controlpoints[1:]:
|
||||
distance = point.distance_to_point(control_point.position)
|
||||
if distance < closest_distance:
|
||||
closest = control_point
|
||||
closest_distance = distance
|
||||
for tgo in control_point.ground_objects:
|
||||
distance = point.distance_to_point(tgo.position)
|
||||
if distance < closest_distance:
|
||||
closest = tgo
|
||||
closest_distance = distance
|
||||
for conflict in self.conflicts():
|
||||
distance = conflict.position.distance_to_point(point)
|
||||
if distance < closest_distance:
|
||||
closest = conflict
|
||||
closest_distance = distance
|
||||
return closest
|
||||
|
||||
def closest_opposing_control_points(self) -> Tuple[ControlPoint, ControlPoint]:
|
||||
"""
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
@ -7,7 +8,8 @@ import re
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
from functools import total_ordering
|
||||
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.ships import (
|
||||
@ -20,24 +22,27 @@ from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
from .base import Base
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import (
|
||||
BaseDefenseGroundObject,
|
||||
EwrGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
VehicleGroupGroundObject, GenericCarrierGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from ..db import PRICES
|
||||
from ..utils import nautical_miles
|
||||
from ..weather import Conditions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
from ..event import UnitsDeliveryEvent
|
||||
|
||||
|
||||
class ControlPointType(Enum):
|
||||
@ -191,6 +196,28 @@ class RunwayStatus:
|
||||
return f"Runway repairing, {turns_remaining} turns remaining"
|
||||
|
||||
|
||||
@total_ordering
|
||||
class GroundUnitDestination:
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.control_point = control_point
|
||||
|
||||
@property
|
||||
def total_value(self) -> float:
|
||||
return self.control_point.base.total_armor_value
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, GroundUnitDestination):
|
||||
raise TypeError
|
||||
|
||||
return self.total_value == other.total_value
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
if not isinstance(other, GroundUnitDestination):
|
||||
raise TypeError
|
||||
|
||||
return self.total_value < other.total_value
|
||||
|
||||
|
||||
class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
position = None # type: Point
|
||||
@ -208,7 +235,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
at: db.StartingPosition, size: int,
|
||||
importance: float, has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE):
|
||||
super().__init__(" ".join(re.split(r"[ \-]", name)[:2]), position)
|
||||
super().__init__(name, position)
|
||||
# TODO: Should be Airbase specific.
|
||||
self.id = cp_id
|
||||
self.full_name = name
|
||||
@ -229,7 +256,8 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.cptype = cptype
|
||||
# TODO: Should be Airbase specific.
|
||||
self.stances: Dict[int, CombatStance] = {}
|
||||
self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None
|
||||
from ..event import UnitsDeliveryEvent
|
||||
self.pending_unit_deliveries = UnitsDeliveryEvent(self)
|
||||
|
||||
self.target_position: Optional[Point] = None
|
||||
|
||||
@ -364,8 +392,90 @@ class ControlPoint(MissionTarget, ABC):
|
||||
base_defense.position)
|
||||
self.base_defenses = []
|
||||
|
||||
def capture_equipment(self, game: Game) -> None:
|
||||
total = self.base.total_armor_value
|
||||
self.base.armor.clear()
|
||||
game.adjust_budget(total, player=not self.captured)
|
||||
game.message(
|
||||
f"{self.name} is not connected to any friendly points. Ground "
|
||||
f"vehicles have been captured and sold for ${total}M.")
|
||||
|
||||
def retreat_ground_units(self, game: Game):
|
||||
# When there are multiple valid destinations, deliver units to whichever
|
||||
# base is least defended first. The closest approximation of unit
|
||||
# strength we have is price
|
||||
destinations = [GroundUnitDestination(cp)
|
||||
for cp in self.connected_points
|
||||
if cp.captured == self.captured]
|
||||
if not destinations:
|
||||
self.capture_equipment(game)
|
||||
return
|
||||
|
||||
heapq.heapify(destinations)
|
||||
destination = heapq.heappop(destinations)
|
||||
while self.base.armor:
|
||||
unit_type, count = self.base.armor.popitem()
|
||||
for _ in range(count):
|
||||
destination.control_point.base.commision_units({unit_type: 1})
|
||||
destination = heapq.heappushpop(destinations, destination)
|
||||
|
||||
def capture_aircraft(self, game: Game, airframe: Type[FlyingType],
|
||||
count: int) -> None:
|
||||
try:
|
||||
value = PRICES[airframe] * count
|
||||
except KeyError:
|
||||
logging.exception(f"Unknown price for {airframe.id}")
|
||||
return
|
||||
|
||||
game.adjust_budget(value, player=not self.captured)
|
||||
game.message(
|
||||
f"No valid retreat destination in range of {self.name} for "
|
||||
f"{airframe.id}. {count} aircraft have been captured and sold for "
|
||||
f"${value}M.")
|
||||
|
||||
def aircraft_retreat_destination(
|
||||
self, game: Game,
|
||||
airframe: Type[FlyingType]) -> Optional[ControlPoint]:
|
||||
closest = ObjectiveDistanceCache.get_closest_airfields(self)
|
||||
# TODO: Should be airframe dependent.
|
||||
max_retreat_distance = nautical_miles(200)
|
||||
# Skip the first airbase because that's the airbase we're retreating
|
||||
# from.
|
||||
airfields = list(closest.airfields_within(max_retreat_distance))[1:]
|
||||
for airbase in airfields:
|
||||
if not airbase.can_operate(airframe):
|
||||
continue
|
||||
if airbase.captured != self.captured:
|
||||
continue
|
||||
if airbase.unclaimed_parking(game) > 0:
|
||||
return airbase
|
||||
return None
|
||||
|
||||
def _retreat_air_units(self, game: Game, airframe: Type[FlyingType],
|
||||
count: int) -> None:
|
||||
while count:
|
||||
logging.debug(f"Retreating {count} {airframe.id} from {self.name}")
|
||||
destination = self.aircraft_retreat_destination(game, airframe)
|
||||
if destination is None:
|
||||
self.capture_aircraft(game, airframe, count)
|
||||
return
|
||||
parking = destination.unclaimed_parking(game)
|
||||
transfer_amount = min([parking, count])
|
||||
destination.base.commision_units({airframe: transfer_amount})
|
||||
count -= transfer_amount
|
||||
|
||||
def retreat_air_units(self, game: Game) -> None:
|
||||
# TODO: Capture in order of price to retain maximum value?
|
||||
while self.base.aircraft:
|
||||
airframe, count = self.base.aircraft.popitem()
|
||||
self._retreat_air_units(game, airframe, count)
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
self.pending_unit_deliveries.refund_all(game)
|
||||
self.retreat_ground_units(game)
|
||||
self.retreat_air_units(game)
|
||||
|
||||
if for_player:
|
||||
self.captured = True
|
||||
else:
|
||||
@ -373,9 +483,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
self.base.set_strength_to_minimum()
|
||||
|
||||
self.base.aircraft = {}
|
||||
self.base.armor = {}
|
||||
|
||||
self.clear_base_defenses()
|
||||
from .start_generator import BaseDefenseGenerator
|
||||
BaseDefenseGenerator(game, self).generate()
|
||||
@ -402,7 +509,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return total
|
||||
|
||||
def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
|
||||
assert self.pending_unit_deliveries
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
@ -439,7 +545,9 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return
|
||||
self.runway_status.begin_repair()
|
||||
|
||||
def process_turn(self) -> None:
|
||||
def process_turn(self, game: Game) -> None:
|
||||
self.pending_unit_deliveries.process(game)
|
||||
|
||||
runway_status = self.runway_status
|
||||
if runway_status is not None:
|
||||
runway_status.process_turn()
|
||||
@ -453,6 +561,8 @@ class ControlPoint(MissionTarget, ABC):
|
||||
# Move the linked unit groups
|
||||
for ground_object in self.ground_objects:
|
||||
if isinstance(ground_object, GenericCarrierGroundObject):
|
||||
ground_object.position.x = ground_object.position.x + delta.x
|
||||
ground_object.position.y = ground_object.position.y + delta.y
|
||||
for group in ground_object.groups:
|
||||
for u in group.units:
|
||||
u.position.x = u.position.x + delta.x
|
||||
@ -478,6 +588,24 @@ class ControlPoint(MissionTarget, ABC):
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def expected_ground_units_next_turn(self) -> PendingOccupancy:
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
continue
|
||||
if unit_bought in TYPE_SHORAD:
|
||||
continue
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
|
||||
return PendingOccupancy(self.base.total_armor, on_order,
|
||||
# Ground unit transfers not yet implemented.
|
||||
transferring=0)
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
|
||||
@ -542,6 +670,10 @@ class Airfield(ControlPoint):
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 20
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
|
||||
@ -741,3 +873,7 @@ class Fob(ControlPoint):
|
||||
@property
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 10
|
||||
|
||||
@ -1,11 +1,30 @@
|
||||
from dataclasses import dataclass
|
||||
import pickle
|
||||
from typing import Collection, Optional, Tuple
|
||||
from functools import cached_property
|
||||
from typing import Optional, Tuple, Union
|
||||
import logging
|
||||
|
||||
from shapely import geometry
|
||||
from shapely.geometry import MultiPolygon, Polygon
|
||||
|
||||
Zone = Collection[Tuple[float, float]]
|
||||
Landmap = Tuple[Collection[geometry.Polygon], Collection[geometry.Polygon], Collection[geometry.Polygon]]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Landmap:
|
||||
inclusion_zones: MultiPolygon
|
||||
exclusion_zones: MultiPolygon
|
||||
sea_zones: MultiPolygon
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.inclusion_zones.is_valid:
|
||||
raise RuntimeError("Inclusion zones not valid")
|
||||
if not self.exclusion_zones.is_valid:
|
||||
raise RuntimeError("Exclusion zones not valid")
|
||||
if not self.sea_zones.is_valid:
|
||||
raise RuntimeError("Sea zones not valid")
|
||||
|
||||
@cached_property
|
||||
def inclusion_zone_only(self) -> MultiPolygon:
|
||||
return self.inclusion_zones - self.exclusion_zones - self.sea_zones
|
||||
|
||||
|
||||
def load_landmap(filename: str) -> Optional[Landmap]:
|
||||
@ -17,7 +36,7 @@ def load_landmap(filename: str) -> Optional[Landmap]:
|
||||
return None
|
||||
|
||||
|
||||
def poly_contains(x, y, poly:geometry.Polygon):
|
||||
def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]):
|
||||
return poly.contains(geometry.Point(x, y))
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import pickle
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
@ -15,7 +14,6 @@ from dcs.vehicles import AirDefence
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import Carrier, Lha, LocationType
|
||||
from game.theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
@ -479,11 +477,11 @@ class BaseDefenseGenerator:
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
group = generate_anti_air_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
groups = generate_anti_air_group(self.game, g, self.faction)
|
||||
if not groups:
|
||||
logging.error(f"Could not generate SAM at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
g.groups = groups
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_shorad(self) -> None:
|
||||
@ -497,13 +495,13 @@ class BaseDefenseGenerator:
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=True)
|
||||
|
||||
group = generate_anti_air_group(self.game, g, self.faction,
|
||||
ranges=[{AirDefenseRange.Short}])
|
||||
if group is None:
|
||||
groups = generate_anti_air_group(self.game, g, self.faction,
|
||||
ranges=[{AirDefenseRange.Short}])
|
||||
if not groups:
|
||||
logging.error(
|
||||
f"Could not generate SHORAD group at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
g.groups = groups
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
|
||||
@ -642,12 +640,12 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
g = SamGroundObject(namegen.random_objective_name(), group_id,
|
||||
position, self.control_point, for_airbase=False)
|
||||
group = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||
if group is None:
|
||||
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||
if not groups:
|
||||
logging.error("Could not generate air defense group for %s at %s",
|
||||
g.name, self.control_point)
|
||||
return
|
||||
g.groups = [group]
|
||||
g.groups = groups
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Iterator, List, TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group
|
||||
|
||||
from .. import db
|
||||
from ..data.radar_db import UNITS_WITH_RADAR
|
||||
from ..utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .controlpoint import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
@ -85,7 +90,6 @@ class TheaterGroundObject(MissionTarget):
|
||||
self.dcs_identifier = dcs_identifier
|
||||
self.airbase_group = airbase_group
|
||||
self.sea_object = sea_object
|
||||
# TODO: There is never more than one group.
|
||||
self.groups: List[Group] = []
|
||||
|
||||
@property
|
||||
@ -147,6 +151,46 @@ class TheaterGroundObject(MissionTarget):
|
||||
def might_have_aa(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def has_radar(self) -> bool:
|
||||
"""Returns True if the ground object contains a unit with radar."""
|
||||
for group in self.groups:
|
||||
for unit in group.units:
|
||||
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
|
||||
if not self.might_have_aa:
|
||||
return meters(0)
|
||||
|
||||
max_range = meters(0)
|
||||
for u in group.units:
|
||||
unit = db.unit_type_from_name(u.type)
|
||||
if unit is None:
|
||||
logging.error(f"Unknown unit type {u.type}")
|
||||
continue
|
||||
|
||||
# Some units in pydcs have detection_range/threat_range defined,
|
||||
# but explicitly set to None.
|
||||
unit_range = getattr(unit, range_type, None)
|
||||
if unit_range is not None:
|
||||
max_range = max(max_range, meters(unit_range))
|
||||
return max_range
|
||||
|
||||
def detection_range(self, group: Group) -> Distance:
|
||||
return self._max_range_of_type(group, "detection_range")
|
||||
|
||||
def threat_range(self, group: Group) -> Distance:
|
||||
if not self.detection_range(group):
|
||||
# For simple SAMs like shilkas, the unit has both a threat and
|
||||
# detection range. For complex sites like SA-2s, the launcher has a
|
||||
# threat range and the search/track radars have detection ranges. If
|
||||
# the site has no detection range it has no radars and can't fire,
|
||||
# so it's not actually a threat even if it still has launchers.
|
||||
return meters(0)
|
||||
return self._max_range_of_type(group, "threat_range")
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
def __init__(self, name: str, category: str, group_id: int, object_id: int,
|
||||
|
||||
158
game/threatzones.py
Normal file
158
game/threatzones.py
Normal file
@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import singledispatchmethod
|
||||
from typing import Optional, TYPE_CHECKING, Union
|
||||
|
||||
from dcs.mapping import Point as DcsPoint
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
MultiPolygon,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
)
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
from shapely.ops import nearest_points, unary_union
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
ThreatPoly = Union[MultiPolygon, Polygon]
|
||||
|
||||
|
||||
class ThreatZones:
|
||||
def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None:
|
||||
self.airbases = airbases
|
||||
self.air_defenses = air_defenses
|
||||
self.all = unary_union([airbases, air_defenses])
|
||||
|
||||
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
|
||||
boundary, _ = nearest_points(self.all.boundary,
|
||||
self.dcs_to_shapely_point(point))
|
||||
return DcsPoint(boundary.x, boundary.y)
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened(self, position) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened.register
|
||||
def _threatened_geometry(self, position: BaseGeometry) -> bool:
|
||||
return self.all.intersects(position)
|
||||
|
||||
@threatened.register
|
||||
def _threatened_dcs_point(self, position: DcsPoint) -> bool:
|
||||
return self.all.intersects(self.dcs_to_shapely_point(position))
|
||||
|
||||
def path_threatened(self, a: DcsPoint, b: DcsPoint) -> bool:
|
||||
return self.threatened(LineString(
|
||||
[self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)]))
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_aircraft(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_aircraft.register
|
||||
def _threatened_by_aircraft_geom(self, position: BaseGeometry) -> bool:
|
||||
return self.airbases.intersects(position)
|
||||
|
||||
@threatened_by_aircraft.register
|
||||
def _threatened_by_aircraft_flight(self, flight: Flight) -> bool:
|
||||
return self.threatened_by_aircraft(LineString((
|
||||
self.dcs_to_shapely_point(p.position) for p in flight.points
|
||||
)))
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_air_defense(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
def _threatened_by_air_defense_geom(self, position: BaseGeometry) -> bool:
|
||||
return self.air_defenses.intersects(position)
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
def _threatened_by_air_defense_flight(self, flight: Flight) -> bool:
|
||||
return self.threatened_by_air_defense(LineString((
|
||||
self.dcs_to_shapely_point(p.position) for p in flight.points
|
||||
)))
|
||||
|
||||
@classmethod
|
||||
def closest_enemy_airbase(cls, location: ControlPoint,
|
||||
max_distance: Distance) -> Optional[ControlPoint]:
|
||||
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
for airfield in airfields.airfields_within(max_distance):
|
||||
if airfield.captured != location.captured:
|
||||
return airfield
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def barcap_threat_range(cls, game: Game,
|
||||
control_point: ControlPoint) -> Distance:
|
||||
doctrine = game.faction_for(control_point.captured).doctrine
|
||||
cap_threat_range = (doctrine.cap_max_distance_from_cp +
|
||||
doctrine.cap_engagement_range)
|
||||
opposing_airfield = cls.closest_enemy_airbase(control_point,
|
||||
cap_threat_range * 2)
|
||||
if opposing_airfield is None:
|
||||
return cap_threat_range
|
||||
|
||||
airfield_distance = meters(
|
||||
opposing_airfield.position.distance_to_point(control_point.position)
|
||||
)
|
||||
|
||||
# BARCAPs should not commit further than halfway to the closest enemy
|
||||
# airfield (with some breathing room) to avoid those missions becoming
|
||||
# offensive. For dissimilar doctrines we could weight this so that, as
|
||||
# an example, modern US goes no closer than 70% of the way to the WW2
|
||||
# German base, and the Germans go no closer than 30% of the way to the
|
||||
# US base, but for now equal weighting is fine.
|
||||
max_distance = airfield_distance * 0.45
|
||||
return min(cap_threat_range, max_distance)
|
||||
|
||||
@classmethod
|
||||
def for_faction(cls, game: Game, player: bool) -> ThreatZones:
|
||||
"""Generates the threat zones projected by the given coalition.
|
||||
|
||||
Args:
|
||||
game: The game to generate the threat zone for.
|
||||
player: True if the coalition projecting the threat zone belongs to
|
||||
the player.
|
||||
|
||||
Returns:
|
||||
The threat zones projected by the given coalition. If the threat
|
||||
zone belongs to the player, it is the zone that will be avoided by
|
||||
the enemy and vice versa.
|
||||
"""
|
||||
airbases = []
|
||||
air_defenses = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
if control_point.captured != player:
|
||||
continue
|
||||
if control_point.runway_is_operational():
|
||||
point = ShapelyPoint(control_point.position.x,
|
||||
control_point.position.y)
|
||||
cap_threat_range = cls.barcap_threat_range(game, control_point)
|
||||
airbases.append(point.buffer(cap_threat_range.meters))
|
||||
|
||||
for tgo in control_point.ground_objects:
|
||||
for group in tgo.groups:
|
||||
threat_range = tgo.threat_range(group)
|
||||
# Any system with a shorter range than this is not worth
|
||||
# even avoiding.
|
||||
if threat_range > nautical_miles(3):
|
||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||
threat_zone = point.buffer(threat_range.meters)
|
||||
air_defenses.append(threat_zone)
|
||||
|
||||
return cls(
|
||||
airbases=unary_union(airbases),
|
||||
air_defenses=unary_union(air_defenses)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def dcs_to_shapely_point(point: DcsPoint) -> ShapelyPoint:
|
||||
return ShapelyPoint(point.x, point.y)
|
||||
@ -112,7 +112,9 @@ class UnitMap:
|
||||
group: Group) -> None:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(group.name)
|
||||
# The name of the initiator in the DCS dead event will have " object"
|
||||
# appended for statics.
|
||||
name = f"{group.name} object"
|
||||
if name in self.buildings:
|
||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||
self.buildings[name] = Building(ground_object)
|
||||
|
||||
225
game/utils.py
225
game/utils.py
@ -1,65 +1,18 @@
|
||||
def meter_to_feet(value_in_meter: float) -> int:
|
||||
"""Converts meters to feets
|
||||
from __future__ import annotations
|
||||
|
||||
:arg value_in_meter Value in meters
|
||||
"""
|
||||
return int(3.28084 * value_in_meter)
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
|
||||
METERS_TO_FEET = 3.28084
|
||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||
NM_TO_METERS = 1852
|
||||
METERS_TO_NM = 1 / NM_TO_METERS
|
||||
|
||||
def feet_to_meter(value_in_feet: float) -> int:
|
||||
"""Converts feets to meters
|
||||
|
||||
:arg value_in_feet Value in feets
|
||||
"""
|
||||
return int(value_in_feet / 3.28084)
|
||||
|
||||
|
||||
def meter_to_nm(value_in_meter: float) -> int:
|
||||
"""Converts meters to nautic miles
|
||||
|
||||
:arg value_in_meter Value in meters
|
||||
"""
|
||||
return int(value_in_meter / 1852)
|
||||
|
||||
|
||||
def nm_to_meter(value_in_nm: float) -> int:
|
||||
"""Converts nautic miles to meters
|
||||
|
||||
:arg value_in_nm Value in nautic miles
|
||||
"""
|
||||
return int(value_in_nm * 1852)
|
||||
|
||||
|
||||
def knots_to_kph(value_in_knots: float) -> int:
|
||||
"""Converts Knots to Kilometer Per Hour
|
||||
|
||||
:arg value_in_knots Knots
|
||||
"""
|
||||
return int(value_in_knots * 1.852)
|
||||
|
||||
|
||||
def mps_to_knots(value_in_mps: float) -> int:
|
||||
"""Converts Meters Per Second To Knots
|
||||
|
||||
:arg value_in_mps Meters Per Second
|
||||
"""
|
||||
return int(value_in_mps * 1.943)
|
||||
|
||||
|
||||
def mps_to_kph(speed: float) -> int:
|
||||
"""Converts meters per second to kilometers per hour.
|
||||
|
||||
:arg speed Speed in m/s.
|
||||
"""
|
||||
return int(speed * 3.6)
|
||||
|
||||
|
||||
def kph_to_mps(speed: float) -> int:
|
||||
"""Converts kilometers per hour to meters per second.
|
||||
|
||||
:arg speed Speed in KPH.
|
||||
"""
|
||||
return int(speed / 3.6)
|
||||
KNOTS_TO_KPH = 1.852
|
||||
KPH_TO_KNOTS = 1 / KNOTS_TO_KPH
|
||||
MS_TO_KPH = 3.6
|
||||
KPH_TO_MS = 1 / MS_TO_KPH
|
||||
|
||||
|
||||
def heading_sum(h, a) -> int:
|
||||
@ -71,5 +24,157 @@ def heading_sum(h, a) -> int:
|
||||
else:
|
||||
return h
|
||||
|
||||
|
||||
def opposite_heading(h):
|
||||
return heading_sum(h, 180)
|
||||
return heading_sum(h, 180)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Distance:
|
||||
distance_in_meters: float
|
||||
|
||||
@property
|
||||
def feet(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_FEET
|
||||
|
||||
@property
|
||||
def meters(self) -> float:
|
||||
return self.distance_in_meters
|
||||
|
||||
@property
|
||||
def nautical_miles(self) -> float:
|
||||
return self.distance_in_meters * METERS_TO_NM
|
||||
|
||||
@classmethod
|
||||
def from_feet(cls, value: float) -> Distance:
|
||||
return cls(value * FEET_TO_METERS)
|
||||
|
||||
@classmethod
|
||||
def from_meters(cls, value: float) -> Distance:
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def from_nautical_miles(cls, value: float) -> Distance:
|
||||
return cls(value * NM_TO_METERS)
|
||||
|
||||
def __add__(self, other: Distance) -> Distance:
|
||||
return meters(self.meters + other.meters)
|
||||
|
||||
def __sub__(self, other: Distance) -> Distance:
|
||||
return meters(self.meters - other.meters)
|
||||
|
||||
def __mul__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters * other)
|
||||
|
||||
def __truediv__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters / other)
|
||||
|
||||
def __floordiv__(self, other: Union[float, int]) -> Distance:
|
||||
return meters(self.meters // other)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return not math.isclose(self.meters, 0.0)
|
||||
|
||||
|
||||
def feet(value: float) -> Distance:
|
||||
return Distance.from_feet(value)
|
||||
|
||||
|
||||
def meters(value: float) -> Distance:
|
||||
return Distance.from_meters(value)
|
||||
|
||||
|
||||
def nautical_miles(value: float) -> Distance:
|
||||
return Distance.from_nautical_miles(value)
|
||||
|
||||
|
||||
@dataclass(frozen=True, order=True)
|
||||
class Speed:
|
||||
speed_in_kph: float
|
||||
|
||||
@property
|
||||
def knots(self) -> float:
|
||||
return self.speed_in_kph * KPH_TO_KNOTS
|
||||
|
||||
@property
|
||||
def kph(self) -> float:
|
||||
return self.speed_in_kph
|
||||
|
||||
@property
|
||||
def meters_per_second(self) -> float:
|
||||
return self.speed_in_kph * KPH_TO_MS
|
||||
|
||||
def mach(self, altitude: Distance = meters(0)) -> float:
|
||||
c_sound = mach(1, altitude)
|
||||
return self.speed_in_kph / c_sound.kph
|
||||
|
||||
@classmethod
|
||||
def from_knots(cls, value: float) -> Speed:
|
||||
return cls(value * KNOTS_TO_KPH)
|
||||
|
||||
@classmethod
|
||||
def from_kph(cls, value: float) -> Speed:
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def from_meters_per_second(cls, value: float) -> Speed:
|
||||
return cls(value * MS_TO_KPH)
|
||||
|
||||
@classmethod
|
||||
def from_mach(cls, value: float, altitude: Distance) -> Speed:
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
if altitude <= feet(36152):
|
||||
temperature_f = 59 - 0.00356 * altitude.feet
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
temperature_f = -70
|
||||
|
||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||
|
||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||
# Dependent on temperature, but varies very little (+/-0.001)
|
||||
# between -40F and 180F.
|
||||
heat_capacity_ratio = 1.4
|
||||
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||
gas_constant = 286 # m^2/s^2/K
|
||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||
return mps(c_sound) * value
|
||||
|
||||
def __add__(self, other: Speed) -> Speed:
|
||||
return kph(self.kph + other.kph)
|
||||
|
||||
def __sub__(self, other: Speed) -> Speed:
|
||||
return kph(self.kph - other.kph)
|
||||
|
||||
def __mul__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph * other)
|
||||
|
||||
def __truediv__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph / other)
|
||||
|
||||
def __floordiv__(self, other: Union[float, int]) -> Speed:
|
||||
return kph(self.kph // other)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return not math.isclose(self.kph, 0.0)
|
||||
|
||||
|
||||
def knots(value: float) -> Speed:
|
||||
return Speed.from_knots(value)
|
||||
|
||||
|
||||
def kph(value: float) -> Speed:
|
||||
return Speed.from_kph(value)
|
||||
|
||||
|
||||
def mps(value: float) -> Speed:
|
||||
return Speed.from_meters_per_second(value)
|
||||
|
||||
|
||||
def mach(value: float, altitude: Distance) -> Speed:
|
||||
return Speed.from_mach(value, altitude)
|
||||
|
||||
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
||||
|
||||
@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["2.3.4"]
|
||||
components = ["2.4"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
|
||||
@ -10,6 +10,7 @@ from typing import Optional, TYPE_CHECKING
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
|
||||
from game.settings import Settings
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
@ -39,7 +40,7 @@ class Clouds:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Fog:
|
||||
visibility: int
|
||||
visibility: Distance
|
||||
thickness: int
|
||||
|
||||
|
||||
@ -56,7 +57,7 @@ class Weather:
|
||||
if random.randrange(5) != 0:
|
||||
return None
|
||||
return Fog(
|
||||
visibility=random.randint(2500, 5000),
|
||||
visibility=meters(random.randint(2500, 5000)),
|
||||
thickness=random.randint(100, 500)
|
||||
)
|
||||
|
||||
|
||||
137
gen/aircraft.py
137
gen/aircraft.py
@ -74,6 +74,7 @@ from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.data.weapons import Pylon, Weapon
|
||||
from game.factions.faction import Faction
|
||||
from game.settings import Settings
|
||||
from game.theater.controlpoint import (
|
||||
@ -85,7 +86,7 @@ from game.theater.controlpoint import (
|
||||
)
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import knots_to_kph, nm_to_meter
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen.airsupportgen import AirSupport
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.callsigns import create_group_callsign_from_unit
|
||||
@ -110,12 +111,10 @@ from .naming import namegen
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
WARM_START_HELI_AIRSPEED = 120
|
||||
WARM_START_HELI_ALT = 500
|
||||
WARM_START_ALTITUDE = 3000
|
||||
WARM_START_AIRSPEED = 550
|
||||
WARM_START_HELI_ALT = meters(500)
|
||||
WARM_START_ALTITUDE = meters(3000)
|
||||
|
||||
RTB_ALTITUDE = 800
|
||||
RTB_ALTITUDE = meters(800)
|
||||
RTB_DISTANCE = 5000
|
||||
HELI_ALT = 500
|
||||
|
||||
@ -263,6 +262,9 @@ class FlightData:
|
||||
#: The package that the flight belongs to.
|
||||
package: Package
|
||||
|
||||
#: The country that the flight belongs to.
|
||||
country: str
|
||||
|
||||
flight_type: FlightType
|
||||
|
||||
#: All units in the flight.
|
||||
@ -300,7 +302,7 @@ class FlightData:
|
||||
|
||||
joker_fuel: Optional[int]
|
||||
|
||||
def __init__(self, package: Package, flight_type: FlightType,
|
||||
def __init__(self, package: Package, country: str, flight_type: FlightType,
|
||||
units: List[FlyingUnit], size: int, friendly: bool,
|
||||
departure_delay: timedelta, departure: RunwayData,
|
||||
arrival: RunwayData, divert: Optional[RunwayData],
|
||||
@ -309,6 +311,7 @@ class FlightData:
|
||||
bingo_fuel: Optional[int],
|
||||
joker_fuel: Optional[int]) -> None:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.flight_type = flight_type
|
||||
self.units = units
|
||||
self.size = size
|
||||
@ -779,6 +782,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
self.flights.append(FlightData(
|
||||
package=package,
|
||||
country=faction.country,
|
||||
flight_type=flight.flight_type,
|
||||
units=group.units,
|
||||
size=len(group.units),
|
||||
@ -832,19 +836,21 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
alt = WARM_START_ALTITUDE
|
||||
|
||||
speed = knots_to_kph(GroundSpeed.for_flight(flight, alt))
|
||||
speed = GroundSpeed.for_flight(flight, alt)
|
||||
|
||||
pos = Point(at.x + random.randint(100, 1000), at.y + random.randint(100, 1000))
|
||||
|
||||
logging.info("airgen: {} for {} at {} at {}".format(flight.unit_type, side.id, alt, speed))
|
||||
logging.info(
|
||||
"airgen: {} for {} at {} at {}".format(flight.unit_type, side.id,
|
||||
alt, int(speed.kph)))
|
||||
group = self.m.flight_group(
|
||||
country=side,
|
||||
name=name,
|
||||
aircraft_type=flight.unit_type,
|
||||
airport=None,
|
||||
position=pos,
|
||||
altitude=alt,
|
||||
speed=speed,
|
||||
altitude=alt.meters,
|
||||
speed=speed.kph,
|
||||
maintask=None,
|
||||
group_size=flight.count)
|
||||
|
||||
@ -867,8 +873,10 @@ class AircraftConflictGenerator:
|
||||
start_type=self._start_type(start_type),
|
||||
group_size=count)
|
||||
|
||||
def _add_radio_waypoint(self, group: FlyingGroup, position, altitude: int, airspeed: int = 600):
|
||||
point = group.add_waypoint(position, altitude, airspeed)
|
||||
def _add_radio_waypoint(self, group: FlyingGroup, position,
|
||||
altitude: Distance,
|
||||
airspeed: int = 600) -> MovingPoint:
|
||||
point = group.add_waypoint(position, altitude.meters, airspeed)
|
||||
point.alt_type = "RADIO"
|
||||
return point
|
||||
|
||||
@ -884,7 +892,8 @@ class AircraftConflictGenerator:
|
||||
tod_location = position.point_from_heading(heading, RTB_DISTANCE)
|
||||
self._add_radio_waypoint(group, tod_location, last_waypoint.alt)
|
||||
|
||||
destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE)
|
||||
destination_waypoint = self._add_radio_waypoint(group, position,
|
||||
RTB_ALTITUDE)
|
||||
if isinstance(at, Airport):
|
||||
group.land_at(at)
|
||||
return destination_waypoint
|
||||
@ -899,22 +908,39 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
assert False
|
||||
|
||||
def _setup_custom_payload(self, flight, group:FlyingGroup):
|
||||
if flight.use_custom_loadout:
|
||||
@staticmethod
|
||||
def _setup_custom_payload(flight: Flight, group: FlyingGroup) -> None:
|
||||
if not flight.use_custom_loadout:
|
||||
return
|
||||
|
||||
logging.info("Custom loadout for flight : " + flight.__repr__())
|
||||
for p in group.units:
|
||||
p.pylons.clear()
|
||||
logging.info("Custom loadout for flight : " + flight.__repr__())
|
||||
for p in group.units:
|
||||
p.pylons.clear()
|
||||
|
||||
for key in flight.loadout.keys():
|
||||
if "Pylon" + key in flight.unit_type.__dict__.keys():
|
||||
print(flight.loadout)
|
||||
weapon_dict = flight.unit_type.__dict__["Pylon" + key].__dict__
|
||||
if flight.loadout[key] in weapon_dict.keys():
|
||||
weapon = weapon_dict[flight.loadout[key]]
|
||||
group.load_pylon(weapon, int(key))
|
||||
else:
|
||||
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
|
||||
for pylon_number, weapon in flight.loadout.items():
|
||||
if weapon is None:
|
||||
continue
|
||||
pylon = Pylon.for_aircraft(flight.unit_type, pylon_number)
|
||||
pylon.equip(group, weapon)
|
||||
|
||||
def _degrade_payload_to_era(self, flight: Flight,
|
||||
group: FlyingGroup) -> None:
|
||||
loadout = dict(group.units[0].pylons)
|
||||
for pylon_number, clsid in loadout.items():
|
||||
weapon = Weapon.from_clsid(clsid["CLSID"])
|
||||
if weapon is None:
|
||||
logging.error(f"Could not find weapon for clsid {clsid}")
|
||||
continue
|
||||
|
||||
if not weapon.available_on(self.game.date):
|
||||
pylon = Pylon.for_aircraft(flight.unit_type, pylon_number)
|
||||
for fallback in weapon.fallbacks:
|
||||
if not pylon.can_equip(fallback):
|
||||
continue
|
||||
if not fallback.available_on(self.game.date):
|
||||
continue
|
||||
pylon.equip(group, fallback)
|
||||
break
|
||||
|
||||
def clear_parking_slots(self) -> None:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
@ -963,13 +989,13 @@ class AircraftConflictGenerator:
|
||||
# Creating a flight even those this isn't a fragged mission lets us
|
||||
# reuse the existing debriefing code.
|
||||
# TODO: Special flight type?
|
||||
flight = Flight(Package(control_point), aircraft, 1,
|
||||
flight = Flight(Package(control_point), faction.country, aircraft, 1,
|
||||
FlightType.BARCAP, "Cold", departure=control_point,
|
||||
arrival=control_point, divert=None)
|
||||
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_unit_name(country, control_point.id,
|
||||
aircraft),
|
||||
name=namegen.next_aircraft_name(country, control_point.id,
|
||||
flight),
|
||||
side=country,
|
||||
unit_type=aircraft,
|
||||
count=1,
|
||||
@ -1033,17 +1059,18 @@ class AircraftConflictGenerator:
|
||||
CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
||||
|
||||
def generate_planned_flight(self, cp, country, flight:Flight):
|
||||
name = namegen.next_aircraft_name(country, cp.id, flight)
|
||||
try:
|
||||
if flight.start_type == "In Flight":
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
name=name,
|
||||
side=country,
|
||||
flight=flight,
|
||||
origin=cp)
|
||||
elif isinstance(cp, NavalControlPoint):
|
||||
group_name = cp.get_carrier_group_name()
|
||||
group = self._generate_at_group(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
name=name,
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
@ -1054,8 +1081,7 @@ class AircraftConflictGenerator:
|
||||
raise RuntimeError(
|
||||
f"Attempted to spawn at airfield for non-airfield {cp}")
|
||||
group = self._generate_at_airport(
|
||||
name=namegen.next_unit_name(country, cp.id,
|
||||
flight.unit_type),
|
||||
name=name,
|
||||
side=country,
|
||||
unit_type=flight.unit_type,
|
||||
count=flight.count,
|
||||
@ -1067,7 +1093,7 @@ class AircraftConflictGenerator:
|
||||
logging.warning("No room on runway or parking slots. Starting from the air.")
|
||||
flight.start_type = "In Flight"
|
||||
group = self._generate_inflight(
|
||||
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
|
||||
name=name,
|
||||
side=country,
|
||||
flight=flight,
|
||||
origin=cp)
|
||||
@ -1312,6 +1338,8 @@ class AircraftConflictGenerator:
|
||||
# have their TOTs set.
|
||||
self.flights[-1].waypoints = [takeoff_point] + flight.points
|
||||
self._setup_custom_payload(flight, group)
|
||||
if self.game.settings.restrict_weapons_by_date:
|
||||
self._degrade_payload_to_era(flight, group)
|
||||
|
||||
def should_delay_flight(self, flight: Flight,
|
||||
start_time: timedelta) -> bool:
|
||||
@ -1380,11 +1408,15 @@ class PydcsWaypointBuilder:
|
||||
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = self.group.add_waypoint(
|
||||
Point(self.waypoint.x, self.waypoint.y), self.waypoint.alt,
|
||||
Point(self.waypoint.x, self.waypoint.y),
|
||||
self.waypoint.alt.meters,
|
||||
name=self.mission.string(self.waypoint.name))
|
||||
|
||||
if self.waypoint.flyover:
|
||||
waypoint.type = PointAction.FlyOverPoint.value
|
||||
waypoint.action = PointAction.FlyOverPoint
|
||||
# It seems we need to leave waypoint.type exactly as it is even
|
||||
# though it's set to "Turning Point". If I set this to "Fly Over
|
||||
# Point" and then save the mission in the ME DCS resets it.
|
||||
|
||||
waypoint.alt_type = self.waypoint.alt_type
|
||||
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
|
||||
@ -1497,7 +1529,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||
waypoint.add_task(EngageTargetsInZone(
|
||||
position=self.flight.flight_plan.target,
|
||||
radius=FRONTLINE_LENGTH / 2,
|
||||
radius=int(self.flight.flight_plan.engagement_distance.meters),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.GroundVehicles,
|
||||
Targets.All.GroundUnits.AirDefence.AAA,
|
||||
@ -1508,7 +1540,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
logging.error(
|
||||
"No CAS waypoint found. Falling back to search and engage")
|
||||
waypoint.add_task(EngageTargets(
|
||||
max_distance=nm_to_meter(10),
|
||||
max_distance=int(nautical_miles(10).meters),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.GroundVehicles,
|
||||
Targets.All.GroundUnits.AirDefence.AAA,
|
||||
@ -1554,7 +1586,7 @@ class OcaAircraftIngressBuilder(PydcsWaypointBuilder):
|
||||
position=target.position,
|
||||
# Al Dhafra is 4 nm across at most. Add a little wiggle room in case
|
||||
# the airport position from DCS is not centered.
|
||||
radius=nm_to_meter(3),
|
||||
radius=int(nautical_miles(3).meters),
|
||||
targets=[Targets.All.Air]
|
||||
)
|
||||
task.params["attackQtyLimit"] = False
|
||||
@ -1591,7 +1623,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
if tgroup is not None:
|
||||
waypoint.add_task(EngageTargetsInZone(
|
||||
position=tgroup.position,
|
||||
radius=nm_to_meter(30),
|
||||
radius=int(nautical_miles(30).meters),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.AirDefence,
|
||||
])
|
||||
@ -1673,7 +1705,7 @@ class SweepIngressBuilder(PydcsWaypointBuilder):
|
||||
return waypoint
|
||||
|
||||
waypoint.tasks.append(EngageTargets(
|
||||
max_distance=nm_to_meter(50),
|
||||
max_distance=int(nautical_miles(50).meters),
|
||||
targets=[Targets.All.Air.Planes.Fighters]))
|
||||
|
||||
return waypoint
|
||||
@ -1716,7 +1748,7 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted
|
||||
waypoint.add_task(ControlledTask(EngageTargets(
|
||||
# TODO: From doctrine.
|
||||
max_distance=nm_to_meter(30),
|
||||
max_distance=int(nautical_miles(30).meters),
|
||||
targets=[Targets.All.Air.Planes.Fighters]
|
||||
)))
|
||||
|
||||
@ -1737,8 +1769,10 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||
flight_plan_type = self.flight.flight_plan.__class__.__name__
|
||||
flight_plan = self.flight.flight_plan
|
||||
|
||||
if not isinstance(flight_plan, PatrollingFlightPlan):
|
||||
flight_plan_type = flight_plan.__class__.__name__
|
||||
logging.error(
|
||||
f"Cannot create race track for {self.flight} because "
|
||||
f"{flight_plan_type} does not define a patrol.")
|
||||
@ -1756,17 +1790,18 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
# later.
|
||||
cap_types = {FlightType.BARCAP, FlightType.TARCAP}
|
||||
if self.flight.flight_type in cap_types:
|
||||
waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50),
|
||||
targets=[Targets.All.Air]))
|
||||
engagement_distance = int(flight_plan.engagement_distance.meters)
|
||||
waypoint.tasks.append(
|
||||
EngageTargets(max_distance=engagement_distance,
|
||||
targets=[Targets.All.Air]))
|
||||
|
||||
racetrack = ControlledTask(OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
))
|
||||
self.set_waypoint_tot(
|
||||
waypoint, self.flight.flight_plan.patrol_start_time)
|
||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||
racetrack.stop_after_time(
|
||||
int(self.flight.flight_plan.patrol_end_time.total_seconds()))
|
||||
int(flight_plan.patrol_end_time.total_seconds()))
|
||||
waypoint.add_task(racetrack)
|
||||
|
||||
return waypoint
|
||||
|
||||
@ -410,7 +410,10 @@ AIRFIELD_DATA = {
|
||||
icao="OMLW",
|
||||
elevation=400,
|
||||
runway_length=10768,
|
||||
atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(119, 300), MHz(250, 850)),
|
||||
tacan=TacanChannel(121, TacanBand.X),
|
||||
tacan_callsign="OMLW",
|
||||
vor=("OMLW", MHz(117,400)),
|
||||
atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(119, 300), MHz(250, 950)),
|
||||
),
|
||||
|
||||
"Al Dhafra AB": AirfieldData(
|
||||
|
||||
20
gen/armor.py
20
gen/armor.py
@ -208,7 +208,7 @@ class GroundConflictGenerator:
|
||||
u = random.choice(manpads)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
namegen.next_infantry_name(side, cp.id, u), u,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
@ -222,7 +222,7 @@ class GroundConflictGenerator:
|
||||
u = random.choice(possible_infantry_units)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
namegen.next_infantry_name(side, cp.id, u), u,
|
||||
position=infantry_position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
@ -233,7 +233,7 @@ class GroundConflictGenerator:
|
||||
position = infantry_position.random_point_within(55, 5)
|
||||
self.mission.vehicle_group(
|
||||
side,
|
||||
namegen.next_infantry_name(side, cp, u), u,
|
||||
namegen.next_infantry_name(side, cp.id, u), u,
|
||||
position=position,
|
||||
group_size=1,
|
||||
heading=forward_heading,
|
||||
@ -352,9 +352,14 @@ class GroundConflictGenerator:
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
# We use an offset heading here because DCS doesn't always
|
||||
# force vehicles to move if there's no heading change.
|
||||
offset_heading = forward_heading - 2
|
||||
if offset_heading < 0:
|
||||
offset_heading = 358
|
||||
attack_point = self.find_offensive_point(
|
||||
dcs_group,
|
||||
forward_heading,
|
||||
offset_heading,
|
||||
AGGRESIVE_MOVE_DISTANCE
|
||||
)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||
@ -367,7 +372,12 @@ class GroundConflictGenerator:
|
||||
to_cp.position.random_point_within(500, 0)
|
||||
)
|
||||
else:
|
||||
attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
|
||||
# We use an offset heading here because DCS doesn't always
|
||||
# force vehicles to move if there's no heading change.
|
||||
offset_heading = forward_heading - 1
|
||||
if offset_heading < 0:
|
||||
offset_heading = 359
|
||||
attack_point = self.find_offensive_point(dcs_group, offset_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
|
||||
dcs_group.add_waypoint(attack_point, PointAction.OffRoad)
|
||||
elif stance == CombatStance.ELIMINATION:
|
||||
# In elimination mode, the units focus on destroying as much enemy groups as possible
|
||||
|
||||
21
gen/ato.py
21
gen/ato.py
@ -17,8 +17,10 @@ from typing import Dict, List, Optional
|
||||
from dcs.mapping import Point
|
||||
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import Speed
|
||||
from .flights.flight import Flight, FlightType
|
||||
from .flights.flightplan import FormationFlightPlan
|
||||
from .flights.traveltime import TotEstimator
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -53,13 +55,18 @@ class Package:
|
||||
|
||||
delay: int = field(default=0)
|
||||
|
||||
#: True if the package ToT should be reset to ASAP whenever the player makes
|
||||
#: a change. This is really a UI property rather than a game property, but
|
||||
#: we want it to persist in the save.
|
||||
auto_asap: bool = field(default=False)
|
||||
|
||||
#: Desired TOT as an offset from mission start.
|
||||
time_over_target: timedelta = field(default=timedelta())
|
||||
|
||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||
|
||||
@property
|
||||
def formation_speed(self) -> Optional[int]:
|
||||
def formation_speed(self) -> Optional[Speed]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
If none of the flights in the package will join a formation, this
|
||||
@ -117,6 +124,18 @@ class Package:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> Optional[timedelta]:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
times.append(flight.flight_plan.mission_departure_time)
|
||||
if times:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
def set_tot_asap(self) -> None:
|
||||
self.time_over_target = TotEstimator(self).earliest_tot()
|
||||
|
||||
def add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
self.flights.append(flight)
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import logging
|
||||
import random
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from dcs.country import Country
|
||||
from dcs.mapping import Point
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint
|
||||
|
||||
from game.theater.conflicttheater import ConflictTheater, FrontLine
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
@ -56,7 +56,7 @@ class Conflict:
|
||||
"""
|
||||
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
|
||||
left_heading = heading_sum(heading, -90)
|
||||
right_heading = heading_sum(heading, 90)
|
||||
right_heading = heading_sum(heading, 90)
|
||||
left_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), left_heading, theater)
|
||||
right_position = cls.extend_ground_position(center_position, int(FRONTLINE_LENGTH / 2), right_heading, theater)
|
||||
distance = int(left_position.distance_to_point(right_position))
|
||||
@ -83,12 +83,25 @@ class Conflict:
|
||||
@classmethod
|
||||
def extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
|
||||
"""Finds the first intersection with an exclusion zone in one heading from an initial point up to max_distance"""
|
||||
pos = initial
|
||||
for distance in range(0, int(max_distance), 100):
|
||||
pos = initial.point_from_heading(heading, distance)
|
||||
if not theater.is_on_land(pos):
|
||||
return initial.point_from_heading(heading, distance - 100)
|
||||
return pos
|
||||
extended = initial.point_from_heading(heading, max_distance)
|
||||
if theater.landmap is None:
|
||||
# TODO: Why is this possible?
|
||||
return extended
|
||||
|
||||
p0 = ShapelyPoint(initial.x, initial.y)
|
||||
p1 = ShapelyPoint(extended.x, extended.y)
|
||||
line = LineString([p0, p1])
|
||||
|
||||
intersection = line.intersection(
|
||||
theater.landmap.inclusion_zone_only.boundary)
|
||||
if intersection.is_empty:
|
||||
# Max extent does not intersect with the boundary of the inclusion
|
||||
# zone, so the full front line is usable. This does assume that the
|
||||
# front line was centered on a valid location.
|
||||
return extended
|
||||
|
||||
# Otherwise extend the front line only up to the intersection.
|
||||
return initial.point_from_heading(heading, p0.distance(intersection))
|
||||
|
||||
@classmethod
|
||||
def find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater, coerce=True) -> Optional[Point]:
|
||||
|
||||
@ -21,7 +21,7 @@ class EnvironmentGenerator:
|
||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||
if fog is None:
|
||||
return
|
||||
self.mission.weather.fog_visibility = fog.visibility
|
||||
self.mission.weather.fog_visibility = fog.visibility.meters
|
||||
self.mission.weather.fog_thickness = fog.thickness
|
||||
|
||||
def set_wind(self, wind: WindConditions) -> None:
|
||||
|
||||
@ -8,7 +8,6 @@ from dcs.ships import (
|
||||
Type_052C_Destroyer,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
@ -27,12 +26,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
include_frigate = random.choice([True, True, False])
|
||||
include_dd = random.choice([True, False])
|
||||
|
||||
if include_dd:
|
||||
include_cc = random.choice([True, False])
|
||||
else:
|
||||
include_cc = False
|
||||
|
||||
if not any([include_frigate, include_dd, include_cc]):
|
||||
if not any([include_frigate, include_dd]):
|
||||
include_frigate = True
|
||||
|
||||
if include_frigate:
|
||||
@ -44,10 +38,6 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
self.add_unit(dd_type, "DD1", self.position.x + 2400, self.position.y + 900, self.heading)
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy])
|
||||
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ from dcs.ships import (
|
||||
FFG_11540_Neustrashimy,
|
||||
FF_1135M_Rezky,
|
||||
CG_1164_Moskva,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
SSK_877,
|
||||
SSK_641B
|
||||
)
|
||||
@ -49,8 +48,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
self.add_unit(dd_type, "DD2", self.position.x + 2400, self.position.y - 900, self.heading)
|
||||
|
||||
if include_cc:
|
||||
cc_type = random.choice([CG_1164_Moskva, CGN_1144_2_Pyotr_Velikiy])
|
||||
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
|
||||
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
|
||||
# See https://github.com/Khopa/dcs_liberation/issues/567
|
||||
self.add_unit(CG_1164_Moskva, "CC1", self.position.x, self.position.y, self.heading)
|
||||
|
||||
self.get_generated_group().points[0].speed = 20
|
||||
|
||||
|
||||
@ -3,9 +3,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
import operator
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from enum import Enum, auto
|
||||
from typing import (
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
@ -18,41 +21,28 @@ from typing import (
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from game.infos.information import Information
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
Fob,
|
||||
FrontLine,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
EwrGroundObject,
|
||||
NavalGroundObject, VehicleGroupGroundObject,
|
||||
NavalGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from game.utils import nm_to_meter
|
||||
from game.utils import Distance, nautical_miles
|
||||
from gen import Conflict
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import (
|
||||
ANTISHIP_CAPABLE,
|
||||
ANTISHIP_PREFERRED,
|
||||
CAP_CAPABLE,
|
||||
CAP_PREFERRED,
|
||||
CAS_CAPABLE,
|
||||
CAS_PREFERRED,
|
||||
RUNWAY_ATTACK_CAPABLE,
|
||||
RUNWAY_ATTACK_PREFERRED,
|
||||
SEAD_CAPABLE,
|
||||
SEAD_PREFERRED,
|
||||
STRIKE_CAPABLE,
|
||||
STRIKE_PREFERRED, capable_aircraft_for_task, preferred_aircraft_for_task,
|
||||
)
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import (
|
||||
ClosestAirfields,
|
||||
ObjectiveDistanceCache,
|
||||
@ -64,11 +54,17 @@ from gen.flights.flight import (
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
# Avoid importing some types that cause circular imports unless type checking.
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
|
||||
|
||||
class EscortType(Enum):
|
||||
AirToAir = auto()
|
||||
Sead = auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProposedFlight:
|
||||
"""A flight outline proposed by the mission planner.
|
||||
@ -85,7 +81,13 @@ class ProposedFlight:
|
||||
num_aircraft: int
|
||||
|
||||
#: The maximum distance between the objective and the departure airfield.
|
||||
max_distance: int
|
||||
max_distance: Distance
|
||||
|
||||
#: The type of threat this flight defends against if it is an escort. Escort
|
||||
#: flights will be pruned if the rest of the package is not threatened by
|
||||
#: the threat they defend against. If this flight is not an escort, this
|
||||
#: field is None.
|
||||
escort_type: Optional[EscortType] = field(default=None)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.task} {self.num_aircraft} ship"
|
||||
@ -123,7 +125,7 @@ class AircraftAllocator:
|
||||
|
||||
def find_aircraft_for_flight(
|
||||
self, flight: ProposedFlight
|
||||
) -> Optional[Tuple[ControlPoint, FlyingType]]:
|
||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||
"""Finds aircraft suitable for the given mission.
|
||||
|
||||
Searches for aircraft capable of performing the given mission within the
|
||||
@ -142,13 +144,8 @@ class AircraftAllocator:
|
||||
on subsequent calls. If the found aircraft are not used, the caller is
|
||||
responsible for returning them to the inventory.
|
||||
"""
|
||||
result = self.find_aircraft_of_type(
|
||||
flight, preferred_aircraft_for_task(flight.task)
|
||||
)
|
||||
if result is not None:
|
||||
return result
|
||||
return self.find_aircraft_of_type(
|
||||
flight, capable_aircraft_for_task(flight.task)
|
||||
flight, aircraft_for_task(flight.task)
|
||||
)
|
||||
|
||||
def find_aircraft_of_type(
|
||||
@ -178,9 +175,11 @@ class PackageBuilder:
|
||||
closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool,
|
||||
package_country: str,
|
||||
start_type: str) -> None:
|
||||
self.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
self.package_country = package_country
|
||||
self.package = Package(location)
|
||||
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
|
||||
is_player)
|
||||
@ -204,15 +203,15 @@ class PackageBuilder:
|
||||
else:
|
||||
start_type = self.start_type
|
||||
|
||||
flight = Flight(self.package, aircraft, plan.num_aircraft, plan.task,
|
||||
flight = Flight(self.package, self.package_country, aircraft, plan.num_aircraft, plan.task,
|
||||
start_type, departure=airfield, arrival=airfield,
|
||||
divert=self.find_divert_field(aircraft, airfield))
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
def find_divert_field(self, aircraft: FlyingType,
|
||||
def find_divert_field(self, aircraft: Type[FlyingType],
|
||||
arrival: ControlPoint) -> Optional[ControlPoint]:
|
||||
divert_limit = nm_to_meter(150)
|
||||
divert_limit = nautical_miles(150)
|
||||
for airfield in self.closest_airfields.airfields_within(divert_limit):
|
||||
if airfield.captured != self.is_player:
|
||||
continue
|
||||
@ -241,8 +240,8 @@ class ObjectiveFinder:
|
||||
"""Identifies potential objectives for the mission planner."""
|
||||
|
||||
# TODO: Merge into doctrine.
|
||||
AIRFIELD_THREAT_RANGE = nm_to_meter(150)
|
||||
SAM_THREAT_RANGE = nm_to_meter(100)
|
||||
AIRFIELD_THREAT_RANGE = nautical_miles(150)
|
||||
SAM_THREAT_RANGE = nautical_miles(100)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
@ -266,7 +265,7 @@ class ObjectiveFinder:
|
||||
if ground_object.name in found_targets:
|
||||
continue
|
||||
|
||||
if not self.object_has_radar(ground_object):
|
||||
if not ground_object.has_radar:
|
||||
continue
|
||||
|
||||
# TODO: Yield in order of most threatening.
|
||||
@ -349,12 +348,35 @@ class ObjectiveFinder:
|
||||
found_targets: Set[str] = set()
|
||||
for enemy_cp in self.enemy_control_points():
|
||||
for ground_object in enemy_cp.ground_objects:
|
||||
# TODO: Reuse ground_object.mission_types.
|
||||
# The mission types for ground objects are currently not
|
||||
# accurate because we include things like strike and BAI for all
|
||||
# targets since they have different planning behavior (waypoint
|
||||
# generation is better for players with strike when the targets
|
||||
# are stationary, AI behavior against weaker air defenses is
|
||||
# better with BAI), so that's not a useful filter. Once we have
|
||||
# better control over planning profiles and target dependent
|
||||
# loadouts we can clean this up.
|
||||
if isinstance(ground_object, VehicleGroupGroundObject):
|
||||
# BAI target, not strike target.
|
||||
continue
|
||||
|
||||
if isinstance(ground_object, NavalGroundObject):
|
||||
# Anti-ship target, not strike target.
|
||||
continue
|
||||
|
||||
if isinstance(ground_object, SamGroundObject):
|
||||
# SAMs are targeted by DEAD. No need to double plan.
|
||||
continue
|
||||
|
||||
is_building = isinstance(ground_object, BuildingGroundObject)
|
||||
is_fob = isinstance(enemy_cp, Fob)
|
||||
if is_building and is_fob and ground_object.airbase_group:
|
||||
# This is the FOB structure itself. Can't be repaired or
|
||||
# targeted by the player, so shouldn't be targetable by the
|
||||
# AI.
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
if ground_object.name in found_targets:
|
||||
@ -368,15 +390,6 @@ class ObjectiveFinder:
|
||||
for target, _range in targets:
|
||||
yield target
|
||||
|
||||
@staticmethod
|
||||
def object_has_radar(ground_object: TheaterGroundObject) -> bool:
|
||||
"""Returns True if the ground object contains a unit with radar."""
|
||||
for group in ground_object.groups:
|
||||
for unit in group.units:
|
||||
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
|
||||
return True
|
||||
return False
|
||||
|
||||
def front_lines(self) -> Iterator[FrontLine]:
|
||||
"""Iterates over all active front lines in the theater."""
|
||||
for cp in self.friendly_control_points():
|
||||
@ -467,25 +480,42 @@ class CoalitionMissionPlanner:
|
||||
"""
|
||||
|
||||
# TODO: Merge into doctrine, also limit by aircraft.
|
||||
MAX_CAP_RANGE = nm_to_meter(100)
|
||||
MAX_CAS_RANGE = nm_to_meter(50)
|
||||
MAX_ANTISHIP_RANGE = nm_to_meter(150)
|
||||
MAX_BAI_RANGE = nm_to_meter(150)
|
||||
MAX_OCA_RANGE = nm_to_meter(150)
|
||||
MAX_SEAD_RANGE = nm_to_meter(150)
|
||||
MAX_STRIKE_RANGE = nm_to_meter(150)
|
||||
MAX_CAP_RANGE = nautical_miles(100)
|
||||
MAX_CAS_RANGE = nautical_miles(50)
|
||||
MAX_ANTISHIP_RANGE = nautical_miles(150)
|
||||
MAX_BAI_RANGE = nautical_miles(150)
|
||||
MAX_OCA_RANGE = nautical_miles(150)
|
||||
MAX_SEAD_RANGE = nautical_miles(150)
|
||||
MAX_STRIKE_RANGE = nautical_miles(150)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
self.objective_finder = ObjectiveFinder(self.game, self.is_player)
|
||||
self.ato = self.game.blue_ato if is_player else self.game.red_ato
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
self.procurement_requests: List[AircraftProcurementRequest] = []
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
def critical_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies the most important missions to plan this turn.
|
||||
|
||||
Non-critical missions that cannot be fulfilled will create purchase
|
||||
orders for the next turn. Critical missions will create a purchase order
|
||||
unless the mission can be doubly fulfilled. In other words, the AI will
|
||||
attempt to have *double* the aircraft it needs for these missions to
|
||||
ensure that they can be planned again next turn even if all aircraft are
|
||||
eliminated this turn.
|
||||
"""
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
# Plan three rounds of CAP to give ~90 minutes coverage. Spacing
|
||||
# these out appropriately is done in stagger_missions.
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
yield ProposedMission(cp, [
|
||||
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
|
||||
])
|
||||
@ -493,10 +523,15 @@ class CoalitionMissionPlanner:
|
||||
# Find front lines, plan CAS.
|
||||
for front_line in self.objective_finder.front_lines():
|
||||
yield ProposedMission(front_line, [
|
||||
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
|
||||
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
|
||||
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE,
|
||||
EscortType.AirToAir),
|
||||
])
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
yield from self.critical_missions()
|
||||
|
||||
# Find enemy SAM sites with ranges that cover friendly CPs, front lines,
|
||||
# or objects, plan DEAD.
|
||||
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
||||
@ -505,39 +540,55 @@ class CoalitionMissionPlanner:
|
||||
yield ProposedMission(sam, [
|
||||
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE,
|
||||
EscortType.AirToAir),
|
||||
])
|
||||
|
||||
for group in self.objective_finder.threatening_ships():
|
||||
yield ProposedMission(group, [
|
||||
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_ANTISHIP_RANGE,
|
||||
EscortType.AirToAir),
|
||||
])
|
||||
|
||||
for group in self.objective_finder.threatening_vehicle_groups():
|
||||
yield ProposedMission(group, [
|
||||
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_BAI_RANGE,
|
||||
EscortType.AirToAir),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
|
||||
EscortType.Sead),
|
||||
])
|
||||
|
||||
for target in self.objective_finder.oca_targets(min_aircraft=20):
|
||||
yield ProposedMission(target, [
|
||||
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE),
|
||||
flights = [
|
||||
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE),
|
||||
]
|
||||
if self.game.settings.default_start_type == "Cold":
|
||||
# Only schedule if the default start type is Cold. If the player
|
||||
# has set anything else there are no targets to hit.
|
||||
flights.append(ProposedFlight(FlightType.OCA_AIRCRAFT, 2,
|
||||
self.MAX_OCA_RANGE))
|
||||
flights.extend([
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_OCA_RANGE,
|
||||
EscortType.AirToAir),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_OCA_RANGE,
|
||||
EscortType.Sead),
|
||||
])
|
||||
yield ProposedMission(target, flights)
|
||||
|
||||
# Plan strike missions.
|
||||
for target in self.objective_finder.strike_targets():
|
||||
yield ProposedMission(target, [
|
||||
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
|
||||
# TODO: Max escort range.
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE),
|
||||
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE,
|
||||
EscortType.AirToAir),
|
||||
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE,
|
||||
EscortType.Sead),
|
||||
])
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
@ -545,6 +596,9 @@ class CoalitionMissionPlanner:
|
||||
for proposed_mission in self.propose_missions():
|
||||
self.plan_mission(proposed_mission)
|
||||
|
||||
for critical_mission in self.critical_missions():
|
||||
self.plan_mission(critical_mission, reserves=True)
|
||||
|
||||
self.stagger_missions()
|
||||
|
||||
for cp in self.objective_finder.friendly_control_points():
|
||||
@ -553,48 +607,128 @@ class CoalitionMissionPlanner:
|
||||
self.message("Unused aircraft",
|
||||
f"{available} {aircraft.id} from {cp}")
|
||||
|
||||
def plan_mission(self, mission: ProposedMission) -> None:
|
||||
def plan_flight(self, mission: ProposedMission, flight: ProposedFlight,
|
||||
builder: PackageBuilder, missing_types: Set[FlightType],
|
||||
for_reserves: bool) -> None:
|
||||
if not builder.plan_flight(flight):
|
||||
missing_types.add(flight.task)
|
||||
purchase_order = AircraftProcurementRequest(
|
||||
near=mission.location,
|
||||
range=flight.max_distance,
|
||||
task_capability=flight.task,
|
||||
number=flight.num_aircraft
|
||||
)
|
||||
if for_reserves:
|
||||
# Reserves are planned for critical missions, so prioritize
|
||||
# those orders over aircraft needed for non-critical missions.
|
||||
self.procurement_requests.insert(0, purchase_order)
|
||||
else:
|
||||
self.procurement_requests.append(purchase_order)
|
||||
|
||||
def scrub_mission_missing_aircraft(
|
||||
self, mission: ProposedMission, builder: PackageBuilder,
|
||||
missing_types: Set[FlightType],
|
||||
not_attempted: Iterable[ProposedFlight],
|
||||
reserves: bool) -> None:
|
||||
# Try to plan the rest of the mission just so we can count the missing
|
||||
# types to buy.
|
||||
for flight in not_attempted:
|
||||
self.plan_flight(mission, flight, builder, missing_types, reserves)
|
||||
|
||||
missing_types_str = ", ".join(
|
||||
sorted([t.name for t in missing_types]))
|
||||
builder.release_planned_aircraft()
|
||||
desc = "reserve aircraft" if reserves else "aircraft"
|
||||
self.message(
|
||||
"Insufficient aircraft",
|
||||
f"Not enough {desc} in range for {mission.location.name} "
|
||||
f"capable of: {missing_types_str}")
|
||||
|
||||
def check_needed_escorts(
|
||||
self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
||||
threats = defaultdict(bool)
|
||||
for flight in builder.package.flights:
|
||||
if self.threat_zones.threatened_by_aircraft(flight):
|
||||
threats[EscortType.AirToAir] = True
|
||||
if self.threat_zones.threatened_by_air_defense(flight):
|
||||
threats[EscortType.Sead] = True
|
||||
return threats
|
||||
|
||||
def plan_mission(self, mission: ProposedMission,
|
||||
reserves: bool = False) -> None:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
|
||||
if self.game.settings.perf_ai_parking_start:
|
||||
start_type = "Cold"
|
||||
if self.is_player:
|
||||
package_country = self.game.player_country
|
||||
else:
|
||||
start_type = "Warm"
|
||||
package_country = self.game.enemy_country
|
||||
|
||||
builder = PackageBuilder(
|
||||
mission.location,
|
||||
self.objective_finder.closest_airfields_to(mission.location),
|
||||
self.game.aircraft_inventory,
|
||||
self.is_player,
|
||||
start_type
|
||||
package_country,
|
||||
self.game.settings.default_start_type
|
||||
)
|
||||
|
||||
# Attempt to plan all the main elements of the mission first. Escorts
|
||||
# will be planned separately so we can prune escorts for packages that
|
||||
# are not expected to encounter that type of threat.
|
||||
missing_types: Set[FlightType] = set()
|
||||
escorts = []
|
||||
for proposed_flight in mission.flights:
|
||||
if not builder.plan_flight(proposed_flight):
|
||||
missing_types.add(proposed_flight.task)
|
||||
self.procurement_requests.append(AircraftProcurementRequest(
|
||||
near=mission.location,
|
||||
range=proposed_flight.max_distance,
|
||||
task_capability=proposed_flight.task,
|
||||
number=proposed_flight.num_aircraft
|
||||
))
|
||||
if proposed_flight.escort_type is not None:
|
||||
# Escorts are planned after the primary elements of the package.
|
||||
# If the package does not need escorts they may be pruned.
|
||||
escorts.append(proposed_flight)
|
||||
continue
|
||||
self.plan_flight(mission, proposed_flight, builder, missing_types,
|
||||
reserves)
|
||||
|
||||
if missing_types:
|
||||
missing_types_str = ", ".join(
|
||||
sorted([t.name for t in missing_types]))
|
||||
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
|
||||
escorts, reserves)
|
||||
return
|
||||
|
||||
# Create flight plans for the main flights of the package so we can
|
||||
# determine threats. This is done *after* creating all of the flights
|
||||
# rather than as each flight is added because the flight plan for
|
||||
# flights that will rendezvous with their package will be affected by
|
||||
# the other flights in the package. Escorts will not be able to
|
||||
# contribute to this.
|
||||
flight_plan_builder = FlightPlanBuilder(self.game, builder.package,
|
||||
self.is_player)
|
||||
for flight in builder.package.flights:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
|
||||
needed_escorts = self.check_needed_escorts(builder)
|
||||
for escort in escorts:
|
||||
# This list was generated from the not None set, so this should be
|
||||
# impossible.
|
||||
assert escort.escort_type is not None
|
||||
if needed_escorts[escort.escort_type]:
|
||||
self.plan_flight(mission, escort, builder, missing_types,
|
||||
reserves)
|
||||
|
||||
# Check again for unavailable aircraft. If the escort was required and
|
||||
# none were found, scrub the mission.
|
||||
if missing_types:
|
||||
self.scrub_mission_missing_aircraft(mission, builder, missing_types,
|
||||
escorts, reserves)
|
||||
return
|
||||
|
||||
if reserves:
|
||||
# Mission is planned reserves which will not be used this turn.
|
||||
# Return reserves to the inventory.
|
||||
builder.release_planned_aircraft()
|
||||
self.message(
|
||||
"Insufficient aircraft",
|
||||
f"Not enough aircraft in range for {mission.location.name} "
|
||||
f"capable of: {missing_types_str}")
|
||||
return
|
||||
|
||||
package = builder.build()
|
||||
flight_plan_builder = FlightPlanBuilder(self.game, package,
|
||||
self.is_player)
|
||||
# Add flight plans for escorts.
|
||||
for flight in package.flights:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
if not flight.flight_plan.waypoints:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
self.ato.add_package(package)
|
||||
|
||||
def stagger_missions(self) -> None:
|
||||
@ -607,10 +741,12 @@ class CoalitionMissionPlanner:
|
||||
|
||||
dca_types = {
|
||||
FlightType.BARCAP,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.TARCAP,
|
||||
}
|
||||
|
||||
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(
|
||||
timedelta
|
||||
)
|
||||
non_dca_packages = [p for p in self.ato.packages if
|
||||
p.primary_task not in dca_types]
|
||||
|
||||
@ -623,8 +759,22 @@ class CoalitionMissionPlanner:
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
if package.primary_task in dca_types:
|
||||
# All CAP missions should be on station ASAP.
|
||||
package.time_over_target = tot
|
||||
previous_end_time = previous_cap_end_time[package.target]
|
||||
if tot > previous_end_time:
|
||||
# Can't get there exactly on time, so get there ASAP. This
|
||||
# will typically only happen for the first CAP at each
|
||||
# target.
|
||||
package.time_over_target = tot
|
||||
else:
|
||||
package.time_over_target = previous_end_time
|
||||
|
||||
departure_time = package.mission_departure_time
|
||||
# Should be impossible for CAPs
|
||||
if departure_time is None:
|
||||
logging.error(
|
||||
f"Could not determine mission end time for {package}")
|
||||
continue
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
|
||||
@ -13,6 +13,7 @@ from dcs.helicopters import (
|
||||
SA342L,
|
||||
SA342M,
|
||||
UH_1H,
|
||||
SH_60B
|
||||
)
|
||||
from dcs.planes import (
|
||||
AJS37,
|
||||
@ -63,6 +64,7 @@ from dcs.planes import (
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
RQ_1A_Predator,
|
||||
S_3B,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_17M4,
|
||||
@ -92,443 +94,278 @@ from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
|
||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
||||
# preferred over those lower in the list.
|
||||
|
||||
# TODO: These lists really ought to be era (faction) dependent.
|
||||
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
|
||||
# factions that also have F-4s should not.
|
||||
|
||||
# Interceptor are the aircraft prioritized for interception tasks
|
||||
# If none is available, the AI will use regular CAP-capable aircraft instead
|
||||
INTERCEPT_CAPABLE = [
|
||||
MiG_21Bis,
|
||||
MiG_25PD,
|
||||
MiG_31,
|
||||
MiG_29S,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
JF_17,
|
||||
J_11A,
|
||||
Su_27,
|
||||
Su_30,
|
||||
Su_33,
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
Rafale_M,
|
||||
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
]
|
||||
|
||||
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
|
||||
CAP_CAPABLE = [
|
||||
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_25PD,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
Su_57,
|
||||
F_22A,
|
||||
MiG_31,
|
||||
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
MiG_25PD,
|
||||
Rafale_M,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_27,
|
||||
J_11A,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_57,
|
||||
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_15E,
|
||||
F_16A,
|
||||
MiG_29S,
|
||||
MiG_29K,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
F_22A,
|
||||
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_4E,
|
||||
JF_17,
|
||||
MiG_23MLD,
|
||||
MiG_21Bis,
|
||||
Mirage_2000_5,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
MiG_19P,
|
||||
A_4E_C,
|
||||
F_86F_Sabre,
|
||||
MiG_15bis,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
|
||||
I_16,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_M,
|
||||
]
|
||||
|
||||
CAP_PREFERRED = [
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
MiG_23MLD,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29S,
|
||||
|
||||
Su_27,
|
||||
J_11A,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_33,
|
||||
Su_57,
|
||||
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15C,
|
||||
F_16C_50,
|
||||
F_22A,
|
||||
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
I_16,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
Rafale_M,
|
||||
]
|
||||
|
||||
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
|
||||
CAS_CAPABLE = [
|
||||
|
||||
MiG_15bis,
|
||||
MiG_29A,
|
||||
MiG_27K,
|
||||
MiG_29S,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_30,
|
||||
Su_34,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
|
||||
A_10C,
|
||||
B_1B,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
F_15E,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
F_15E,
|
||||
F_22A,
|
||||
|
||||
Tornado_IDS,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Tornado_GR4,
|
||||
|
||||
Tornado_IDS,
|
||||
JF_17,
|
||||
A_10A,
|
||||
A_4E_C,
|
||||
AJS37,
|
||||
Su_24MR,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
Su_34,
|
||||
Su_30,
|
||||
MiG_29S,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
AH_64D,
|
||||
AH_64A,
|
||||
AH_1W,
|
||||
OH_58D,
|
||||
SA342M,
|
||||
SA342L,
|
||||
Ka_50,
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Mi_8MT,
|
||||
UH_1H,
|
||||
MiG_15bis,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
OH_58D,
|
||||
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
AH_1W,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
|
||||
A_20G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
I_16,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator
|
||||
RQ_1A_Predator,
|
||||
]
|
||||
|
||||
CAS_PREFERRED = [
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_30,
|
||||
Su_34,
|
||||
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
Tornado_GR4,
|
||||
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
OH_58D,
|
||||
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
AH_1W,
|
||||
|
||||
Mi_28N,
|
||||
Mi_24V,
|
||||
Ka_50,
|
||||
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
I_16,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
|
||||
WingLoong_I,
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator
|
||||
]
|
||||
|
||||
# Aircraft used for SEAD / DEAD tasks
|
||||
# Aircraft used for SEAD tasks
|
||||
SEAD_CAPABLE = [
|
||||
F_4E,
|
||||
FA_18C_hornet,
|
||||
|
||||
F_16C_50,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
Su_24M,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_17M4,
|
||||
Su_30,
|
||||
Su_34,
|
||||
MiG_27K,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
]
|
||||
|
||||
SEAD_PREFERRED = [
|
||||
F_4E,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Tornado_IDS,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tornado_IDS,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
AV8BNA,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Su_34,
|
||||
Su_30,
|
||||
MiG_27K,
|
||||
Tornado_GR4,
|
||||
F_117A,
|
||||
B_17G,
|
||||
A_20G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for DEAD tasks
|
||||
DEAD_CAPABLE = [
|
||||
AJS37,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
B_1B,
|
||||
B_52H,
|
||||
Tu_160,
|
||||
Tu_95MS,
|
||||
] + SEAD_CAPABLE
|
||||
|
||||
|
||||
# Aircraft used for Strike mission
|
||||
STRIKE_CAPABLE = [
|
||||
MiG_15bis,
|
||||
MiG_21Bis,
|
||||
MiG_27K,
|
||||
MB_339PAN,
|
||||
|
||||
Su_17M4,
|
||||
Su_24M,
|
||||
Su_24MR,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
Su_25TM,
|
||||
Su_27,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_34,
|
||||
MiG_29A,
|
||||
MiG_29G,
|
||||
MiG_29K,
|
||||
MiG_29S,
|
||||
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
|
||||
JF_17,
|
||||
|
||||
M_2000C,
|
||||
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
|
||||
F_86F_Sabre,
|
||||
F_5E_3,
|
||||
|
||||
F_14A_135_GR,
|
||||
F_14B,
|
||||
F_15E,
|
||||
F_16A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
|
||||
F_117A,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
|
||||
Tornado_IDS,
|
||||
Tu_160,
|
||||
Tu_95MS,
|
||||
Tu_22M3,
|
||||
F_15E,
|
||||
AJS37,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Tornado_GR4,
|
||||
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
Tornado_IDS,
|
||||
Su_17M4,
|
||||
Su_24MR,
|
||||
Su_24M,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
Su_34,
|
||||
Su_33,
|
||||
Su_30,
|
||||
Su_27,
|
||||
MiG_29S,
|
||||
MiG_29K,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
JF_17,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
M_2000C,
|
||||
MiG_27K,
|
||||
MiG_21Bis,
|
||||
MiG_15bis,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
MB_339PAN,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
AJS37,
|
||||
|
||||
B_17G,
|
||||
A_20G,
|
||||
P_47D_40,
|
||||
P_47D_30bl1,
|
||||
P_47D_30,
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
A_20G,
|
||||
B_17G,
|
||||
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
|
||||
A_4E_C,
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
|
||||
]
|
||||
|
||||
STRIKE_PREFERRED = [
|
||||
AJS37,
|
||||
A_20G,
|
||||
B_17G,
|
||||
B_1B,
|
||||
B_52H,
|
||||
F_117A,
|
||||
F_15E,
|
||||
Su_24M,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
Tu_160,
|
||||
Tu_22M3,
|
||||
Tu_95MS,
|
||||
]
|
||||
|
||||
ANTISHIP_CAPABLE = [
|
||||
AJS37,
|
||||
C_101CC,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
FA_18C_hornet,
|
||||
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tu_22M3,
|
||||
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
|
||||
Ju_88A4,
|
||||
Rafale_A_S,
|
||||
Rafale_B
|
||||
]
|
||||
|
||||
ANTISHIP_PREFERRED = [
|
||||
AJS37,
|
||||
C_101CC,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
Rafale_A_S,
|
||||
Rafale_B,
|
||||
Su_24M,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Tu_22M3,
|
||||
Ju_88A4
|
||||
]
|
||||
|
||||
RUNWAY_ATTACK_PREFERRED = [
|
||||
Su_17M4,
|
||||
JF_17,
|
||||
Su_30,
|
||||
Su_34,
|
||||
Su_30,
|
||||
Tornado_IDS,
|
||||
Tornado_GR4,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
C_101CC,
|
||||
SH_60B,
|
||||
]
|
||||
|
||||
RUNWAY_ATTACK_CAPABLE = STRIKE_CAPABLE
|
||||
|
||||
# Duplicates some list entries but that's fine.
|
||||
RUNWAY_ATTACK_CAPABLE = [
|
||||
JF_17,
|
||||
Su_34,
|
||||
Su_30,
|
||||
Tornado_IDS,
|
||||
] + STRIKE_CAPABLE
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
# missions in a direct combat sense, but can transport objects and infantry.
|
||||
TRANSPORT_CAPABLE = [
|
||||
Hercules,
|
||||
Mi_8MT,
|
||||
UH_1H,
|
||||
]
|
||||
|
||||
DRONES = [
|
||||
MQ_9_Reaper,
|
||||
@ -537,31 +374,7 @@ DRONES = [
|
||||
]
|
||||
|
||||
|
||||
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_PREFERRED
|
||||
elif task == FlightType.ANTISHIP:
|
||||
return ANTISHIP_PREFERRED
|
||||
elif task == FlightType.BAI:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_PREFERRED
|
||||
elif task in (FlightType.DEAD, FlightType.SEAD):
|
||||
return SEAD_PREFERRED
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
return CAS_PREFERRED
|
||||
elif task == FlightType.OCA_RUNWAY:
|
||||
return RUNWAY_ATTACK_PREFERRED
|
||||
elif task == FlightType.STRIKE:
|
||||
return STRIKE_PREFERRED
|
||||
elif task == FlightType.ESCORT:
|
||||
return CAP_PREFERRED
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
@ -571,8 +384,10 @@ def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.CAS:
|
||||
return CAS_CAPABLE
|
||||
elif task in (FlightType.DEAD, FlightType.SEAD):
|
||||
elif task == FlightType.SEAD:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.DEAD:
|
||||
return DEAD_CAPABLE
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.OCA_RUNWAY:
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
"""Objective adjacency lists."""
|
||||
from typing import Dict, Iterator, List, Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from game.theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
from typing import Dict, Iterator, List, Optional, TYPE_CHECKING
|
||||
|
||||
from game.utils import Distance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater, ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class ClosestAirfields:
|
||||
@ -10,18 +15,25 @@ class ClosestAirfields:
|
||||
def __init__(self, target: MissionTarget,
|
||||
all_control_points: List[ControlPoint]) -> None:
|
||||
self.target = target
|
||||
# This cache is configured once on load, so it's important that it is
|
||||
# complete and deterministic to avoid different behaviors across loads.
|
||||
# E.g. https://github.com/Khopa/dcs_liberation/issues/819
|
||||
self.closest_airfields: List[ControlPoint] = sorted(
|
||||
all_control_points, key=lambda c: self.target.distance_to(c)
|
||||
)
|
||||
|
||||
def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
|
||||
@property
|
||||
def operational_airfields(self) -> Iterator[ControlPoint]:
|
||||
return (c for c in self.closest_airfields if c.runway_is_operational())
|
||||
|
||||
def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
for cp in self.closest_airfields:
|
||||
if cp.distance_to(self.target) < meters:
|
||||
if cp.distance_to(self.target) < distance.meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type
|
||||
@ -9,7 +10,9 @@ from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.data.weapons import Weapon
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.ato import Package
|
||||
@ -67,7 +70,7 @@ class FlightWaypointType(Enum):
|
||||
class FlightWaypoint:
|
||||
|
||||
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
|
||||
alt: int = 0) -> None:
|
||||
alt: Distance = meters(0)) -> None:
|
||||
"""Creates a flight waypoint.
|
||||
|
||||
Args:
|
||||
@ -83,6 +86,9 @@ class FlightWaypoint:
|
||||
self.alt = alt
|
||||
self.alt_type = "BARO"
|
||||
self.name = ""
|
||||
# TODO: Merge with pretty_name.
|
||||
# Only used in the waypoint list in the flight edit page. No sense
|
||||
# having three names. A short and long form is enough.
|
||||
self.description = ""
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.obj_name = ""
|
||||
@ -105,7 +111,7 @@ class FlightWaypoint:
|
||||
def from_pydcs(cls, point: MovingPoint,
|
||||
from_cp: ControlPoint) -> "FlightWaypoint":
|
||||
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
|
||||
point.position.y, point.alt)
|
||||
point.position.y, meters(point.alt))
|
||||
waypoint.alt_type = point.alt_type
|
||||
# Other actions exist... but none of them *should* be the first
|
||||
# waypoint for a flight.
|
||||
@ -130,11 +136,13 @@ class FlightWaypoint:
|
||||
|
||||
class Flight:
|
||||
|
||||
def __init__(self, package: Package, unit_type: Type[FlyingType],
|
||||
def __init__(self, package: Package, country: str, unit_type: Type[FlyingType],
|
||||
count: int, flight_type: FlightType, start_type: str,
|
||||
departure: ControlPoint, arrival: ControlPoint,
|
||||
divert: Optional[ControlPoint]) -> None:
|
||||
divert: Optional[ControlPoint],
|
||||
custom_name: Optional[str] = None) -> None:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.departure = departure
|
||||
@ -143,10 +151,11 @@ class Flight:
|
||||
self.flight_type = flight_type
|
||||
# TODO: Replace with FlightPlan.
|
||||
self.targets: List[MissionTarget] = []
|
||||
self.loadout: Dict[str, str] = {}
|
||||
self.loadout: Dict[int, Optional[Weapon]] = {}
|
||||
self.start_type = start_type
|
||||
self.use_custom_loadout = False
|
||||
self.client_count = 0
|
||||
self.custom_name = custom_name
|
||||
|
||||
# Will be replaced with a more appropriate FlightPlan by
|
||||
# FlightPlanBuilder, but an empty flight plan the flight begins with an
|
||||
@ -168,4 +177,12 @@ class Flight:
|
||||
|
||||
def __repr__(self):
|
||||
name = db.unit_type_name(self.unit_type)
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {name}"
|
||||
return f"[{self.flight_type}] {self.count} x {name}"
|
||||
|
||||
def __str__(self):
|
||||
name = db.unit_get_expanded_info(self.country, self.unit_type, 'name')
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {name}"
|
||||
return f"[{self.flight_type}] {self.count} x {name}"
|
||||
|
||||
@ -17,6 +17,7 @@ from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.theater import (
|
||||
@ -28,12 +29,12 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.theater.theatergroundobject import EwrGroundObject
|
||||
from game.utils import nm_to_meter, meter_to_nm
|
||||
from game.utils import Distance, Speed, meters, nautical_miles
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
from .traveltime import GroundSpeed, TravelTime
|
||||
from .waypointbuilder import StrikeTarget, WaypointBuilder
|
||||
from ..conflictgen import Conflict
|
||||
from ..conflictgen import Conflict, FRONTLINE_LENGTH
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -86,7 +87,7 @@ class FlightPlan:
|
||||
return zip(self.waypoints[:last_index], self.waypoints[1:last_index])
|
||||
|
||||
def best_speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> int:
|
||||
b: FlightWaypoint) -> Speed:
|
||||
"""Desired ground speed between points a and b."""
|
||||
factor = 1.0
|
||||
if b.waypoint_type == FlightWaypointType.ASCEND_POINT:
|
||||
@ -105,11 +106,10 @@ class FlightPlan:
|
||||
# We don't have an exact heightmap, but we should probably be performing
|
||||
# *some* adjustment for NTTR since the minimum altitude of the map is
|
||||
# near 2000 ft MSL.
|
||||
return int(
|
||||
GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor)
|
||||
return GroundSpeed.for_flight(self.flight, min(a.alt, b.alt)) * factor
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> int:
|
||||
b: FlightWaypoint) -> Speed:
|
||||
return self.best_speed_between_waypoints(a, b)
|
||||
|
||||
@property
|
||||
@ -126,16 +126,17 @@ class FlightPlan:
|
||||
def bingo_fuel(self) -> int:
|
||||
"""Bingo fuel value for the FlightPlan
|
||||
"""
|
||||
distance_to_arrival = meter_to_nm(self.max_distance_from(self.flight.arrival))
|
||||
distance_to_arrival = self.max_distance_from(self.flight.arrival)
|
||||
|
||||
bingo = 1000 # Minimum Emergency Fuel
|
||||
bingo += 500 # Visual Traffic
|
||||
bingo += 15 * distance_to_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:
|
||||
bingo += 10 * meter_to_nm(self.max_distance_from(self.flight.divert))
|
||||
max_divert_distance = self.max_distance_from(self.flight.divert)
|
||||
bingo += 10 * max_divert_distance.nautical_miles
|
||||
|
||||
return round(bingo / 100) * 100
|
||||
|
||||
@ -145,13 +146,14 @@ class FlightPlan:
|
||||
"""
|
||||
return self.bingo_fuel + 1000
|
||||
|
||||
def max_distance_from(self, cp: ControlPoint) -> int:
|
||||
def max_distance_from(self, cp: ControlPoint) -> Distance:
|
||||
"""Returns the farthest waypoint of the flight plan from a ControlPoint.
|
||||
:arg cp The ControlPoint to measure distance from.
|
||||
"""
|
||||
if not self.waypoints:
|
||||
return 0
|
||||
return max([cp.position.distance_to_point(w.position) for w in self.waypoints])
|
||||
return meters(0)
|
||||
return max([meters(cp.position.distance_to_point(w.position)) for w in
|
||||
self.waypoints])
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
@ -242,6 +244,11 @@ class FlightPlan:
|
||||
else:
|
||||
return timedelta(minutes=5)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoiterFlightPlan(FlightPlan):
|
||||
@ -303,7 +310,7 @@ class FormationFlightPlan(LoiterFlightPlan):
|
||||
return self.split
|
||||
|
||||
@cached_property
|
||||
def best_flight_formation_speed(self) -> int:
|
||||
def best_flight_formation_speed(self) -> Speed:
|
||||
"""The best speed this flight is capable at all formation waypoints.
|
||||
|
||||
To ease coordination with other flights, we aim to have a single mission
|
||||
@ -319,7 +326,7 @@ class FormationFlightPlan(LoiterFlightPlan):
|
||||
return min(speeds)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> int:
|
||||
b: FlightWaypoint) -> Speed:
|
||||
if b in self.package_speed_waypoints:
|
||||
# Should be impossible, as any package with at least one
|
||||
# FormationFlightPlan flight needs a formation speed.
|
||||
@ -355,15 +362,27 @@ class FormationFlightPlan(LoiterFlightPlan):
|
||||
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
||||
)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.split_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatrollingFlightPlan(FlightPlan):
|
||||
nav_to: List[FlightWaypoint]
|
||||
nav_from: List[FlightWaypoint]
|
||||
patrol_start: FlightWaypoint
|
||||
patrol_end: FlightWaypoint
|
||||
|
||||
#: Maximum time to remain on station.
|
||||
patrol_duration: timedelta
|
||||
|
||||
#: The engagement range of any Search Then Engage task, or the radius of a
|
||||
#: Search Then Engage in Zone task. Any enemies of the appropriate type for
|
||||
#: this mission within this range of the flight's current position (or the
|
||||
#: center of the zone) will be engaged by the flight.
|
||||
engagement_distance: Distance
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
@ -397,6 +416,10 @@ class PatrollingFlightPlan(FlightPlan):
|
||||
def tot_waypoint(self) -> Optional[FlightWaypoint]:
|
||||
return self.patrol_start
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.patrol_end_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BarCapFlightPlan(PatrollingFlightPlan):
|
||||
@ -405,12 +428,14 @@ class BarCapFlightPlan(PatrollingFlightPlan):
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.patrol_end,
|
||||
self.land,
|
||||
]
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@ -423,13 +448,15 @@ class CasFlightPlan(PatrollingFlightPlan):
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.target,
|
||||
self.patrol_end,
|
||||
self.land,
|
||||
]
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@ -448,12 +475,14 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
lead_time: timedelta
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.patrol_start,
|
||||
self.patrol_end,
|
||||
self.land,
|
||||
]
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@ -486,27 +515,27 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
class StrikeFlightPlan(FormationFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
hold: FlightWaypoint
|
||||
nav_to: List[FlightWaypoint]
|
||||
join: FlightWaypoint
|
||||
ingress: FlightWaypoint
|
||||
targets: List[FlightWaypoint]
|
||||
egress: FlightWaypoint
|
||||
split: FlightWaypoint
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.hold,
|
||||
self.join,
|
||||
self.ingress
|
||||
]
|
||||
yield self.takeoff
|
||||
yield self.hold
|
||||
yield from self.nav_to
|
||||
yield self.join
|
||||
yield self.ingress
|
||||
yield from self.targets
|
||||
yield from [
|
||||
self.egress,
|
||||
self.split,
|
||||
self.land,
|
||||
]
|
||||
yield self.egress
|
||||
yield self.split
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@ -519,7 +548,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
} | set(self.targets)
|
||||
|
||||
def speed_between_waypoints(self, a: FlightWaypoint,
|
||||
b: FlightWaypoint) -> int:
|
||||
b: FlightWaypoint) -> Speed:
|
||||
# FlightWaypoint is only comparable by identity, so adding
|
||||
# target_area_waypoint to package_speed_waypoints is useless.
|
||||
if b.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||
@ -537,7 +566,8 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(FlightWaypointType.TARGET_GROUP_LOC,
|
||||
self.package.target.position.x,
|
||||
self.package.target.position.y, 0)
|
||||
self.package.target.position.y,
|
||||
meters(0))
|
||||
|
||||
@property
|
||||
def travel_time_to_target(self) -> timedelta:
|
||||
@ -562,7 +592,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
return total
|
||||
|
||||
@property
|
||||
def mission_speed(self) -> int:
|
||||
def mission_speed(self) -> Speed:
|
||||
return GroundSpeed.for_flight(self.flight, self.ingress.alt)
|
||||
|
||||
@property
|
||||
@ -604,20 +634,22 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
@dataclass(frozen=True)
|
||||
class SweepFlightPlan(LoiterFlightPlan):
|
||||
takeoff: FlightWaypoint
|
||||
nav_to: List[FlightWaypoint]
|
||||
sweep_start: FlightWaypoint
|
||||
sweep_end: FlightWaypoint
|
||||
nav_from: List[FlightWaypoint]
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
lead_time: timedelta
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield from [
|
||||
self.takeoff,
|
||||
self.hold,
|
||||
self.sweep_start,
|
||||
self.sweep_end,
|
||||
self.land,
|
||||
]
|
||||
yield self.takeoff
|
||||
yield self.hold
|
||||
yield from self.nav_to
|
||||
yield self.sweep_start
|
||||
yield self.sweep_end
|
||||
yield from self.nav_from
|
||||
yield self.land
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
|
||||
@ -660,6 +692,9 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
GroundSpeed.for_flight(self.flight, self.hold.alt)
|
||||
)
|
||||
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.sweep_end_time
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CustomFlightPlan(FlightPlan):
|
||||
@ -690,6 +725,10 @@ class CustomFlightPlan(FlightPlan):
|
||||
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
class FlightPlanBuilder:
|
||||
"""Generates flight plans for flights."""
|
||||
@ -710,6 +749,7 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
faction = self.game.enemy_faction
|
||||
self.doctrine: Doctrine = faction.doctrine
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def populate_flight_plan(
|
||||
self, flight: Flight,
|
||||
@ -755,12 +795,79 @@ class FlightPlanBuilder:
|
||||
f"{task} flight plan generation not implemented")
|
||||
|
||||
def regenerate_package_waypoints(self) -> None:
|
||||
ingress_point = self._ingress_point()
|
||||
egress_point = self._egress_point()
|
||||
# The simple case is where the target is greater than the ingress
|
||||
# distance into the threat zone and the target is not near the departure
|
||||
# airfield. In this case, we can plan the shortest route from the
|
||||
# departure airfield to the target, use the last non-threatened point as
|
||||
# the join point, and plan the IP inside the threatened area.
|
||||
#
|
||||
# When the target is near the edge of the threat zone the IP may need to
|
||||
# be placed outside the zone.
|
||||
#
|
||||
# +--------------+ +---------------+
|
||||
# | | | |
|
||||
# | | IP---+-T |
|
||||
# | | | |
|
||||
# | | | |
|
||||
# +--------------+ +---------------+
|
||||
#
|
||||
# Here we want to place the IP first and route the flight to the IP
|
||||
# rather than routing to the target and placing the IP based on the join
|
||||
# point.
|
||||
#
|
||||
# The other case that we need to handle is when the target is close to
|
||||
# the origin airfield. In this case we also need to set up the IP first,
|
||||
# but depending on the placement of the IP we may need to place the join
|
||||
# point in a retreating position.
|
||||
#
|
||||
# A messy (and very unlikely) case that we can't do much about:
|
||||
#
|
||||
# +--------------+ +---------------+
|
||||
# | | | |
|
||||
# | IP-+---+-T |
|
||||
# | | | |
|
||||
# | | | |
|
||||
# +--------------+ +---------------+
|
||||
from gen.ato import PackageWaypoints
|
||||
target = self.package.target.position
|
||||
|
||||
join_point = self.preferred_join_point()
|
||||
if join_point is None:
|
||||
# The whole path from the origin airfield to the target is
|
||||
# threatened. Need to retreat out of the threat area.
|
||||
join_point = self.retreat_point(self.package_airfield().position)
|
||||
|
||||
attack_heading = join_point.heading_between_point(target)
|
||||
ingress_point = self._ingress_point(attack_heading)
|
||||
join_distance = meters(join_point.distance_to_point(target))
|
||||
ingress_distance = meters(ingress_point.distance_to_point(target))
|
||||
if join_distance < ingress_distance:
|
||||
# The second case described above. The ingress point is farther from
|
||||
# the target than the join point. Use the fallback behavior for now.
|
||||
self.legacy_package_waypoints_impl()
|
||||
return
|
||||
|
||||
# The first case described above. The ingress and join points are placed
|
||||
# reasonably relative to each other.
|
||||
egress_point = self._egress_point(attack_heading)
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
WaypointBuilder.perturb(join_point),
|
||||
ingress_point,
|
||||
egress_point,
|
||||
WaypointBuilder.perturb(join_point),
|
||||
)
|
||||
|
||||
def retreat_point(self, origin: Point) -> Point:
|
||||
return self.threat_zones.closest_boundary(origin)
|
||||
|
||||
def legacy_package_waypoints_impl(self) -> None:
|
||||
from gen.ato import PackageWaypoints
|
||||
ingress_point = self._ingress_point(
|
||||
self._target_heading_to_package_airfield())
|
||||
egress_point = self._egress_point(
|
||||
self._target_heading_to_package_airfield())
|
||||
join_point = self._rendezvous_point(ingress_point)
|
||||
split_point = self._rendezvous_point(egress_point)
|
||||
|
||||
from gen.ato import PackageWaypoints
|
||||
self.package.waypoints = PackageWaypoints(
|
||||
join_point,
|
||||
ingress_point,
|
||||
@ -768,6 +875,14 @@ class FlightPlanBuilder:
|
||||
split_point,
|
||||
)
|
||||
|
||||
def preferred_join_point(self) -> Optional[Point]:
|
||||
path = self.game.navmesh_for(self.is_player).shortest_path(
|
||||
self.package_airfield().position, self.package.target.position)
|
||||
for point in reversed(path):
|
||||
if not self.threat_zones.threatened(point):
|
||||
return point
|
||||
return None
|
||||
|
||||
def generate_strike(self, flight: Flight) -> StrikeFlightPlan:
|
||||
"""Generates a strike flight plan.
|
||||
|
||||
@ -862,20 +977,25 @@ class FlightPlanBuilder:
|
||||
if isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
start, end = self.racetrack_for_objective(location)
|
||||
patrol_alt = random.randint(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
self.doctrine.max_patrol_altitude
|
||||
)
|
||||
start, end = self.racetrack_for_objective(location, barcap=True)
|
||||
patrol_alt = meters(random.randint(
|
||||
int(self.doctrine.min_patrol_altitude.meters),
|
||||
int(self.doctrine.max_patrol_altitude.meters)
|
||||
))
|
||||
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start, end = builder.race_track(start, end, patrol_alt)
|
||||
|
||||
return BarCapFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
engagement_distance=self.doctrine.cap_engagement_range,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, start.position,
|
||||
patrol_alt),
|
||||
nav_from=builder.nav_path(end.position, flight.arrival.position,
|
||||
patrol_alt),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
land=builder.land(flight.arrival),
|
||||
@ -888,33 +1008,40 @@ class FlightPlanBuilder:
|
||||
Args:
|
||||
flight: The flight to generate the flight plan for.
|
||||
"""
|
||||
assert self.package.waypoints is not None
|
||||
target = self.package.target.position
|
||||
|
||||
heading = self._heading_to_package_airfield(target)
|
||||
heading = self.package.waypoints.join.heading_between_point(target)
|
||||
start = target.point_from_heading(heading,
|
||||
-self.doctrine.sweep_distance)
|
||||
-self.doctrine.sweep_distance.meters)
|
||||
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start, end = builder.sweep(start, target,
|
||||
self.doctrine.ingress_altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
|
||||
return SweepFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
lead_time=timedelta(minutes=5),
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
hold=hold,
|
||||
hold_duration=timedelta(minutes=5),
|
||||
nav_to=builder.nav_path(hold.position, start.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
nav_from=builder.nav_path(end.position, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
sweep_start=start,
|
||||
sweep_end=end,
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
|
||||
def racetrack_for_objective(self,
|
||||
location: MissionTarget) -> Tuple[Point, Point]:
|
||||
def racetrack_for_objective(self, location: MissionTarget,
|
||||
barcap: bool) -> Tuple[Point, Point]:
|
||||
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
for airfield in closest_cache.closest_airfields:
|
||||
for airfield in closest_cache.operational_airfields:
|
||||
# If the mission is a BARCAP of an enemy airfield, find the *next*
|
||||
# closest enemy airfield.
|
||||
if airfield == self.package.target:
|
||||
@ -929,11 +1056,28 @@ class FlightPlanBuilder:
|
||||
closest_airfield.position
|
||||
)
|
||||
|
||||
min_distance_from_enemy = nm_to_meter(20)
|
||||
distance_to_airfield = int(closest_airfield.position.distance_to_point(
|
||||
self.package.target.position
|
||||
))
|
||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||
position = ShapelyPoint(self.package.target.position.x,
|
||||
self.package.target.position.y)
|
||||
|
||||
if barcap:
|
||||
# BARCAPs should remain far enough back from the enemy that their
|
||||
# commit range does not enter the enemy's threat zone. Include a 5nm
|
||||
# buffer.
|
||||
distance_to_no_fly = meters(
|
||||
position.distance(self.threat_zones.all)
|
||||
) - self.doctrine.cap_engagement_range - nautical_miles(5)
|
||||
else:
|
||||
# Other race tracks (TARCAPs, currently) just try to keep some
|
||||
# distance from the nearest enemy airbase, but since they are by
|
||||
# definition in enemy territory they can't avoid the threat zone
|
||||
# without being useless.
|
||||
min_distance_from_enemy = nautical_miles(20)
|
||||
distance_to_airfield = meters(
|
||||
closest_airfield.position.distance_to_point(
|
||||
self.package.target.position
|
||||
))
|
||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||
|
||||
min_cap_distance = min(self.doctrine.cap_min_distance_from_cp,
|
||||
distance_to_no_fly)
|
||||
max_cap_distance = min(self.doctrine.cap_max_distance_from_cp,
|
||||
@ -941,16 +1085,17 @@ class FlightPlanBuilder:
|
||||
|
||||
end = location.position.point_from_heading(
|
||||
heading,
|
||||
random.randint(min_cap_distance, max_cap_distance)
|
||||
random.randint(int(min_cap_distance.meters),
|
||||
int(max_cap_distance.meters))
|
||||
)
|
||||
diameter = random.randint(
|
||||
self.doctrine.cap_min_track_length,
|
||||
self.doctrine.cap_max_track_length
|
||||
int(self.doctrine.cap_min_track_length.meters),
|
||||
int(self.doctrine.cap_max_track_length.meters)
|
||||
)
|
||||
start = end.point_from_heading(heading - 180, diameter)
|
||||
return start, end
|
||||
|
||||
def racetrack_for_frontline(self,
|
||||
def racetrack_for_frontline(self, origin: Point,
|
||||
front_line: FrontLine) -> Tuple[Point, Point]:
|
||||
ally_cp, enemy_cp = front_line.control_points
|
||||
|
||||
@ -960,7 +1105,8 @@ class FlightPlanBuilder:
|
||||
)
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
orbit_center = center.point_from_heading(
|
||||
heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))
|
||||
heading - 90, random.randint(int(nautical_miles(6).meters),
|
||||
int(nautical_miles(15).meters))
|
||||
)
|
||||
|
||||
combat_width = distance / 2
|
||||
@ -970,10 +1116,12 @@ class FlightPlanBuilder:
|
||||
combat_width = 35000
|
||||
|
||||
radius = combat_width * 1.25
|
||||
orbit0p = orbit_center.point_from_heading(heading, radius)
|
||||
orbit1p = orbit_center.point_from_heading(heading + 180, radius)
|
||||
start = orbit_center.point_from_heading(heading, radius)
|
||||
end = orbit_center.point_from_heading(heading + 180, radius)
|
||||
|
||||
return orbit0p, orbit1p
|
||||
if end.distance_to_point(origin) < start.distance_to_point(origin):
|
||||
start, end = end, start
|
||||
return start, end
|
||||
|
||||
def generate_tarcap(self, flight: Flight) -> TarCapFlightPlan:
|
||||
"""Generate a CAP flight plan for the given front line.
|
||||
@ -983,16 +1131,19 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
|
||||
self.doctrine.max_patrol_altitude)
|
||||
patrol_alt = meters(
|
||||
random.randint(int(self.doctrine.min_patrol_altitude.meters),
|
||||
int(self.doctrine.max_patrol_altitude.meters)))
|
||||
|
||||
# Create points
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
|
||||
if isinstance(location, FrontLine):
|
||||
orbit0p, orbit1p = self.racetrack_for_frontline(location)
|
||||
orbit0p, orbit1p = self.racetrack_for_frontline(
|
||||
flight.departure.position, location)
|
||||
else:
|
||||
orbit0p, orbit1p = self.racetrack_for_objective(location)
|
||||
orbit0p, orbit1p = self.racetrack_for_objective(location,
|
||||
barcap=False)
|
||||
|
||||
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
|
||||
return TarCapFlightPlan(
|
||||
@ -1004,7 +1155,12 @@ class FlightPlanBuilder:
|
||||
# requests an escort the CAP flight will remain on station for the
|
||||
# duration of the escorted mission, or until it is winchester/bingo.
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
engagement_distance=self.doctrine.cap_engagement_range,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, orbit0p,
|
||||
patrol_alt),
|
||||
nav_from=builder.nav_path(orbit1p, flight.arrival.position,
|
||||
patrol_alt),
|
||||
patrol_start=start,
|
||||
patrol_end=end,
|
||||
land=builder.land(flight.arrival),
|
||||
@ -1099,22 +1255,29 @@ class FlightPlanBuilder:
|
||||
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
ingress, target, egress = builder.escort(
|
||||
self.package.waypoints.ingress, self.package.target,
|
||||
self.package.waypoints.egress)
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
hold=hold,
|
||||
hold_duration=timedelta(minutes=5),
|
||||
join=builder.join(self.package.waypoints.join),
|
||||
nav_to=builder.nav_path(hold.position, join.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
join=join,
|
||||
ingress=ingress,
|
||||
targets=[target],
|
||||
egress=egress,
|
||||
split=builder.split(self.package.waypoints.split),
|
||||
split=split,
|
||||
nav_from=builder.nav_path(split.position, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
@ -1137,15 +1300,25 @@ class FlightPlanBuilder:
|
||||
center = ingress.point_from_heading(heading, distance / 2)
|
||||
egress = ingress.point_from_heading(heading, distance)
|
||||
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
ingress_distance = ingress.distance_to_point(flight.departure.position)
|
||||
egress_distance = egress.distance_to_point(flight.departure.position)
|
||||
if egress_distance < ingress_distance:
|
||||
ingress, egress = egress, ingress
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
|
||||
return CasFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cas_duration,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, ingress,
|
||||
self.doctrine.ingress_altitude),
|
||||
nav_from=builder.nav_path(egress, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
patrol_start=builder.ingress(FlightWaypointType.INGRESS_CAS,
|
||||
ingress, location),
|
||||
engagement_distance=meters(FRONTLINE_LENGTH) / 2,
|
||||
target=builder.cas(center),
|
||||
patrol_end=builder.egress(egress, location),
|
||||
land=builder.land(flight.arrival),
|
||||
@ -1188,12 +1361,13 @@ class FlightPlanBuilder:
|
||||
# point, plan the hold point such that it retreats from the origin
|
||||
# airfield.
|
||||
return join.point_from_heading(target.heading_between_point(origin),
|
||||
self.doctrine.push_distance)
|
||||
self.doctrine.push_distance.meters)
|
||||
|
||||
heading_to_join = origin.heading_between_point(join)
|
||||
hold_point = origin.point_from_heading(heading_to_join,
|
||||
self.doctrine.push_distance)
|
||||
if hold_point.distance_to_point(join) >= self.doctrine.push_distance:
|
||||
hold_point = origin.point_from_heading(
|
||||
heading_to_join, self.doctrine.push_distance.meters)
|
||||
hold_distance = meters(hold_point.distance_to_point(join))
|
||||
if hold_distance >= self.doctrine.push_distance:
|
||||
# Hold point is between the origin airfield and the join point and
|
||||
# spaced sufficiently.
|
||||
return hold_point
|
||||
@ -1205,10 +1379,10 @@ class FlightPlanBuilder:
|
||||
# properly.
|
||||
origin_to_join = origin.distance_to_point(join)
|
||||
cos_theta = (
|
||||
(self.doctrine.hold_distance ** 2 +
|
||||
(self.doctrine.hold_distance.meters ** 2 +
|
||||
origin_to_join ** 2 -
|
||||
self.doctrine.join_distance ** 2) /
|
||||
(2 * self.doctrine.hold_distance * origin_to_join)
|
||||
self.doctrine.join_distance.meters ** 2) /
|
||||
(2 * self.doctrine.hold_distance.meters * origin_to_join)
|
||||
)
|
||||
try:
|
||||
theta = math.acos(cos_theta)
|
||||
@ -1217,10 +1391,10 @@ class FlightPlanBuilder:
|
||||
# hold point away from the target.
|
||||
return origin.point_from_heading(
|
||||
target.heading_between_point(origin),
|
||||
self.doctrine.hold_distance)
|
||||
self.doctrine.hold_distance.meters)
|
||||
|
||||
return origin.point_from_heading(heading_to_join - theta,
|
||||
self.doctrine.hold_distance)
|
||||
self.doctrine.hold_distance.meters)
|
||||
|
||||
# TODO: Make a model for the waypoint builder and use that in the UI.
|
||||
def generate_rtb_waypoint(self, flight: Flight,
|
||||
@ -1231,7 +1405,7 @@ class FlightPlanBuilder:
|
||||
flight: The flight to generate the landing waypoint for.
|
||||
arrival: Arrival airfield or carrier.
|
||||
"""
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
return builder.land(arrival)
|
||||
|
||||
def strike_flightplan(
|
||||
@ -1239,8 +1413,7 @@ class FlightPlanBuilder:
|
||||
ingress_type: FlightWaypointType,
|
||||
targets: Optional[List[StrikeTarget]] = None) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine,
|
||||
targets)
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
|
||||
|
||||
target_waypoints: List[FlightWaypoint] = []
|
||||
if targets is not None:
|
||||
@ -1251,18 +1424,26 @@ class FlightPlanBuilder:
|
||||
target_waypoints.append(
|
||||
self.target_area_waypoint(flight, location, builder))
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
|
||||
return StrikeFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
hold=builder.hold(self._hold_point(flight)),
|
||||
hold=hold,
|
||||
hold_duration=timedelta(minutes=5),
|
||||
join=builder.join(self.package.waypoints.join),
|
||||
nav_to=builder.nav_path(hold.position, join.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
join=join,
|
||||
ingress=builder.ingress(ingress_type,
|
||||
self.package.waypoints.ingress, location),
|
||||
targets=target_waypoints,
|
||||
egress=builder.egress(self.package.waypoints.egress, location),
|
||||
split=builder.split(self.package.waypoints.split),
|
||||
split=split,
|
||||
nav_from=builder.nav_path(split.position, flight.arrival.position,
|
||||
self.doctrine.ingress_altitude),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert)
|
||||
)
|
||||
@ -1272,13 +1453,13 @@ class FlightPlanBuilder:
|
||||
return attack_transition.point_from_heading(
|
||||
self.package.target.position.heading_between_point(
|
||||
self.package_airfield().position),
|
||||
self.doctrine.join_distance)
|
||||
self.doctrine.join_distance.meters)
|
||||
|
||||
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
"""Creates a rendezvous point that advances toward the target."""
|
||||
heading = self._heading_to_package_airfield(attack_transition)
|
||||
return attack_transition.point_from_heading(
|
||||
heading, -self.doctrine.join_distance)
|
||||
heading, -self.doctrine.join_distance.meters)
|
||||
|
||||
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
|
||||
transition_target_distance = attack_transition.distance_to_point(
|
||||
@ -1303,16 +1484,14 @@ class FlightPlanBuilder:
|
||||
return self._retreating_rendezvous_point(attack_transition)
|
||||
return self._advancing_rendezvous_point(attack_transition)
|
||||
|
||||
def _ingress_point(self) -> Point:
|
||||
heading = self._target_heading_to_package_airfield()
|
||||
def _ingress_point(self, heading: int) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 + 25, self.doctrine.ingress_egress_distance
|
||||
heading - 180 + 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _egress_point(self) -> Point:
|
||||
heading = self._target_heading_to_package_airfield()
|
||||
def _egress_point(self, heading: int) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 - 25, self.doctrine.ingress_egress_distance
|
||||
heading - 180 - 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _target_heading_to_package_airfield(self) -> int:
|
||||
@ -1338,7 +1517,7 @@ class FlightPlanBuilder:
|
||||
cache = ObjectiveDistanceCache.get_closest_airfields(
|
||||
self.package.target
|
||||
)
|
||||
for airfield in cache.closest_airfields:
|
||||
for airfield in cache.operational_airfields:
|
||||
for flight in self.package.flights:
|
||||
if flight.departure == airfield:
|
||||
return airfield
|
||||
|
||||
@ -3,12 +3,19 @@ from __future__ import annotations
|
||||
import logging
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from game.utils import (
|
||||
Distance,
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||
Speed,
|
||||
kph,
|
||||
mach,
|
||||
meters,
|
||||
)
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -18,7 +25,7 @@ if TYPE_CHECKING:
|
||||
class GroundSpeed:
|
||||
|
||||
@classmethod
|
||||
def for_flight(cls, flight: Flight, altitude: int) -> int:
|
||||
def for_flight(cls, flight: Flight, altitude: Distance) -> Speed:
|
||||
if not issubclass(flight.unit_type, FlyingType):
|
||||
raise TypeError("Flight has non-flying unit")
|
||||
|
||||
@ -27,64 +34,29 @@ class GroundSpeed:
|
||||
# on fuel, but mission speed will be fast enough to keep the flight
|
||||
# safer.
|
||||
|
||||
c_sound_sea_level = 661.5
|
||||
|
||||
# DCS's max speed is in kph at 0 MSL. Convert to knots.
|
||||
max_speed = flight.unit_type.max_speed * 0.539957
|
||||
if max_speed > c_sound_sea_level:
|
||||
# DCS's max speed is in kph at 0 MSL.
|
||||
max_speed = kph(flight.unit_type.max_speed)
|
||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
||||
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
return int(cls.from_mach(0.8, altitude))
|
||||
return mach(0.8, altitude)
|
||||
|
||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
mach = max_speed * 0.8 / c_sound_sea_level
|
||||
return int(cls.from_mach(mach, altitude)) # knots
|
||||
|
||||
@staticmethod
|
||||
def from_mach(mach: float, altitude_m: int) -> float:
|
||||
"""Returns the ground speed in knots for the given mach and altitude.
|
||||
|
||||
Args:
|
||||
mach: The mach number to convert to ground speed.
|
||||
altitude_m: The altitude in meters.
|
||||
|
||||
Returns:
|
||||
The ground speed corresponding to the given altitude and mach number
|
||||
in knots.
|
||||
"""
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
|
||||
altitude_ft = altitude_m * 3.28084
|
||||
if altitude_ft <= 36152:
|
||||
temperature_f = 59 - 0.00356 * altitude_ft
|
||||
else:
|
||||
# There's another formula for altitudes over 82k feet, but we better
|
||||
# not be planning waypoints that high...
|
||||
temperature_f = -70
|
||||
|
||||
temperature_k = (temperature_f + 459.67) * (5 / 9)
|
||||
|
||||
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
|
||||
# Dependent on temperature, but varies very little (+/-0.001)
|
||||
# between -40F and 180F.
|
||||
heat_capacity_ratio = 1.4
|
||||
|
||||
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
|
||||
gas_constant = 286 # m^2/s^2/K
|
||||
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
|
||||
# c_sound is in m/s, convert to knots.
|
||||
return (c_sound * 1.944) * mach
|
||||
cruise_mach = max_speed.mach() * 0.8
|
||||
return mach(cruise_mach, altitude)
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@staticmethod
|
||||
def between_points(a: Point, b: Point, speed: float) -> timedelta:
|
||||
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
||||
error_factor = 1.1
|
||||
distance = meter_to_nm(a.distance_to_point(b))
|
||||
return timedelta(hours=distance / speed * error_factor)
|
||||
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.
|
||||
|
||||
@ -1,20 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import (
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
MissionTarget,
|
||||
OffMapSpawn,
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.weather import Conditions
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
|
||||
|
||||
@ -25,12 +36,13 @@ class StrikeTarget:
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
def __init__(self, conditions: Conditions, flight: Flight,
|
||||
doctrine: Doctrine,
|
||||
def __init__(self, flight: Flight, game: Game, player: bool,
|
||||
targets: Optional[List[StrikeTarget]] = None) -> None:
|
||||
self.conditions = conditions
|
||||
self.flight = flight
|
||||
self.doctrine = doctrine
|
||||
self.conditions = game.conditions
|
||||
self.doctrine = game.faction_for(player).doctrine
|
||||
self.threat_zones = game.threat_zone_for(not player)
|
||||
self.navmesh = game.navmesh_for(player)
|
||||
self.targets = targets
|
||||
|
||||
@property
|
||||
@ -53,7 +65,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.alt_type = "BARO"
|
||||
@ -64,7 +78,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TAKEOFF,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
meters(0)
|
||||
)
|
||||
waypoint.name = "TAKEOFF"
|
||||
waypoint.alt_type = "RADIO"
|
||||
@ -84,7 +98,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.alt_type = "BARO"
|
||||
@ -95,7 +111,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.LANDING_POINT,
|
||||
position.x,
|
||||
position.y,
|
||||
0
|
||||
meters(0)
|
||||
)
|
||||
waypoint.name = "LANDING"
|
||||
waypoint.alt_type = "RADIO"
|
||||
@ -116,12 +132,12 @@ class WaypointBuilder:
|
||||
position = divert.position
|
||||
if isinstance(divert, OffMapSpawn):
|
||||
if self.is_helo:
|
||||
altitude = 500
|
||||
altitude = meters(500)
|
||||
else:
|
||||
altitude = self.doctrine.rendezvous_altitude
|
||||
altitude_type = "BARO"
|
||||
else:
|
||||
altitude = 0
|
||||
altitude = meters(0)
|
||||
altitude_type = "RADIO"
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
@ -142,7 +158,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.LOITER,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Hold"
|
||||
waypoint.description = "Wait until push time"
|
||||
@ -154,7 +172,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.JOIN,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Join"
|
||||
waypoint.description = "Rendezvous with package"
|
||||
@ -166,7 +186,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.SPLIT,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "Split"
|
||||
waypoint.description = "Depart from package"
|
||||
@ -179,7 +201,9 @@ class WaypointBuilder:
|
||||
ingress_type,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "INGRESS on " + objective.name
|
||||
waypoint.description = "INGRESS on " + objective.name
|
||||
@ -193,7 +217,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.EGRESS,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.pretty_name = "EGRESS from " + target.name
|
||||
waypoint.description = "EGRESS from " + target.name
|
||||
@ -218,7 +244,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_POINT,
|
||||
target.target.position.x,
|
||||
target.target.position.y,
|
||||
0
|
||||
meters(0)
|
||||
)
|
||||
waypoint.description = description
|
||||
waypoint.pretty_name = description
|
||||
@ -249,7 +275,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
location.position.x,
|
||||
location.position.y,
|
||||
0
|
||||
meters(0)
|
||||
)
|
||||
waypoint.description = name
|
||||
waypoint.pretty_name = name
|
||||
@ -274,7 +300,7 @@ class WaypointBuilder:
|
||||
FlightWaypointType.CAS,
|
||||
position.x,
|
||||
position.y,
|
||||
500 if self.is_helo else 1000
|
||||
meters(500) if self.is_helo else meters(1000)
|
||||
)
|
||||
waypoint.alt_type = "RADIO"
|
||||
waypoint.description = "Provide CAS"
|
||||
@ -283,12 +309,12 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
|
||||
def race_track_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a racetrack start waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack in meters.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL_TRACK,
|
||||
@ -302,12 +328,12 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
|
||||
def race_track_end(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a racetrack end waypoint.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the racetrack in meters.
|
||||
altitude: Altitude of the racetrack.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.PATROL,
|
||||
@ -321,7 +347,7 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
def race_track(self, start: Point, end: Point,
|
||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
@ -333,7 +359,7 @@ class WaypointBuilder:
|
||||
self.race_track_end(end, altitude))
|
||||
|
||||
@staticmethod
|
||||
def sweep_start(position: Point, altitude: int) -> FlightWaypoint:
|
||||
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a sweep start waypoint.
|
||||
|
||||
Args:
|
||||
@ -352,7 +378,7 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def sweep_end(position: Point, altitude: int) -> FlightWaypoint:
|
||||
def sweep_end(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a sweep end waypoint.
|
||||
|
||||
Args:
|
||||
@ -371,7 +397,7 @@ class WaypointBuilder:
|
||||
return waypoint
|
||||
|
||||
def sweep(self, start: Point, end: Point,
|
||||
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
altitude: Distance) -> Tuple[FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates two waypoint for a racetrack orbit.
|
||||
|
||||
Args:
|
||||
@ -404,7 +430,9 @@ class WaypointBuilder:
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
target.position.x,
|
||||
target.position.y,
|
||||
500 if self.is_helo else self.doctrine.ingress_altitude
|
||||
meters(
|
||||
500
|
||||
) if self.is_helo else self.doctrine.ingress_altitude
|
||||
)
|
||||
waypoint.name = "TARGET"
|
||||
waypoint.description = "Escort the package"
|
||||
@ -412,3 +440,80 @@ class WaypointBuilder:
|
||||
|
||||
egress = self.egress(egress, target)
|
||||
return ingress, waypoint, egress
|
||||
|
||||
@staticmethod
|
||||
def nav(position: Point, altitude: Distance) -> FlightWaypoint:
|
||||
"""Creates a navigation point.
|
||||
|
||||
Args:
|
||||
position: Position of the waypoint.
|
||||
altitude: Altitude of the waypoint.
|
||||
"""
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.NAV,
|
||||
position.x,
|
||||
position.y,
|
||||
altitude
|
||||
)
|
||||
waypoint.name = "NAV"
|
||||
waypoint.description = "NAV"
|
||||
waypoint.pretty_name = "Nav"
|
||||
return waypoint
|
||||
|
||||
def nav_path(self, a: Point, b: Point,
|
||||
altitude: Distance) -> List[FlightWaypoint]:
|
||||
path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
|
||||
return [self.nav(self.perturb(p), altitude) for p in path]
|
||||
|
||||
def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]:
|
||||
# Examine a sliding window of three waypoints. `current` is the waypoint
|
||||
# being checked for prunability. `previous` is the last emitted waypoint
|
||||
# before `current`. `nxt` is the waypoint after `current`.
|
||||
previous: Optional[Point] = None
|
||||
current: Optional[Point] = None
|
||||
for nxt in points:
|
||||
if current is None:
|
||||
current = nxt
|
||||
continue
|
||||
if previous is None:
|
||||
previous = current
|
||||
current = nxt
|
||||
continue
|
||||
|
||||
if self.nav_point_prunable(previous, current, nxt):
|
||||
current = nxt
|
||||
continue
|
||||
|
||||
yield current
|
||||
previous = current
|
||||
current = nxt
|
||||
|
||||
def nav_point_prunable(self, previous: Point, current: Point,
|
||||
nxt: Point) -> bool:
|
||||
previous_threatened = self.threat_zones.path_threatened(previous,
|
||||
current)
|
||||
next_threatened = self.threat_zones.path_threatened(current, nxt)
|
||||
pruned_threatened = self.threat_zones.path_threatened(previous, nxt)
|
||||
previous_distance = meters(previous.distance_to_point(current))
|
||||
distance = meters(current.distance_to_point(nxt))
|
||||
distance_without = previous_distance + distance
|
||||
if distance > distance_without:
|
||||
# Don't prune paths to make them longer.
|
||||
return False
|
||||
|
||||
# We could shorten the path by removing the intermediate
|
||||
# waypoint. Do so if the new path isn't higher threat.
|
||||
if not pruned_threatened:
|
||||
# The new path is not threatened, so safe to prune.
|
||||
return True
|
||||
|
||||
# The new path is threatened. Only allow if both paths were
|
||||
# threatened anyway.
|
||||
return previous_threatened and next_threatened
|
||||
|
||||
@staticmethod
|
||||
def perturb(point: Point) -> Point:
|
||||
deviation = nautical_miles(1)
|
||||
x_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
y_adj = random.randint(int(-deviation.meters), int(deviation.meters))
|
||||
return Point(point.x + x_adj, point.y + y_adj)
|
||||
|
||||
@ -35,7 +35,7 @@ from game.theater.theatergroundobject import (
|
||||
LhaGroundObject, ShipGroundObject, MissileSiteGroundObject,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import knots_to_kph, kph_to_mps, mps_to_kph
|
||||
from game.utils import knots, mps
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .runways import RunwayData
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
@ -300,13 +300,13 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
wind = self.game.conditions.weather.wind.at_0m
|
||||
brc = wind.direction + 180
|
||||
# Aim for 25kts over the deck.
|
||||
carrier_speed = knots_to_kph(25) - mps_to_kph(wind.speed)
|
||||
carrier_speed = knots(25) - mps(wind.speed)
|
||||
for attempt in range(5):
|
||||
point = group.points[0].position.point_from_heading(
|
||||
brc, 100000 - attempt * 20000)
|
||||
if self.game.theater.is_in_sea(point):
|
||||
group.points[0].speed = kph_to_mps(carrier_speed)
|
||||
group.add_waypoint(point, carrier_speed)
|
||||
group.points[0].speed = carrier_speed.meters_per_second
|
||||
group.add_waypoint(point, carrier_speed.kph)
|
||||
return brc
|
||||
return None
|
||||
|
||||
|
||||
@ -33,8 +33,7 @@ from dcs.mission import Mission
|
||||
from dcs.unittype import FlyingType
|
||||
from tabulate import tabulate
|
||||
|
||||
from game.utils import meter_to_nm
|
||||
from . import units
|
||||
from game.utils import meters
|
||||
from .aircraft import AIRCRAFT_DATA, FlightData
|
||||
from .airsupportgen import AwacsInfo, TankerInfo
|
||||
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
@ -95,6 +94,23 @@ class KneeboardPageWriter:
|
||||
def write(self, path: Path) -> None:
|
||||
self.image.save(path)
|
||||
|
||||
@staticmethod
|
||||
def wrap_line(inputstr: str, max_length: int) -> str:
|
||||
if len(inputstr) <= max_length:
|
||||
return inputstr
|
||||
tokens = inputstr.split(" ")
|
||||
output = ""
|
||||
segments = []
|
||||
for token in tokens:
|
||||
combo = output + " " + token
|
||||
if len(combo) > max_length:
|
||||
combo = output + "\n" + token
|
||||
segments.append(combo)
|
||||
output = ""
|
||||
else:
|
||||
output = combo
|
||||
return "".join(segments + [output]).strip()
|
||||
|
||||
|
||||
class KneeboardPage:
|
||||
"""Base class for all kneeboard pages."""
|
||||
@ -111,6 +127,9 @@ class NumberedWaypoint:
|
||||
|
||||
|
||||
class FlightPlanBuilder:
|
||||
|
||||
WAYPOINT_DESC_MAX_LEN = 25
|
||||
|
||||
def __init__(self, start_time: datetime.datetime) -> None:
|
||||
self.start_time = start_time
|
||||
self.rows: List[List[str]] = []
|
||||
@ -152,8 +171,10 @@ class FlightPlanBuilder:
|
||||
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
|
||||
self.rows.append([
|
||||
str(waypoint.number),
|
||||
waypoint.waypoint.pretty_name,
|
||||
str(int(units.meters_to_feet(waypoint.waypoint.alt))),
|
||||
KneeboardPageWriter.wrap_line(
|
||||
waypoint.waypoint.pretty_name,
|
||||
FlightPlanBuilder.WAYPOINT_DESC_MAX_LEN),
|
||||
str(int(waypoint.waypoint.alt.feet)),
|
||||
self._waypoint_distance(waypoint.waypoint),
|
||||
self._ground_speed(waypoint.waypoint),
|
||||
self._format_time(waypoint.waypoint.tot),
|
||||
@ -170,10 +191,10 @@ class FlightPlanBuilder:
|
||||
if self.last_waypoint is None:
|
||||
return "-"
|
||||
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
distance = meters(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
return f"{distance} NM"
|
||||
return f"{distance.nautical_miles:.1f} NM"
|
||||
|
||||
def _ground_speed(self, waypoint: FlightWaypoint) -> str:
|
||||
if self.last_waypoint is None:
|
||||
@ -189,19 +210,11 @@ class FlightPlanBuilder:
|
||||
else:
|
||||
return "-"
|
||||
|
||||
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
|
||||
distance = meters(self.last_waypoint.position.distance_to_point(
|
||||
waypoint.position
|
||||
))
|
||||
duration = (waypoint.tot - last_time).total_seconds() / 3600
|
||||
try:
|
||||
return f"{int(distance / duration)} kt"
|
||||
except ZeroDivisionError:
|
||||
# TODO: Improve resolution of unit conversions.
|
||||
# When waypoints are very close to each other they can end up with
|
||||
# identical TOTs because our unit conversion functions truncate to
|
||||
# int. When waypoints have the same TOT the duration will be zero.
|
||||
# https://github.com/Khopa/dcs_liberation/issues/557
|
||||
return "-"
|
||||
return f"{int(distance.nautical_miles / duration)} kt"
|
||||
|
||||
def build(self) -> List[List[str]]:
|
||||
return self.rows
|
||||
@ -267,11 +280,9 @@ class BriefingPage(KneeboardPage):
|
||||
str(tanker.tacan),
|
||||
self.format_frequency(tanker.freq),
|
||||
])
|
||||
|
||||
|
||||
writer.table(comm_ladder, headers=["Callsign","Task", "Type", "TACAN", "FREQ"])
|
||||
|
||||
|
||||
writer.heading("JTAC")
|
||||
jtacs = []
|
||||
for jtac in self.jtacs:
|
||||
|
||||
130
gen/naming.py
130
gen/naming.py
@ -1,16 +1,21 @@
|
||||
from game import db
|
||||
import random
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
ALPHA_MILITARY = ["Alpha","Bravo","Charlie","Delta","Echo","Foxtrot",
|
||||
"Golf","Hotel","India","Juliet","Kilo","Lima","Mike",
|
||||
"November","Oscar","Papa","Quebec","Romeo","Sierra",
|
||||
"Tango","Uniform","Victor","Whisky","XRay","Yankee",
|
||||
"Zulu","Zero"]
|
||||
from dcs.country import Country
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
class NameGenerator:
|
||||
number = 0
|
||||
from game import db
|
||||
|
||||
ANIMALS = [
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
ALPHA_MILITARY = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot",
|
||||
"Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike",
|
||||
"November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra",
|
||||
"Tango", "Uniform", "Victor", "Whisky", "XRay", "Yankee",
|
||||
"Zulu", "Zero"]
|
||||
|
||||
ANIMALS = [
|
||||
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF",
|
||||
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER",
|
||||
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA",
|
||||
@ -38,47 +43,92 @@ class NameGenerator:
|
||||
"ANACONDA"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.number = 0
|
||||
self.ANIMALS = NameGenerator.ANIMALS.copy()
|
||||
class NameGenerator:
|
||||
number = 0
|
||||
infantry_number = 0
|
||||
aircraft_number = 0
|
||||
|
||||
def reset(self):
|
||||
self.number = 0
|
||||
self.ANIMALS = NameGenerator.ANIMALS.copy()
|
||||
ANIMALS = ANIMALS
|
||||
existing_alphas: List[str] = []
|
||||
|
||||
def next_unit_name(self, country, parent_base_id, unit_type):
|
||||
self.number += 1
|
||||
return "unit|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
cls.number = 0
|
||||
cls.infantry_number = 0
|
||||
cls.ANIMALS = ANIMALS
|
||||
cls.existing_alphas = []
|
||||
|
||||
def next_infantry_name(self, country, parent_base_id, unit_type):
|
||||
self.number += 1
|
||||
return "infantry|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
@classmethod
|
||||
def reset_numbers(cls):
|
||||
cls.number = 0
|
||||
cls.infantry_number = 0
|
||||
cls.aircraft_number = 0
|
||||
|
||||
def next_basedefense_name(self):
|
||||
@classmethod
|
||||
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
|
||||
cls.aircraft_number += 1
|
||||
try:
|
||||
if flight.custom_name:
|
||||
name_str = flight.custom_name
|
||||
else:
|
||||
name_str = "{} {}".format(
|
||||
flight.package.target.name, flight.flight_type)
|
||||
except AttributeError: # Here to maintain save compatibility with 2.3
|
||||
name_str = "{} {}".format(
|
||||
flight.package.target.name, flight.flight_type)
|
||||
return "{}|{}|{}|{}|{}|".format(name_str, country.id, cls.aircraft_number, parent_base_id, db.unit_type_name(flight.unit_type))
|
||||
|
||||
@classmethod
|
||||
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||
cls.number += 1
|
||||
return "unit|{}|{}|{}|{}|".format(country.id, cls.number, parent_base_id, db.unit_type_name(unit_type))
|
||||
|
||||
@classmethod
|
||||
def next_infantry_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||
cls.infantry_number += 1
|
||||
return "infantry|{}|{}|{}|{}|".format(country.id, cls.infantry_number, parent_base_id, db.unit_type_name(unit_type))
|
||||
|
||||
@staticmethod
|
||||
def next_basedefense_name():
|
||||
return "basedefense_aa|0|0|"
|
||||
|
||||
def next_awacs_name(self, country):
|
||||
self.number += 1
|
||||
return "awacs|{}|{}|0|".format(country.id, self.number)
|
||||
@classmethod
|
||||
def next_awacs_name(cls, country: Country):
|
||||
cls.number += 1
|
||||
return "awacs|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
def next_tanker_name(self, country, unit_type):
|
||||
self.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, self.number, db.unit_type_name(unit_type))
|
||||
@classmethod
|
||||
def next_tanker_name(cls, country: Country, unit_type: UnitType):
|
||||
cls.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, cls.number, db.unit_type_name(unit_type))
|
||||
|
||||
def next_carrier_name(self, country):
|
||||
self.number += 1
|
||||
return "carrier|{}|{}|0|".format(country.id, self.number)
|
||||
@classmethod
|
||||
def next_carrier_name(cls, country: Country):
|
||||
cls.number += 1
|
||||
return "carrier|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
def random_objective_name(self):
|
||||
if len(self.ANIMALS) == 0:
|
||||
return random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100))
|
||||
@classmethod
|
||||
def random_objective_name(cls):
|
||||
if len(cls.ANIMALS) == 0:
|
||||
for i in range(10):
|
||||
new_name_generated = True
|
||||
alpha_mil_name = random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100))
|
||||
for existing_name in cls.existing_alphas:
|
||||
if existing_name == alpha_mil_name:
|
||||
new_name_generated = False
|
||||
if new_name_generated:
|
||||
cls.existing_alphas.append(alpha_mil_name)
|
||||
return alpha_mil_name
|
||||
|
||||
# At this point, give up trying - something has gone wrong and we haven't been able to make a new name in 10 tries.
|
||||
# We'll just make a longer name using the current unix epoch in nanoseconds. That should be unique... right?
|
||||
last_chance_name = alpha_mil_name + str(time.time_ns())
|
||||
cls.existing_alphas.append(last_chance_name)
|
||||
return last_chance_name
|
||||
else:
|
||||
animal = random.choice(self.ANIMALS)
|
||||
self.ANIMALS.remove(animal)
|
||||
animal = random.choice(cls.ANIMALS)
|
||||
cls.ANIMALS.remove(animal)
|
||||
return animal
|
||||
|
||||
|
||||
namegen = NameGenerator()
|
||||
|
||||
|
||||
|
||||
namegen = NameGenerator
|
||||
|
||||
34
gen/sam/aaa_ks19.py
Normal file
34
gen/sam/aaa_ks19.py
Normal file
@ -0,0 +1,34 @@
|
||||
import random
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
)
|
||||
from pydcs_extensions.highdigitsams import highdigitsams
|
||||
|
||||
|
||||
class KS19Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
This generate a KS 19 flak artillery group (KS-19 from the High Digit SAM mod)
|
||||
"""
|
||||
|
||||
name = "KS-19 AAA Site"
|
||||
price = 98
|
||||
|
||||
def generate(self):
|
||||
|
||||
spacing = random.randint(10, 40)
|
||||
|
||||
self.add_unit(highdigitsams.AAA_SON_9_Fire_Can, "TR", self.position.x - 20, self.position.y - 20, self.heading)
|
||||
|
||||
index = 0
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
index = index + 1
|
||||
self.add_unit(highdigitsams.AAA_100mm_KS_19, "AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j, self.heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Short
|
||||
@ -1,5 +1,9 @@
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Iterator, List
|
||||
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game import Game
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
@ -21,6 +25,25 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
||||
ground_object.skynet_capable = True
|
||||
super().__init__(game, ground_object)
|
||||
|
||||
self.auxiliary_groups: List[VehicleGroup] = []
|
||||
|
||||
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
|
||||
group = VehicleGroup(self.game.next_group_id(),
|
||||
"|".join([self.go.group_name, name_suffix]))
|
||||
self.auxiliary_groups.append(group)
|
||||
return group
|
||||
|
||||
def get_generated_group(self) -> VehicleGroup:
|
||||
raise RuntimeError(
|
||||
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
|
||||
"misses auxiliary groups. Use AirDefenseGroupGenerator.groups "
|
||||
"instead.")
|
||||
|
||||
@property
|
||||
def groups(self) -> Iterator[VehicleGroup]:
|
||||
yield self.vg
|
||||
yield from self.auxiliary_groups
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from dcs import unitgroup
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import PointAction
|
||||
from dcs.unit import Vehicle, Ship
|
||||
from dcs.unit import Ship, Vehicle
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.factions.faction import Faction
|
||||
@ -40,12 +42,17 @@ class GroupGenerator:
|
||||
|
||||
def add_unit(self, unit_type: Type[VehicleType], name: str, pos_x: float,
|
||||
pos_y: float, heading: int) -> Vehicle:
|
||||
return self.add_unit_to_group(self.vg, unit_type, name,
|
||||
Point(pos_x, pos_y), heading)
|
||||
|
||||
def add_unit_to_group(self, group: unitgroup.VehicleGroup,
|
||||
unit_type: Type[VehicleType], name: str,
|
||||
position: Point, heading: int) -> Vehicle:
|
||||
unit = Vehicle(self.game.next_unit_id(),
|
||||
f"{self.go.group_name}|{name}", unit_type.id)
|
||||
unit.position.x = pos_x
|
||||
unit.position.y = pos_y
|
||||
f"{group.name}|{name}", unit_type.id)
|
||||
unit.position = position
|
||||
unit.heading = heading
|
||||
self.vg.add_unit(unit)
|
||||
group.add_unit(unit)
|
||||
return unit
|
||||
|
||||
def get_circular_position(self, num_units, launcher_distance, coverage=90):
|
||||
|
||||
@ -11,6 +11,7 @@ from game.theater.theatergroundobject import SamGroundObject
|
||||
from gen.sam.aaa_bofors import BoforsGenerator
|
||||
from gen.sam.aaa_flak import FlakGenerator
|
||||
from gen.sam.aaa_flak18 import Flak18Generator
|
||||
from gen.sam.aaa_ks19 import KS19Generator
|
||||
from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
|
||||
from gen.sam.aaa_zsu57 import ZSU57Generator
|
||||
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
|
||||
@ -48,11 +49,12 @@ from gen.sam.sam_roland import RolandGenerator
|
||||
from gen.sam.sam_sa10 import (
|
||||
SA10Generator,
|
||||
Tier2SA10Generator,
|
||||
Tier3SA10Generator,
|
||||
Tier3SA10Generator, SA10BGenerator, SA12Generator, SA20Generator, SA20BGenerator, SA23Generator,
|
||||
)
|
||||
from gen.sam.sam_sa11 import SA11Generator
|
||||
from gen.sam.sam_sa13 import SA13Generator
|
||||
from gen.sam.sam_sa15 import SA15Generator
|
||||
from gen.sam.sam_sa17 import SA17Generator
|
||||
from gen.sam.sam_sa19 import SA19Generator
|
||||
from gen.sam.sam_sa2 import SA2Generator
|
||||
from gen.sam.sam_sa3 import SA3Generator
|
||||
@ -100,7 +102,15 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
|
||||
"EarlyColdWarFlakGenerator": EarlyColdWarFlakGenerator,
|
||||
"FreyaGenerator": FreyaGenerator,
|
||||
"AllyWW2FlakGenerator": AllyWW2FlakGenerator,
|
||||
"ZSU57Generator": ZSU57Generator
|
||||
"ZSU57Generator": ZSU57Generator,
|
||||
|
||||
"KS19Generator": KS19Generator,
|
||||
"SA10BGenerator": SA10BGenerator,
|
||||
"SA12Generator": SA12Generator,
|
||||
"SA17Generator": SA17Generator,
|
||||
"SA20Generator": SA20Generator,
|
||||
"SA20BGenerator": SA20BGenerator,
|
||||
"SA23Generator": SA23Generator,
|
||||
}
|
||||
|
||||
|
||||
@ -171,19 +181,19 @@ def get_faction_possible_ewrs_generator(faction: Faction) -> List[Type[GroupGene
|
||||
|
||||
def _generate_anti_air_from(
|
||||
generators: Sequence[Type[AirDefenseGroupGenerator]], game: Game,
|
||||
ground_object: SamGroundObject) -> Optional[VehicleGroup]:
|
||||
ground_object: SamGroundObject) -> List[VehicleGroup]:
|
||||
if not generators:
|
||||
return None
|
||||
return []
|
||||
sam_generator_class = random.choice(generators)
|
||||
generator = sam_generator_class(game, ground_object)
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
return list(generator.groups)
|
||||
|
||||
|
||||
def generate_anti_air_group(
|
||||
game: Game, ground_object: SamGroundObject, faction: Faction,
|
||||
ranges: Optional[Iterable[Set[AirDefenseRange]]] = None
|
||||
) -> Optional[VehicleGroup]:
|
||||
) -> List[VehicleGroup]:
|
||||
"""
|
||||
This generate a SAM group
|
||||
:param game: The Game.
|
||||
@ -212,11 +222,11 @@ def generate_anti_air_group(
|
||||
for range_options in ranges:
|
||||
generators_for_range = [g for g in generators if
|
||||
g.range() in range_options]
|
||||
group = _generate_anti_air_from(generators_for_range, game,
|
||||
ground_object)
|
||||
if group is not None:
|
||||
return group
|
||||
return None
|
||||
groups = _generate_anti_air_from(generators_for_range, game,
|
||||
ground_object)
|
||||
if groups:
|
||||
return groups
|
||||
return []
|
||||
|
||||
|
||||
def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
@ -22,7 +23,9 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
||||
self.add_unit(AirDefence.SAM_Hawk_TR_AN_MPQ_46, "TR", self.position.x + 40, self.position.y, self.heading)
|
||||
|
||||
# Triple A for close range defense
|
||||
self.add_unit(AirDefence.AAA_Vulcan_M163, "AAA", self.position.x + 20, self.position.y+30, self.heading)
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163, "AAA",
|
||||
self.position + Point(20, 30), self.heading)
|
||||
|
||||
num_launchers = random.randint(3, 6)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
@ -21,8 +22,13 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
||||
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading)
|
||||
|
||||
# Triple A for close range defense
|
||||
self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "AAA1", self.position.x + 20, self.position.y+30, self.heading)
|
||||
self.add_unit(AirDefence.AAA_ZU_23_on_Ural_375, "AAA2", self.position.x - 20, self.position.y-30, self.heading)
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
"AAA1", self.position + Point(20, 30),
|
||||
self.heading)
|
||||
self.add_unit_to_group(aa_group, AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
"AAA2", self.position - Point(20, 30),
|
||||
self.heading)
|
||||
|
||||
num_launchers = random.randint(0, 3)
|
||||
if num_launchers > 0:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import random
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
@ -30,10 +31,12 @@ class PatriotGenerator(AirDefenseGroupGenerator):
|
||||
self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
# Short range protection for high value site
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
num_launchers = random.randint(3, 4)
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(aa_group, AirDefence.AAA_Vulcan_M163,
|
||||
f"SPAAA#{i}", Point(x, y), heading)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import random
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game
|
||||
from game.theater import SamGroundObject
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
)
|
||||
from pydcs_extensions.highdigitsams import highdigitsams
|
||||
|
||||
|
||||
class SA10Generator(AirDefenseGroupGenerator):
|
||||
@ -16,20 +21,30 @@ class SA10Generator(AirDefenseGroupGenerator):
|
||||
name = "SA-10/S-300PS Battery"
|
||||
price = 550
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = AirDefence.SAM_SA_10_S_300PS_SR_5N66M
|
||||
self.sr2 = AirDefence.SAM_SA_10_S_300PS_SR_64H6E
|
||||
self.cp = AirDefence.SAM_SA_10_S_300PS_CP_54K6
|
||||
self.tr1 = AirDefence.SAM_SA_10_S_300PS_TR_30N6
|
||||
self.tr2 = AirDefence.SAM_SA_10_S_300PS_TR_30N6
|
||||
self.ln1 = AirDefence.SAM_SA_10_S_300PS_LN_5P85C
|
||||
self.ln2 = AirDefence.SAM_SA_10_S_300PS_LN_5P85D
|
||||
|
||||
def generate(self):
|
||||
# Search Radar
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_5N66M, "SR1", self.position.x, self.position.y + 40, self.heading)
|
||||
self.add_unit(self.sr1, "SR1", self.position.x, self.position.y + 40, self.heading)
|
||||
|
||||
# Search radar for missiles (optionnal)
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_SR_64H6E, "SR2", self.position.x - 40, self.position.y, self.heading)
|
||||
self.add_unit(self.sr2, "SR2", self.position.x - 40, self.position.y, self.heading)
|
||||
|
||||
# Command Post
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading)
|
||||
self.add_unit(self.cp, "CP", self.position.x, self.position.y, self.heading)
|
||||
|
||||
# 2 Tracking radars
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_TR_30N6, "TR1", self.position.x - 40, self.position.y - 40, self.heading)
|
||||
self.add_unit(self.tr1, "TR1", self.position.x - 40, self.position.y - 40, self.heading)
|
||||
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_TR_30N6, "TR2", self.position.x + 40, self.position.y - 40,
|
||||
self.add_unit(self.tr2, "TR2", self.position.x + 40, self.position.y - 40,
|
||||
self.heading)
|
||||
|
||||
# 2 different launcher type (C & D)
|
||||
@ -37,9 +52,9 @@ class SA10Generator(AirDefenseGroupGenerator):
|
||||
positions = self.get_circular_position(num_launchers, launcher_distance=100, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
if i%2 == 0:
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85C, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(self.ln1, "LN#" + str(i), position[0], position[1], position[2])
|
||||
else:
|
||||
self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85D, "LN#" + str(i), position[0], position[1], position[2])
|
||||
self.add_unit(self.ln2, "LN#" + str(i), position[0], position[1], position[2])
|
||||
|
||||
self.generate_defensive_groups()
|
||||
|
||||
@ -49,47 +64,126 @@ class SA10Generator(AirDefenseGroupGenerator):
|
||||
|
||||
def generate_defensive_groups(self) -> None:
|
||||
# AAA for defending against close targets.
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
num_launchers = random.randint(6, 8)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=210, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(aa_group, AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
f"AA#{i}", Point(x, y), heading)
|
||||
|
||||
|
||||
class Tier2SA10Generator(SA10Generator):
|
||||
def generate_defensive_groups(self) -> None:
|
||||
# Create AAA the way the main group does.
|
||||
super().generate_defensive_groups()
|
||||
|
||||
# SA-15 for both shorter range targets and point defense.
|
||||
pd_group = self.add_auxiliary_group("PD")
|
||||
num_launchers = random.randint(2, 4)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=140, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
|
||||
# AAA for defending against close targets.
|
||||
num_launchers = random.randint(6, 8)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=210, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331,
|
||||
f"PD#{i}", Point(x, y), heading)
|
||||
|
||||
|
||||
class Tier3SA10Generator(SA10Generator):
|
||||
def generate_defensive_groups(self) -> None:
|
||||
# SA-15 for both shorter range targets and point defense.
|
||||
num_launchers = random.randint(2, 4)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=140, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "PD#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
|
||||
# AAA for defending against close targets.
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
num_launchers = random.randint(6, 8)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=210, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(AirDefence.SAM_SA_19_Tunguska_2S6, "AA#" + str(i),
|
||||
position[0], position[1], position[2])
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(aa_group, AirDefence.SAM_SA_19_Tunguska_2S6,
|
||||
f"AA#{i}", Point(x, y), heading)
|
||||
|
||||
# SA-15 for both shorter range targets and point defense.
|
||||
pd_group = self.add_auxiliary_group("PD")
|
||||
num_launchers = random.randint(2, 4)
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=140, coverage=360)
|
||||
for i, (x, y, heading) in enumerate(positions):
|
||||
self.add_unit_to_group(pd_group, AirDefence.SAM_SA_15_Tor_9A331,
|
||||
f"PD#{i}", Point(x, y), heading)
|
||||
|
||||
|
||||
class SA10BGenerator(Tier3SA10Generator):
|
||||
|
||||
price = 700
|
||||
name = "SA-10B/S-300PS Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR
|
||||
self.sr2 = highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR
|
||||
self.cp = highdigitsams.SAM_SA_10B_S_300PS_54K6_CP
|
||||
self.tr1 = highdigitsams.SAM_SA_10B_S_300PS_30N6_TR
|
||||
self.tr2 = highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR
|
||||
self.ln1 = highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN
|
||||
self.ln2 = highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN
|
||||
|
||||
|
||||
class SA12Generator(Tier3SA10Generator):
|
||||
|
||||
price = 750
|
||||
name = "SA-12/S-300V Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_12_S_300V_9S15_SR
|
||||
self.sr2 = highdigitsams.SAM_SA_12_S_300V_9S19_SR
|
||||
self.cp = highdigitsams.SAM_SA_12_S_300V_9S457_CP
|
||||
self.tr1 = highdigitsams.SAM_SA_12_S_300V_9S32_TR
|
||||
self.tr2 = highdigitsams.SAM_SA_12_S_300V_9S32_TR
|
||||
self.ln1 = highdigitsams.SAM_SA_12_S_300V_9A82_LN
|
||||
self.ln2 = highdigitsams.SAM_SA_12_S_300V_9A83_LN
|
||||
|
||||
|
||||
class SA20Generator(Tier3SA10Generator):
|
||||
|
||||
price = 800
|
||||
name = "SA-20/S-300PMU-1 Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E
|
||||
self.sr2 = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E
|
||||
self.cp = highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6
|
||||
self.tr1 = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E
|
||||
self.tr2 = highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck
|
||||
self.ln1 = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE
|
||||
self.ln2 = highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE
|
||||
|
||||
|
||||
class SA20BGenerator(Tier3SA10Generator):
|
||||
|
||||
price = 850
|
||||
name = "SA-20B/S-300PMU-2 Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E
|
||||
self.sr2 = highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E
|
||||
self.cp = highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2
|
||||
self.tr1 = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck
|
||||
self.tr2 = highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck
|
||||
self.ln1 = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2
|
||||
self.ln2 = highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2
|
||||
|
||||
|
||||
class SA23Generator(Tier3SA10Generator):
|
||||
|
||||
price = 950
|
||||
name = "SA-23/S-300VM Battery"
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject):
|
||||
super().__init__(game, ground_object)
|
||||
self.sr1 = highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR
|
||||
self.sr2 = highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR
|
||||
self.cp = highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP
|
||||
self.tr1 = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
|
||||
self.tr2 = highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR
|
||||
self.ln1 = highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN
|
||||
self.ln2 = highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN
|
||||
30
gen/sam/sam_sa17.py
Normal file
30
gen/sam/sam_sa17.py
Normal file
@ -0,0 +1,30 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
)
|
||||
from pydcs_extensions.highdigitsams import highdigitsams
|
||||
|
||||
|
||||
class SA17Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
This generate a SA-17 group
|
||||
"""
|
||||
|
||||
name = "SA-17 Grizzly Battery"
|
||||
price = 180
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x + 20, self.position.y, self.heading)
|
||||
self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading)
|
||||
|
||||
positions = self.get_circular_position(3, launcher_distance=140, coverage=180)
|
||||
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2, "LN#" + str(i), position[0], position[1],
|
||||
position[2])
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
return AirDefenseRange.Medium
|
||||
2
pydcs
2
pydcs
@ -1 +1 @@
|
||||
Subproject commit 059c88c91b5be4b5b6406249a52527c3ccea3db9
|
||||
Subproject commit 84f116c35879b05470ad7f30b0c5b0bca26088a5
|
||||
@ -132,31 +132,31 @@ class Hercules(PlaneType):
|
||||
charge_total = 1680
|
||||
chaff_charge_size = 1
|
||||
flare_charge_size = 1
|
||||
radio_frequency = 305
|
||||
radio_frequency = 118
|
||||
|
||||
panel_radio = {
|
||||
1: {
|
||||
"channels": {
|
||||
1: 305,
|
||||
2: 264,
|
||||
4: 256,
|
||||
8: 257,
|
||||
16: 261,
|
||||
17: 261,
|
||||
9: 255,
|
||||
18: 251,
|
||||
5: 254,
|
||||
10: 262,
|
||||
20: 266,
|
||||
11: 259,
|
||||
3: 265,
|
||||
6: 250,
|
||||
12: 268,
|
||||
13: 269,
|
||||
7: 270,
|
||||
14: 260,
|
||||
19: 253,
|
||||
15: 263
|
||||
1: 118,
|
||||
2: 119,
|
||||
4: 121,
|
||||
8: 125,
|
||||
16: 133,
|
||||
17: 134,
|
||||
9: 126,
|
||||
18: 135,
|
||||
5: 122,
|
||||
10: 127,
|
||||
20: 143,
|
||||
11: 128,
|
||||
3: 120,
|
||||
6: 123,
|
||||
12: 129,
|
||||
13: 130,
|
||||
7: 124,
|
||||
14: 131,
|
||||
19: 136,
|
||||
15: 132
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,102 @@
|
||||
from dcs import unittype
|
||||
|
||||
|
||||
class AAA_SON_9_Fire_Can(unittype.VehicleType):
|
||||
id = "Fire Can radar"
|
||||
name = "AAA SON-9 Fire Can"
|
||||
detection_range = 35000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class AAA_100mm_KS_19(unittype.VehicleType):
|
||||
id = "KS19"
|
||||
name = "AAA 100mm KS-19"
|
||||
detection_range = 0
|
||||
threat_range = 15000
|
||||
air_weapon_dist = 15000
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_54K6_CP(unittype.VehicleType):
|
||||
id = "S-300PS SA-10B 54K6 cp"
|
||||
name = "SAM SA-10B S-300PS 54K6 CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_5P85SE_LN(unittype.VehicleType):
|
||||
id = "S-300PS 5P85SE_mod ln"
|
||||
name = "SAM SA-10B S-300PS 5P85SE LN "
|
||||
detection_range = 0
|
||||
threat_range = 75000
|
||||
air_weapon_dist = 75000
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_5P85SU_LN(unittype.VehicleType):
|
||||
id = "S-300PS 5P85SU_mod ln"
|
||||
name = "SAM SA-10B S-300PS 5P85SU LN "
|
||||
detection_range = 0
|
||||
threat_range = 75000
|
||||
air_weapon_dist = 75000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85CE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85DE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_30N6_TR(unittype.VehicleType):
|
||||
id = "S-300PS 30N6 TRAILER tr"
|
||||
name = "SAM SA-10B S-300PS 30N6 TR"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_40B6M_TR(unittype.VehicleType):
|
||||
id = "S-300PS SA-10B 40B6M MAST tr"
|
||||
name = "SAM SA-10B S-300PS 40B6M TR"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_40B6MD_SR(unittype.VehicleType):
|
||||
id = "S-300PS SA-10B 40B6MD MAST sr"
|
||||
name = "SAM SA-10B S-300PS 40B6MD SR"
|
||||
detection_range = 60000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_10B_S_300PS_64H6E_SR(unittype.VehicleType):
|
||||
id = "S-300PS 64H6E TRAILER sr"
|
||||
name = "SAM SA-10B S-300PS 64H6E SR"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType):
|
||||
id = "S-300PMU1 54K6 cp"
|
||||
name = "SAM SA-20 S-300PMU1 CP 54K6"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_TR_30N6E(unittype.VehicleType):
|
||||
id = "S-300PMU1 40B6M tr"
|
||||
name = "SAM SA-20 S-300PMU1 TR 30N6E"
|
||||
@ -33,6 +129,110 @@ class SAM_SA_20_S_300PMU1_SR_64N6E(unittype.VehicleType):
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85CE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85DE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_20B_S_300PMU2_CP_54K6E2(unittype.VehicleType):
|
||||
id = "S-300PMU2 54K6E2 cp"
|
||||
name = "SAM SA-20B S-300PMU2 CP 54K6E2"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20B_S_300PMU2_TR_92H6E_truck(unittype.VehicleType):
|
||||
id = "S-300PMU2 92H6E tr"
|
||||
name = "SAM SA-20B S-300PMU2 TR 92H6E(truck)"
|
||||
detection_range = 270000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20B_S_300PMU2_SR_64N6E2(unittype.VehicleType):
|
||||
id = "S-300PMU2 64H6E2 sr"
|
||||
name = "SAM SA-20B S-300PMU2 SR 64N6E2"
|
||||
detection_range = 330000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20B_S_300PMU2_LN_5P85SE2(unittype.VehicleType):
|
||||
id = "S-300PMU2 5P85SE2 ln"
|
||||
name = "SAM SA-20B S-300PMU2 LN 5P85SE2"
|
||||
detection_range = 0
|
||||
threat_range = 200000
|
||||
air_weapon_dist = 200000
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9S457_CP(unittype.VehicleType):
|
||||
id = "S-300V 9S457 cp"
|
||||
name = "SAM SA-12 S-300V 9S457 CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9A82_LN(unittype.VehicleType):
|
||||
id = "S-300V 9A82 ln"
|
||||
name = "SAM SA-12 S-300V 9A82 LN"
|
||||
detection_range = 0
|
||||
threat_range = 100000
|
||||
air_weapon_dist = 100000
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9A83_LN(unittype.VehicleType):
|
||||
id = "S-300V 9A83 ln"
|
||||
name = "SAM SA-12 S-300V 9A83 LN"
|
||||
detection_range = 0
|
||||
threat_range = 75000
|
||||
air_weapon_dist = 75000
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9S15_SR(unittype.VehicleType):
|
||||
id = "S-300V 9S15 sr"
|
||||
name = "SAM SA-12 S-300V 9S15 SR"
|
||||
detection_range = 240000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9S19_SR(unittype.VehicleType):
|
||||
id = "S-300V 9S19 sr"
|
||||
name = "SAM SA-12 S-300V 9S19 SR"
|
||||
detection_range = 175000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_12_S_300V_9S32_TR(unittype.VehicleType):
|
||||
id = "S-300V 9S32 tr"
|
||||
name = "SAM SA-12 S-300V 9S32 TR"
|
||||
detection_range = 150000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType):
|
||||
id = "S-300VM 9S457ME cp"
|
||||
name = "SAM SA-23 S-300VM 9S457ME CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S15M2_SR(unittype.VehicleType):
|
||||
id = "S-300VM 9S15M2 sr"
|
||||
name = "SAM SA-23 S-300VM 9S15M2 SR"
|
||||
@ -57,38 +257,6 @@ class SAM_SA_23_S_300VM_9S32ME_TR(unittype.VehicleType):
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85CE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PMU1 5P85DE ln"
|
||||
name = "SAM SA-20 S-300PMU1 LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85CE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType):
|
||||
id = "S-300PS 5P85DE ln"
|
||||
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE"
|
||||
detection_range = 0
|
||||
threat_range = 90000
|
||||
air_weapon_dist = 90000
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9A83ME_LN(unittype.VehicleType):
|
||||
id = "S-300VM 9A83ME ln"
|
||||
name = "SAM SA-23 S-300VM 9A83ME LN"
|
||||
@ -137,22 +305,6 @@ class SAM_SA_3__V_601P__LN_5P73(unittype.VehicleType):
|
||||
air_weapon_dist = 18000
|
||||
|
||||
|
||||
class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType):
|
||||
id = "S-300PMU1 54K6 cp"
|
||||
name = "SAM SA-20 S-300PMU1 CP 54K6"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType):
|
||||
id = "S-300VM 9S457ME cp"
|
||||
name = "SAM SA-23 S-300VM 9S457ME CP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class SAM_SA_24_Igla_S_manpad(unittype.VehicleType):
|
||||
id = "SA-24 Igla-S manpad"
|
||||
name = "SAM SA-24 Igla-S manpad"
|
||||
@ -167,3 +319,20 @@ class SAM_SA_14_Strela_3_manpad(unittype.VehicleType):
|
||||
detection_range = 5000
|
||||
threat_range = 4500
|
||||
air_weapon_dist = 4500
|
||||
|
||||
|
||||
class Polyana_D4M1_C2_node(unittype.VehicleType):
|
||||
id = "polyana-d4m1 cp"
|
||||
name = "Polyana-D4M1 C2 node"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
class _34Ya6E_Gazetchik_E_decoy(unittype.VehicleType):
|
||||
id = "34Ya6E Gazetchik E decoy"
|
||||
name = "34Ya6E Gazetchik E decoy"
|
||||
detection_range = 20000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
|
||||
|
||||
@ -43,25 +43,46 @@ MODDED_VEHICLES = [
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
highdigitsams.AAA_SON_9_Fire_Can,
|
||||
highdigitsams.AAA_100mm_KS_19,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_54K6_CP,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_5P85SE_LN,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_5P85SU_LN,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_30N6_TR,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_40B6M_TR,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_40B6MD_SR,
|
||||
highdigitsams.SAM_SA_10B_S_300PS_64H6E_SR,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_CP_54K6E2,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_TR_92H6E_truck,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_SR_64N6E2,
|
||||
highdigitsams.SAM_SA_20B_S_300PMU2_LN_5P85SE2,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S457_CP,
|
||||
highdigitsams.SAM_SA_12_S_300V_9A82_LN,
|
||||
highdigitsams.SAM_SA_12_S_300V_9A83_LN,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S15_SR,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S19_SR,
|
||||
highdigitsams.SAM_SA_12_S_300V_9S32_TR,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE,
|
||||
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN,
|
||||
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2,
|
||||
highdigitsams.SAM_SA_2__V759__LN_SM_90,
|
||||
highdigitsams.SAM_HQ_2_LN_SM_90,
|
||||
highdigitsams.SAM_SA_3__V_601P__LN_5P73,
|
||||
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6,
|
||||
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP,
|
||||
highdigitsams.SAM_SA_24_Igla_S_manpad,
|
||||
highdigitsams.SAM_SA_14_Strela_3_manpad
|
||||
highdigitsams.SAM_SA_14_Strela_3_manpad,
|
||||
highdigitsams.Polyana_D4M1_C2_node,
|
||||
highdigitsams._34Ya6E_Gazetchik_E_decoy
|
||||
]
|
||||
@ -60,7 +60,7 @@ class Dialog:
|
||||
"""Opens the dialog to edit the given flight."""
|
||||
cls.edit_flight_dialog = QEditFlightDialog(
|
||||
cls.game_model,
|
||||
package_model.package,
|
||||
package_model,
|
||||
flight,
|
||||
parent=parent
|
||||
)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""Visibility options for the game map."""
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Iterator, Optional, Union
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ from typing import Iterator, Optional, Union
|
||||
class DisplayRule:
|
||||
name: str
|
||||
_value: bool
|
||||
debug_only: bool = field(default=False)
|
||||
|
||||
@property
|
||||
def menu_text(self) -> str:
|
||||
@ -29,8 +30,9 @@ class DisplayRule:
|
||||
|
||||
|
||||
class DisplayGroup:
|
||||
def __init__(self, name: Optional[str]) -> None:
|
||||
def __init__(self, name: Optional[str], debug_only: bool = False) -> None:
|
||||
self.name = name
|
||||
self.debug_only = debug_only
|
||||
|
||||
def __iter__(self) -> Iterator[DisplayRule]:
|
||||
# Python 3.6 enforces that __dict__ is order preserving by default.
|
||||
@ -47,6 +49,46 @@ class FlightPathOptions(DisplayGroup):
|
||||
self.all = DisplayRule("Show All Flight Paths", True)
|
||||
|
||||
|
||||
class ThreatZoneOptions(DisplayGroup):
|
||||
def __init__(self, coalition_name: str) -> None:
|
||||
super().__init__(f"{coalition_name} Threat Zones")
|
||||
self.none = DisplayRule(
|
||||
f"Hide {coalition_name.lower()} threat zones", True)
|
||||
self.all = DisplayRule(
|
||||
f"Show full {coalition_name.lower()} threat zones", False)
|
||||
self.aircraft = DisplayRule(
|
||||
f"Show {coalition_name.lower()} aircraft threat tones", False)
|
||||
self.air_defenses = DisplayRule(
|
||||
f"Show {coalition_name.lower()} air defenses threat zones", False)
|
||||
|
||||
|
||||
class NavMeshOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Navmeshes", debug_only=True)
|
||||
self.hide = DisplayRule("DEBUG Hide Navmeshes", True)
|
||||
self.blue_navmesh = DisplayRule("DEBUG Show blue navmesh", False)
|
||||
self.red_navmesh = DisplayRule("DEBUG Show red navmesh", False)
|
||||
|
||||
|
||||
class PathDebugFactionOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Faction for path debugging", debug_only=True)
|
||||
self.blue = DisplayRule("Debug blue paths", True)
|
||||
self.red = DisplayRule("Debug red paths", False)
|
||||
|
||||
|
||||
class PathDebugOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Shortest paths", debug_only=True)
|
||||
self.hide = DisplayRule("DEBUG Hide paths", True)
|
||||
self.shortest_path = DisplayRule("DEBUG Show shortest path", False)
|
||||
self.barcap = DisplayRule("DEBUG Show BARCAP plan", False)
|
||||
self.cas = DisplayRule("DEBUG Show CAS plan", False)
|
||||
self.sweep = DisplayRule("DEBUG Show fighter sweep plan", False)
|
||||
self.strike = DisplayRule("DEBUG Show strike plan", False)
|
||||
self.tarcap = DisplayRule("DEBUG Show TARCAP plan", False)
|
||||
|
||||
|
||||
class DisplayOptions:
|
||||
ground_objects = DisplayRule("Ground Objects", True)
|
||||
control_points = DisplayRule("Control Points", True)
|
||||
@ -57,14 +99,27 @@ class DisplayOptions:
|
||||
map_poly = DisplayRule("Map Polygon Debug Mode", False)
|
||||
waypoint_info = DisplayRule("Waypoint Information", True)
|
||||
culling = DisplayRule("Display Culling Zones", False)
|
||||
actual_frontline_pos = DisplayRule("Display Actual Frontline Location",
|
||||
False)
|
||||
barcap_commit_range = DisplayRule("Display selected BARCAP commit range",
|
||||
False)
|
||||
flight_paths = FlightPathOptions()
|
||||
actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False)
|
||||
blue_threat_zones = ThreatZoneOptions("Blue")
|
||||
red_threat_zones = ThreatZoneOptions("Red")
|
||||
navmeshes = NavMeshOptions()
|
||||
path_debug_faction = PathDebugFactionOptions()
|
||||
path_debug = PathDebugOptions()
|
||||
|
||||
@classmethod
|
||||
def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:
|
||||
debug = False # Set to True to enable debug options.
|
||||
# Python 3.6 enforces that __dict__ is order preserving by default.
|
||||
for value in cls.__dict__.values():
|
||||
if isinstance(value, DisplayRule):
|
||||
if value.debug_only and not debug:
|
||||
continue
|
||||
yield value
|
||||
elif isinstance(value, DisplayGroup):
|
||||
if value.debug_only and not debug:
|
||||
continue
|
||||
yield value
|
||||
|
||||
@ -7,11 +7,18 @@ from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import dcs
|
||||
from dcs.weapons_data import weapon_ids
|
||||
|
||||
from PySide2 import QtWidgets
|
||||
from PySide2.QtGui import QPixmap
|
||||
from PySide2.QtWidgets import QApplication, QSplashScreen
|
||||
|
||||
from game import Game, db, persistency, VERSION
|
||||
from game.data.weapons import (
|
||||
WEAPON_FALLBACK_MAP,
|
||||
WEAPON_INTRODUCTION_YEARS,
|
||||
Weapon,
|
||||
)
|
||||
from game.settings import Settings
|
||||
from game.theater.start_generator import GameGenerator, GeneratorSettings
|
||||
from qt_ui import (
|
||||
@ -67,6 +74,8 @@ def run_ui(game: Optional[Game] = None) -> None:
|
||||
uiconstants.load_event_icons()
|
||||
uiconstants.load_aircraft_icons()
|
||||
uiconstants.load_vehicle_icons()
|
||||
uiconstants.load_aircraft_banners()
|
||||
uiconstants.load_vehicle_banners()
|
||||
|
||||
# Replace DCS Mission scripting file to allow DCS Liberation to work
|
||||
try:
|
||||
@ -103,6 +112,11 @@ def parse_args() -> argparse.Namespace:
|
||||
raise argparse.ArgumentTypeError("path does not exist")
|
||||
return path
|
||||
|
||||
parser.add_argument(
|
||||
"--warn-missing-weapon-data", action="store_true",
|
||||
help="Emits a warning for weapons without date or fallback information."
|
||||
)
|
||||
|
||||
new_game = subparsers.add_parser("new-game")
|
||||
|
||||
new_game.add_argument(
|
||||
@ -163,6 +177,15 @@ def create_game(campaign_path: Path, blue: str, red: str,
|
||||
return generator.generate()
|
||||
|
||||
|
||||
def lint_weapon_data() -> None:
|
||||
for clsid in weapon_ids:
|
||||
weapon = Weapon.from_clsid(clsid)
|
||||
if weapon not in WEAPON_INTRODUCTION_YEARS:
|
||||
logging.warning(f"{weapon} has no introduction date")
|
||||
if weapon not in WEAPON_FALLBACK_MAP:
|
||||
logging.warning(f"{weapon} has no fallback")
|
||||
|
||||
|
||||
def main():
|
||||
logging_config.init_logging(VERSION)
|
||||
|
||||
@ -172,6 +195,11 @@ def main():
|
||||
game: Optional[Game] = None
|
||||
|
||||
args = parse_args()
|
||||
|
||||
# TODO: Flesh out data and then make unconditional.
|
||||
if args.warn_missing_weapon_data:
|
||||
lint_weapon_data()
|
||||
|
||||
if args.subcommand == "new-game":
|
||||
game = create_game(args.campaign, args.blue, args.red,
|
||||
args.supercarrier, args.auto_procurement,
|
||||
|
||||
@ -100,6 +100,8 @@ class PackageModel(QAbstractListModel):
|
||||
#: Emitted when this package is being deleted from the ATO.
|
||||
deleted = Signal()
|
||||
|
||||
tot_changed = Signal()
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
super().__init__()
|
||||
self.package = package
|
||||
@ -139,6 +141,8 @@ class PackageModel(QAbstractListModel):
|
||||
"""Adds the given flight to the package."""
|
||||
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
|
||||
self.package.add_flight(flight)
|
||||
# update_tot is not called here because the new flight does not have a
|
||||
# flight plan yet. Will be called manually by the caller.
|
||||
self.endInsertRows()
|
||||
|
||||
def delete_flight_at_index(self, index: QModelIndex) -> None:
|
||||
@ -155,15 +159,27 @@ class PackageModel(QAbstractListModel):
|
||||
self.beginRemoveRows(QModelIndex(), index, index)
|
||||
self.package.remove_flight(flight)
|
||||
self.endRemoveRows()
|
||||
self.update_tot()
|
||||
|
||||
def flight_at_index(self, index: QModelIndex) -> Flight:
|
||||
"""Returns the flight located at the given index."""
|
||||
return self.package.flights[index.row()]
|
||||
|
||||
def update_tot(self, tot: datetime.timedelta) -> None:
|
||||
def set_tot(self, tot: datetime.timedelta) -> None:
|
||||
self.package.time_over_target = tot
|
||||
self.update_tot()
|
||||
# For some reason this is needed to make the UI update quickly.
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def set_asap(self, asap: bool) -> None:
|
||||
self.package.auto_asap = asap
|
||||
self.update_tot()
|
||||
|
||||
def update_tot(self) -> None:
|
||||
if self.package.auto_asap:
|
||||
self.package.set_tot_asap()
|
||||
self.tot_changed.emit()
|
||||
|
||||
@property
|
||||
def mission_target(self) -> MissionTarget:
|
||||
"""Returns the mission target of the package."""
|
||||
|
||||
@ -72,7 +72,9 @@ COLORS: Dict[str, QColor] = {
|
||||
|
||||
CP_SIZE = 12
|
||||
|
||||
AIRCRAFT_BANNERS: Dict[str, QPixmap] = {}
|
||||
AIRCRAFT_ICONS: Dict[str, QPixmap] = {}
|
||||
VEHICLE_BANNERS: Dict[str, QPixmap] = {}
|
||||
VEHICLES_ICONS: Dict[str, QPixmap] = {}
|
||||
ICONS: Dict[str, QPixmap] = {}
|
||||
|
||||
@ -130,6 +132,8 @@ def load_icons():
|
||||
ICONS["ship_blue"] = QPixmap("./resources/ui/ground_assets/ship_blue.png")
|
||||
ICONS["missile"] = QPixmap("./resources/ui/ground_assets/missile.png")
|
||||
ICONS["missile_blue"] = QPixmap("./resources/ui/ground_assets/missile_blue.png")
|
||||
ICONS["nothreat"] = QPixmap("./resources/ui/ground_assets/nothreat.png")
|
||||
ICONS["nothreat_blue"] = QPixmap("./resources/ui/ground_assets/nothreat_blue.png")
|
||||
|
||||
ICONS["Generator"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/generator.png")
|
||||
ICONS["Missile"] = QPixmap("./resources/ui/misc/"+get_theme_icons()+"/missile.png")
|
||||
@ -171,15 +175,25 @@ def load_event_icons():
|
||||
EVENT_ICONS[image[:-4]] = QPixmap(os.path.join("./resources/ui/events/", image))
|
||||
|
||||
def load_aircraft_icons():
|
||||
for aircraft in os.listdir("./resources/ui/units/aircrafts/"):
|
||||
for aircraft in os.listdir("./resources/ui/units/aircrafts/icons/"):
|
||||
if aircraft.endswith(".jpg"):
|
||||
AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/", aircraft))
|
||||
AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/icons/", aircraft))
|
||||
AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"]
|
||||
AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"]
|
||||
AIRCRAFT_ICONS["A-10C_2"] = AIRCRAFT_ICONS["A-10C"]
|
||||
|
||||
|
||||
def load_vehicle_icons():
|
||||
for vehicle in os.listdir("./resources/ui/units/vehicles/"):
|
||||
for vehicle in os.listdir("./resources/ui/units/vehicles/icons/"):
|
||||
if vehicle.endswith(".jpg"):
|
||||
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/", vehicle))
|
||||
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/icons/", vehicle))
|
||||
|
||||
def load_aircraft_banners():
|
||||
for aircraft in os.listdir("./resources/ui/units/aircrafts/banners/"):
|
||||
if aircraft.endswith(".jpg"):
|
||||
AIRCRAFT_BANNERS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/banners/", aircraft))
|
||||
|
||||
def load_vehicle_banners():
|
||||
for aircraft in os.listdir("./resources/ui/units/vehicles/banners/"):
|
||||
if aircraft.endswith(".jpg"):
|
||||
VEHICLE_BANNERS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/banners/", aircraft))
|
||||
@ -35,7 +35,7 @@ class QBudgetBox(QGroupBox):
|
||||
:param budget: Current money available
|
||||
:param reward: Planned reward for next turn
|
||||
"""
|
||||
self.money_amount.setText(str(budget) + "M (+" + str(reward) + "M)")
|
||||
self.money_amount.setText(str(budget) + "M (+" + str(round(reward,2)) + "M)")
|
||||
|
||||
def setGame(self, game):
|
||||
if game is None:
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout, QFrame, QGridLayout
|
||||
from PySide2.QtGui import QPixmap
|
||||
|
||||
from game.weather import Conditions, TimeOfDay, Weather
|
||||
from game.utils import meter_to_nm, mps_to_knots
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from dcs.weather import Weather as PydcsWeather
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.utils import mps
|
||||
from game.weather import Conditions, TimeOfDay
|
||||
|
||||
|
||||
class QTimeTurnWidget(QGroupBox):
|
||||
"""
|
||||
@ -163,20 +169,20 @@ class QWeatherWidget(QGroupBox):
|
||||
def updateWinds(self):
|
||||
"""Updates the UI with the current conditions wind info.
|
||||
"""
|
||||
windGlSpeed = mps_to_knots(self.conditions.weather.wind.at_0m.speed or 0)
|
||||
windGlSpeed = mps(self.conditions.weather.wind.at_0m.speed or 0)
|
||||
windGlDir = str(self.conditions.weather.wind.at_0m.direction or 0).rjust(3, '0')
|
||||
self.windGLSpeedLabel.setText('{}kts'.format(windGlSpeed))
|
||||
self.windGLDirLabel.setText('{}º'.format(windGlDir))
|
||||
self.windGLSpeedLabel.setText(f'{int(windGlSpeed.knots)}kts')
|
||||
self.windGLDirLabel.setText(f'{windGlDir}º')
|
||||
|
||||
windFL08Speed = mps_to_knots(self.conditions.weather.wind.at_2000m.speed or 0)
|
||||
windFL08Speed = mps(self.conditions.weather.wind.at_2000m.speed or 0)
|
||||
windFL08Dir = str(self.conditions.weather.wind.at_2000m.direction or 0).rjust(3, '0')
|
||||
self.windFL08SpeedLabel.setText('{}kts'.format(windFL08Speed))
|
||||
self.windFL08DirLabel.setText('{}º'.format(windFL08Dir))
|
||||
self.windFL08SpeedLabel.setText(f'{int(windFL08Speed.knots)}kts')
|
||||
self.windFL08DirLabel.setText(f'{windFL08Dir}º')
|
||||
|
||||
windFL26Speed = mps_to_knots(self.conditions.weather.wind.at_8000m.speed or 0)
|
||||
windFL26Speed = mps(self.conditions.weather.wind.at_8000m.speed or 0)
|
||||
windFL26Dir = str(self.conditions.weather.wind.at_8000m.direction or 0).rjust(3, '0')
|
||||
self.windFL26SpeedLabel.setText('{}kts'.format(windFL26Speed))
|
||||
self.windFL26DirLabel.setText('{}º'.format(windFL26Dir))
|
||||
self.windFL26SpeedLabel.setText(f'{int(windFL26Speed.knots)}kts')
|
||||
self.windFL26DirLabel.setText(f'{windFL26Dir}º')
|
||||
|
||||
def updateForecast(self):
|
||||
"""Updates the Forecast Text and icon with the current conditions wind info.
|
||||
@ -223,11 +229,10 @@ class QWeatherWidget(QGroupBox):
|
||||
if not fog:
|
||||
self.forecastFog.setText('No fog')
|
||||
else:
|
||||
visvibilityNm = round(meter_to_nm(fog.visibility), 1)
|
||||
self.forecastFog.setText('Fog vis: {}nm'.format(visvibilityNm))
|
||||
visibility = round(fog.visibility.nautical_miles, 1)
|
||||
self.forecastFog.setText(f'Fog vis: {visibility}nm')
|
||||
icon = [time, ('cloudy' if cloudDensity > 1 else None), 'fog']
|
||||
|
||||
|
||||
icon_key = "Weather_{}".format('-'.join(filter(None.__ne__, icon)))
|
||||
icon = CONST.ICONS.get(icon_key) or CONST.ICONS['Weather_night-partly-cloudy']
|
||||
self.weather_icon.setPixmap(icon)
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import logging
|
||||
import timeit
|
||||
from datetime import timedelta
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
@ -130,9 +133,12 @@ class QTopPanel(QFrame):
|
||||
self.subwindow.show()
|
||||
|
||||
def passTurn(self):
|
||||
start = timeit.default_timer()
|
||||
self.game.pass_turn(no_action=True)
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
self.proceedButton.setEnabled(True)
|
||||
end = timeit.default_timer()
|
||||
logging.info("Skipping turn took %s", timedelta(seconds=end - start))
|
||||
|
||||
def negative_start_packages(self) -> List[Package]:
|
||||
packages = []
|
||||
|
||||
@ -60,16 +60,15 @@ class FlightDelegate(QStyledItemDelegate):
|
||||
|
||||
def first_row_text(self, index: QModelIndex) -> str:
|
||||
flight = self.flight(index)
|
||||
task = flight.flight_type
|
||||
count = flight.count
|
||||
name = db.unit_type_name(flight.unit_type)
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = estimator.mission_start_time(flight)
|
||||
return f"[{task}] {count} x {name} in {delay}"
|
||||
return f"{flight} in {delay}"
|
||||
|
||||
def second_row_text(self, index: QModelIndex) -> str:
|
||||
flight = self.flight(index)
|
||||
origin = flight.from_cp.name
|
||||
if flight.arrival != flight.departure:
|
||||
return f"From {origin} to {flight.arrival.name}"
|
||||
return f"From {origin}"
|
||||
|
||||
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
|
||||
|
||||
@ -5,12 +5,50 @@ from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
import gen.flights.ai_flight_planner_db
|
||||
|
||||
from game import Game, db
|
||||
|
||||
class QAircraftTypeSelector(QComboBox):
|
||||
"""Combo box for selecting among the given aircraft types."""
|
||||
|
||||
def __init__(self, aircraft_types: Iterable[Type[FlyingType]]) -> None:
|
||||
def __init__(self, aircraft_types: Iterable[Type[FlyingType]], country: str, mission_type: str) -> None:
|
||||
super().__init__()
|
||||
for aircraft in aircraft_types:
|
||||
self.addItem(f"{aircraft.id}", userData=aircraft)
|
||||
|
||||
self.model().sort(0)
|
||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||
self.country = country
|
||||
self.updateItems(mission_type, aircraft_types)
|
||||
|
||||
def updateItems(self, mission_type: str, aircraft_types):
|
||||
current_aircraft = self.currentData()
|
||||
self.clear()
|
||||
for aircraft in aircraft_types:
|
||||
if mission_type in [FlightType.BARCAP, FlightType.ESCORT, FlightType.INTERCEPTION, FlightType.SWEEP, FlightType.TARCAP]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.CAP_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.CAS, FlightType.BAI, FlightType.OCA_AIRCRAFT]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.CAS_CAPABLE or aircraft in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.SEAD]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.SEAD_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.DEAD]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.DEAD_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.STRIKE]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE or aircraft in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.ANTISHIP]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.ANTISHIP_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
elif mission_type in [FlightType.OCA_RUNWAY]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.RUNWAY_ATTACK_CAPABLE:
|
||||
self.addItem(f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}", userData=aircraft)
|
||||
current_aircraft_index = self.findData(current_aircraft)
|
||||
if current_aircraft_index != -1:
|
||||
self.setCurrentIndex(current_aircraft_index)
|
||||
if self.count() == 0:
|
||||
self.addItem("No capable aircraft available", userData=None)
|
||||
@ -27,6 +27,7 @@ class QOriginAirfieldSelector(QComboBox):
|
||||
self.aircraft = aircraft
|
||||
self.rebuild_selector()
|
||||
self.currentIndexChanged.connect(self.index_changed)
|
||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||
|
||||
def change_aircraft(self, aircraft: FlyingType) -> None:
|
||||
if self.aircraft == aircraft:
|
||||
|
||||
@ -2,6 +2,7 @@ from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
from game import Game
|
||||
from game.theater import ControlPointType
|
||||
from game.utils import Distance
|
||||
from gen import BuildingGroundObject, Conflict, FlightWaypointType
|
||||
from gen.flights.flight import FlightWaypoint
|
||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||
@ -59,7 +60,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
FlightWaypointType.CUSTOM,
|
||||
pos.x,
|
||||
pos.y,
|
||||
800)
|
||||
Distance.from_meters(800))
|
||||
wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]"
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.pretty_name = wpt.name
|
||||
@ -70,12 +71,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
|
||||
for ground_object in cp.ground_objects:
|
||||
if not ground_object.is_dead and not isinstance(ground_object, BuildingGroundObject):
|
||||
if not ground_object.is_dead and isinstance(ground_object, BuildingGroundObject):
|
||||
wpt = FlightWaypoint(
|
||||
FlightWaypointType.CUSTOM,
|
||||
ground_object.position.x,
|
||||
ground_object.position.y,
|
||||
0
|
||||
Distance.from_meters(0)
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.name = ground_object.waypoint_name
|
||||
@ -99,7 +100,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
FlightWaypointType.CUSTOM,
|
||||
u.position.x,
|
||||
u.position.y,
|
||||
0
|
||||
Distance.from_meters(0)
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
|
||||
@ -120,7 +121,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
||||
FlightWaypointType.CUSTOM,
|
||||
cp.position.x,
|
||||
cp.position.y,
|
||||
0
|
||||
Distance.from_meters(0)
|
||||
)
|
||||
wpt.alt_type = "RADIO"
|
||||
wpt.name = cp.name
|
||||
|
||||
@ -58,13 +58,14 @@ class QFrontLine(QGraphicsLineItem):
|
||||
new_package_action.triggered.connect(self.open_new_package_dialog)
|
||||
menu.addAction(new_package_action)
|
||||
|
||||
cheat_forward = QAction(f"CHEAT: Advance Frontline")
|
||||
cheat_forward.triggered.connect(self.cheat_forward)
|
||||
menu.addAction(cheat_forward)
|
||||
if self.game_model.game.settings.enable_frontline_cheats:
|
||||
cheat_forward = QAction(f"CHEAT: Advance Frontline")
|
||||
cheat_forward.triggered.connect(self.cheat_forward)
|
||||
menu.addAction(cheat_forward)
|
||||
|
||||
cheat_backward = QAction(f"CHEAT: Retreat Frontline")
|
||||
cheat_backward.triggered.connect(self.cheat_backward)
|
||||
menu.addAction(cheat_backward)
|
||||
cheat_backward = QAction(f"CHEAT: Retreat Frontline")
|
||||
cheat_backward.triggered.connect(self.cheat_backward)
|
||||
menu.addAction(cheat_backward)
|
||||
|
||||
menu.exec_(event.screenPos())
|
||||
|
||||
|
||||
@ -3,10 +3,11 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
from functools import singledispatchmethod
|
||||
from typing import Iterable, Iterator, List, Optional, Tuple
|
||||
|
||||
from PySide2 import QtWidgets, QtCore
|
||||
from PySide2.QtCore import QPointF, Qt, QLineF, QRectF
|
||||
from PySide2 import QtCore, QtWidgets
|
||||
from PySide2.QtCore import QLineF, QPointF, QRectF, Qt
|
||||
from PySide2.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
@ -14,30 +15,52 @@ from PySide2.QtGui import (
|
||||
QPen,
|
||||
QPixmap,
|
||||
QPolygonF,
|
||||
QWheelEvent, )
|
||||
QWheelEvent,
|
||||
)
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGraphicsItem,
|
||||
QGraphicsOpacityEffect,
|
||||
QGraphicsScene,
|
||||
QGraphicsView, QGraphicsSceneMouseEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QGraphicsView,
|
||||
)
|
||||
from dcs import Point
|
||||
from dcs.planes import F_16C_50
|
||||
from dcs.mapping import point_from_heading
|
||||
from dcs.unitgroup import Group
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
MultiPolygon,
|
||||
Point as ShapelyPoint,
|
||||
Polygon,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game import Game, db
|
||||
from game import Game
|
||||
from game.navmesh import NavMesh
|
||||
from game.theater import ControlPoint, Enum
|
||||
from game.theater.conflicttheater import FrontLine, ReferencePoint
|
||||
from game.theater.theatergroundobject import (
|
||||
TheaterGroundObject,
|
||||
)
|
||||
from game.utils import meter_to_feet, nm_to_meter, meter_to_nm
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.weather import TimeOfDay
|
||||
from gen import Conflict
|
||||
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
from gen.flights.flightplan import FlightPlan
|
||||
from qt_ui.displayoptions import DisplayOptions
|
||||
from gen import Conflict, Package
|
||||
from gen.flights.flight import (
|
||||
Flight,
|
||||
FlightType,
|
||||
FlightWaypoint,
|
||||
FlightWaypointType,
|
||||
)
|
||||
from gen.flights.flightplan import (
|
||||
BarCapFlightPlan,
|
||||
FlightPlan,
|
||||
FlightPlanBuilder,
|
||||
InvalidObjectiveLocation,
|
||||
)
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.displayoptions import DisplayOptions, ThreatZoneOptions
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.widgets.map.QFrontLine import QFrontLine
|
||||
from qt_ui.widgets.map.QLiberationScene import QLiberationScene
|
||||
@ -45,7 +68,7 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
|
||||
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
|
||||
MAX_SHIP_DISTANCE = 80
|
||||
MAX_SHIP_DISTANCE = nautical_miles(80)
|
||||
|
||||
def binomial(i: int, n: int) -> float:
|
||||
"""Binomial coefficient"""
|
||||
@ -157,6 +180,9 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
self.nm_to_pixel_ratio: int = 0
|
||||
|
||||
self.navmesh_highlight: Optional[QPolygonF] = None
|
||||
self.shortest_path_segments: List[QLineF] = []
|
||||
|
||||
|
||||
def init_scene(self):
|
||||
scene = QLiberationScene(self)
|
||||
@ -171,7 +197,7 @@ class QLiberationMap(QGraphicsView):
|
||||
self.game = game
|
||||
if self.game is not None:
|
||||
logging.debug("Reloading Map Canvas")
|
||||
self.nm_to_pixel_ratio = self.km_to_pixel(float(nm_to_meter(1)) / 1000.0)
|
||||
self.nm_to_pixel_ratio = self.distance_to_pixels(nautical_miles(1))
|
||||
self.reload_scene()
|
||||
|
||||
"""
|
||||
@ -239,29 +265,6 @@ class QLiberationMap(QGraphicsView):
|
||||
def update_reference_point(point: ReferencePoint, change: Point) -> None:
|
||||
point.image_coordinates += change
|
||||
|
||||
@staticmethod
|
||||
def aa_ranges(ground_object: TheaterGroundObject) -> Tuple[int, int]:
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
for g in ground_object.groups:
|
||||
for u in g.units:
|
||||
unit = db.unit_type_from_name(u.type)
|
||||
if unit is None:
|
||||
logging.error(f"Unknown unit type {u.type}")
|
||||
continue
|
||||
|
||||
# Some units in pydcs have detection_range and threat_range
|
||||
# defined, but explicitly set to None.
|
||||
unit_detection_range = getattr(unit, "detection_range", None)
|
||||
if unit_detection_range is not None:
|
||||
detection_range = max(detection_range, unit_detection_range)
|
||||
|
||||
unit_threat_range = getattr(unit, "threat_range", None)
|
||||
if unit_threat_range is not None:
|
||||
threat_range = max(threat_range, unit_threat_range)
|
||||
|
||||
return detection_range, threat_range
|
||||
|
||||
def display_culling(self, scene: QGraphicsScene) -> None:
|
||||
"""Draws the culling distance rings on the map"""
|
||||
culling_points = self.game_model.game.get_culling_points()
|
||||
@ -273,19 +276,186 @@ class QLiberationMap(QGraphicsView):
|
||||
radius = distance_point[0] - transformed[0]
|
||||
scene.addEllipse(transformed[0]-radius, transformed[1]-radius, 2*radius, 2*radius, CONST.COLORS["transparent"], CONST.COLORS["light_green_transparent"])
|
||||
|
||||
def draw_shapely_poly(self, scene: QGraphicsScene, poly: Polygon, pen: QPen,
|
||||
brush: QBrush) -> Optional[QPolygonF]:
|
||||
if poly.is_empty:
|
||||
return None
|
||||
points = []
|
||||
for x, y in poly.exterior.coords:
|
||||
x, y = self._transform_point(Point(x, y))
|
||||
points.append(QPointF(x, y))
|
||||
return scene.addPolygon(QPolygonF(points), pen, brush)
|
||||
|
||||
def draw_threat_zone(self, scene: QGraphicsScene, poly: Polygon,
|
||||
player: bool) -> None:
|
||||
if player:
|
||||
brush = QColor(0, 132, 255, 100)
|
||||
else:
|
||||
brush = QColor(227, 32, 0, 100)
|
||||
self.draw_shapely_poly(scene, poly, CONST.COLORS["transparent"], brush)
|
||||
|
||||
def display_threat_zones(self, scene: QGraphicsScene,
|
||||
options: ThreatZoneOptions, player: bool) -> None:
|
||||
"""Draws the threat zones on the map."""
|
||||
threat_zones = self.game.threat_zone_for(player)
|
||||
if options.all:
|
||||
threat_poly = threat_zones.all
|
||||
elif options.aircraft:
|
||||
threat_poly = threat_zones.airbases
|
||||
elif options.air_defenses:
|
||||
threat_poly = threat_zones.air_defenses
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(threat_poly, MultiPolygon):
|
||||
polys = threat_poly.geoms
|
||||
else:
|
||||
polys = [threat_poly]
|
||||
for poly in polys:
|
||||
self.draw_threat_zone(scene, poly, player)
|
||||
|
||||
def draw_navmesh_neighbor_line(self, scene: QGraphicsScene, poly: Polygon,
|
||||
begin: ShapelyPoint) -> None:
|
||||
vertex = Point(begin.x, begin.y)
|
||||
centroid = poly.centroid
|
||||
direction = Point(centroid.x, centroid.y)
|
||||
end = vertex.point_from_heading(vertex.heading_between_point(direction),
|
||||
nautical_miles(2).meters)
|
||||
|
||||
scene.addLine(QLineF(QPointF(*self._transform_point(vertex)),
|
||||
QPointF(*self._transform_point(end))),
|
||||
CONST.COLORS["yellow"])
|
||||
|
||||
@singledispatchmethod
|
||||
def draw_navmesh_border(self, intersection, scene: QGraphicsScene,
|
||||
poly: Polygon) -> None:
|
||||
raise NotImplementedError("draw_navmesh_border not implemented for %s",
|
||||
intersection.__class__.__name__)
|
||||
|
||||
@draw_navmesh_border.register
|
||||
def draw_navmesh_point_border(self, intersection: ShapelyPoint,
|
||||
scene: QGraphicsScene, poly: Polygon) -> None:
|
||||
# Draw a line from the vertex toward the center of the polygon.
|
||||
self.draw_navmesh_neighbor_line(scene, poly, intersection)
|
||||
|
||||
@draw_navmesh_border.register
|
||||
def draw_navmesh_edge_border(self, intersection: LineString,
|
||||
scene: QGraphicsScene, poly: Polygon) -> None:
|
||||
# Draw a line from the center of the edge toward the center of the
|
||||
# polygon.
|
||||
edge_center = intersection.interpolate(0.5, normalized=True)
|
||||
self.draw_navmesh_neighbor_line(scene, poly, edge_center)
|
||||
|
||||
def display_navmesh(self, scene: QGraphicsScene, player: bool) -> None:
|
||||
for navpoly in self.game.navmesh_for(player).polys:
|
||||
self.draw_shapely_poly(scene, navpoly.poly, CONST.COLORS["black"],
|
||||
CONST.COLORS["transparent"])
|
||||
|
||||
position = self._transform_point(
|
||||
Point(navpoly.poly.centroid.x, navpoly.poly.centroid.y))
|
||||
text = scene.addSimpleText(f"Navmesh {navpoly.ident}",
|
||||
self.waypoint_info_font)
|
||||
text.setBrush(QColor(255, 255, 255))
|
||||
text.setPen(QColor(255, 255, 255))
|
||||
text.moveBy(position[0] + 8, position[1])
|
||||
text.setZValue(2)
|
||||
|
||||
for border in navpoly.neighbors.values():
|
||||
self.draw_navmesh_border(border, scene, navpoly.poly)
|
||||
|
||||
def highlight_mouse_navmesh(self, scene: QGraphicsScene, navmesh: NavMesh,
|
||||
mouse_position: Point) -> None:
|
||||
if self.navmesh_highlight is not None:
|
||||
try:
|
||||
scene.removeItem(self.navmesh_highlight)
|
||||
except RuntimeError:
|
||||
pass
|
||||
navpoly = navmesh.localize(mouse_position)
|
||||
if navpoly is None:
|
||||
return
|
||||
self.navmesh_highlight = self.draw_shapely_poly(
|
||||
scene, navpoly.poly, CONST.COLORS["transparent"],
|
||||
CONST.COLORS["light_green_transparent"])
|
||||
|
||||
def draw_shortest_path(self, scene: QGraphicsScene, navmesh: NavMesh,
|
||||
destination: Point, player: bool) -> None:
|
||||
for line in self.shortest_path_segments:
|
||||
try:
|
||||
scene.removeItem(line)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
if player:
|
||||
origin = self.game.theater.player_points()[0]
|
||||
else:
|
||||
origin = self.game.theater.enemy_points()[0]
|
||||
|
||||
prev_pos = self._transform_point(origin.position)
|
||||
try:
|
||||
path = navmesh.shortest_path(origin.position, destination)
|
||||
except ValueError:
|
||||
return
|
||||
for waypoint in path[1:]:
|
||||
new_pos = self._transform_point(waypoint)
|
||||
flight_path_pen = self.flight_path_pen(player, selected=True)
|
||||
# Draw the line to the *middle* of the waypoint.
|
||||
offset = self.WAYPOINT_SIZE // 2
|
||||
self.shortest_path_segments.append(scene.addLine(
|
||||
prev_pos[0] + offset, prev_pos[1] + offset,
|
||||
new_pos[0] + offset, new_pos[1] + offset,
|
||||
flight_path_pen
|
||||
))
|
||||
|
||||
self.shortest_path_segments.append(scene.addEllipse(
|
||||
new_pos[0], new_pos[1], self.WAYPOINT_SIZE,
|
||||
self.WAYPOINT_SIZE, flight_path_pen, flight_path_pen
|
||||
))
|
||||
|
||||
prev_pos = new_pos
|
||||
|
||||
def draw_test_flight_plan(self, scene: QGraphicsScene, task: FlightType,
|
||||
point_near_target: Point, player: bool) -> None:
|
||||
for line in self.shortest_path_segments:
|
||||
try:
|
||||
scene.removeItem(line)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
self.clear_flight_paths(scene)
|
||||
|
||||
target = self.game.theater.closest_target(point_near_target)
|
||||
|
||||
if player:
|
||||
origin = self.game.theater.player_points()[0]
|
||||
else:
|
||||
origin = self.game.theater.enemy_points()[0]
|
||||
|
||||
package = Package(target)
|
||||
flight = Flight(package, F_16C_50, 2, task, start_type="Warm",
|
||||
departure=origin, arrival=origin, divert=None)
|
||||
package.add_flight(flight)
|
||||
planner = FlightPlanBuilder(self.game, package, is_player=player)
|
||||
try:
|
||||
planner.populate_flight_plan(flight)
|
||||
except InvalidObjectiveLocation:
|
||||
return
|
||||
|
||||
package.time_over_target = TotEstimator(package).earliest_tot()
|
||||
self.draw_flight_plan(scene, flight, selected=True)
|
||||
|
||||
@staticmethod
|
||||
def should_display_ground_objects_at(cp: ControlPoint) -> bool:
|
||||
return ((DisplayOptions.sam_ranges and cp.captured) or
|
||||
(DisplayOptions.enemy_sam_ranges and not cp.captured))
|
||||
|
||||
def draw_threat_range(self, scene: QGraphicsScene, ground_object: TheaterGroundObject, cp: ControlPoint) -> None:
|
||||
def draw_threat_range(self, scene: QGraphicsScene, group: Group, ground_object: TheaterGroundObject, cp: ControlPoint) -> None:
|
||||
go_pos = self._transform_point(ground_object.position)
|
||||
detection_range, threat_range = self.aa_ranges(
|
||||
ground_object
|
||||
)
|
||||
detection_range = ground_object.detection_range(group)
|
||||
threat_range = ground_object.threat_range(group)
|
||||
if threat_range:
|
||||
threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
|
||||
ground_object.position.y+threat_range))
|
||||
threat_pos = self._transform_point(
|
||||
ground_object.position + Point(threat_range.meters,
|
||||
threat_range.meters))
|
||||
threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
|
||||
|
||||
# Add threat range circle
|
||||
@ -294,8 +464,9 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
if detection_range and DisplayOptions.detection_range:
|
||||
# Add detection range circle
|
||||
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
|
||||
ground_object.position.y+detection_range))
|
||||
detection_pos = self._transform_point(
|
||||
ground_object.position + Point(detection_range.meters,
|
||||
detection_range.meters))
|
||||
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
|
||||
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
|
||||
detection_radius, detection_radius, self.detection_pen(cp.captured))
|
||||
@ -313,7 +484,8 @@ class QLiberationMap(QGraphicsView):
|
||||
|
||||
should_display = self.should_display_ground_objects_at(cp)
|
||||
if ground_object.might_have_aa and should_display:
|
||||
self.draw_threat_range(scene, ground_object, cp)
|
||||
for group in ground_object.groups:
|
||||
self.draw_threat_range(scene, group, ground_object, cp)
|
||||
added_objects.append(ground_object.obj_name)
|
||||
|
||||
def reload_scene(self):
|
||||
@ -329,6 +501,16 @@ class QLiberationMap(QGraphicsView):
|
||||
if DisplayOptions.culling and self.game.settings.perf_culling:
|
||||
self.display_culling(scene)
|
||||
|
||||
self.display_threat_zones(scene, DisplayOptions.blue_threat_zones,
|
||||
player=True)
|
||||
self.display_threat_zones(scene, DisplayOptions.red_threat_zones,
|
||||
player=False)
|
||||
|
||||
if DisplayOptions.navmeshes.blue_navmesh:
|
||||
self.display_navmesh(scene, player=True)
|
||||
if DisplayOptions.navmeshes.red_navmesh:
|
||||
self.display_navmesh(scene, player=False)
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
|
||||
pos = self._transform_point(cp.position)
|
||||
@ -428,8 +610,30 @@ class QLiberationMap(QGraphicsView):
|
||||
flight.flight_plan)
|
||||
prev_pos = tuple(new_pos)
|
||||
|
||||
def draw_waypoint(self, scene: QGraphicsScene, position: Tuple[int, int],
|
||||
player: bool, selected: bool) -> None:
|
||||
if selected and DisplayOptions.barcap_commit_range:
|
||||
self.draw_barcap_commit_range(scene, flight)
|
||||
|
||||
def draw_barcap_commit_range(self, scene: QGraphicsScene,
|
||||
flight: Flight) -> None:
|
||||
if flight.flight_type is not FlightType.BARCAP:
|
||||
return
|
||||
if not isinstance(flight.flight_plan, BarCapFlightPlan):
|
||||
return
|
||||
start = flight.flight_plan.patrol_start
|
||||
end = flight.flight_plan.patrol_end
|
||||
line = LineString([
|
||||
ShapelyPoint(start.x, start.y),
|
||||
ShapelyPoint(end.x, end.y),
|
||||
])
|
||||
doctrine = self.game.faction_for(flight.departure.captured).doctrine
|
||||
bubble = line.buffer(doctrine.cap_engagement_range.meters)
|
||||
self.flight_path_items.append(self.draw_shapely_poly(
|
||||
scene, bubble, CONST.COLORS["yellow"], CONST.COLORS["transparent"]
|
||||
))
|
||||
|
||||
def draw_waypoint(self, scene: QGraphicsScene,
|
||||
position: Tuple[float, float], player: bool,
|
||||
selected: bool) -> None:
|
||||
waypoint_pen = self.waypoint_pen(player, selected)
|
||||
waypoint_brush = self.waypoint_brush(player, selected)
|
||||
self.flight_path_items.append(scene.addEllipse(
|
||||
@ -441,7 +645,7 @@ class QLiberationMap(QGraphicsView):
|
||||
waypoint: FlightWaypoint, position: Tuple[int, int],
|
||||
flight_plan: FlightPlan) -> None:
|
||||
|
||||
altitude = meter_to_feet(waypoint.alt)
|
||||
altitude = int(waypoint.alt.feet)
|
||||
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
|
||||
|
||||
prefix = "TOT"
|
||||
@ -472,8 +676,8 @@ class QLiberationMap(QGraphicsView):
|
||||
item.setZValue(2)
|
||||
self.flight_path_items.append(item)
|
||||
|
||||
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[int, int],
|
||||
pos1: Tuple[int, int], player: bool,
|
||||
def draw_flight_path(self, scene: QGraphicsScene, pos0: Tuple[float, float],
|
||||
pos1: Tuple[float, float], player: bool,
|
||||
selected: bool) -> None:
|
||||
flight_path_pen = self.flight_path_pen(player, selected)
|
||||
# Draw the line to the *middle* of the waypoint.
|
||||
@ -567,7 +771,7 @@ class QLiberationMap(QGraphicsView):
|
||||
BIG_LINE = 5
|
||||
SMALL_LINE = 2
|
||||
|
||||
dist = self.km_to_pixel(nm_to_meter(scale_distance_nm)/1000.0)
|
||||
dist = self.distance_to_pixels(nautical_miles(scale_distance_nm))
|
||||
self.scene().addRect(POS_X, POS_Y-PADDING, PADDING*2 + dist, BIG_LINE*2+3*PADDING, pen=CONST.COLORS["black"], brush=CONST.COLORS["black"])
|
||||
l = self.scene().addLine(POS_X + PADDING, POS_Y + BIG_LINE*2, POS_X + PADDING + dist, POS_Y + BIG_LINE*2)
|
||||
|
||||
@ -663,12 +867,12 @@ class QLiberationMap(QGraphicsView):
|
||||
Point(offset.x / scale.x, offset.y / scale.y))
|
||||
return point_a.world_coordinates - scaled
|
||||
|
||||
def km_to_pixel(self, km):
|
||||
def distance_to_pixels(self, distance: Distance) -> int:
|
||||
p1 = Point(0, 0)
|
||||
p2 = Point(0, 1000*km)
|
||||
p2 = Point(0, distance.meters)
|
||||
p1a = Point(*self._transform_point(p1))
|
||||
p2a = Point(*self._transform_point(p2))
|
||||
return p1a.distance_to_point(p2a)
|
||||
return int(p1a.distance_to_point(p2a))
|
||||
|
||||
def highlight_color(self, transparent: Optional[bool] = False) -> QColor:
|
||||
return QColor(255, 255, 0, 20 if transparent else 255)
|
||||
@ -742,7 +946,7 @@ class QLiberationMap(QGraphicsView):
|
||||
# Polygon display mode
|
||||
if self.game.theater.landmap is not None:
|
||||
|
||||
for sea_zone in self.game.theater.landmap[2]:
|
||||
for sea_zone in self.game.theater.landmap.sea_zones:
|
||||
print(sea_zone)
|
||||
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in sea_zone.exterior.coords])
|
||||
if self.reference_point_setup_mode:
|
||||
@ -751,14 +955,14 @@ class QLiberationMap(QGraphicsView):
|
||||
color = "sea_blue"
|
||||
scene.addPolygon(poly, CONST.COLORS[color], CONST.COLORS[color])
|
||||
|
||||
for inclusion_zone in self.game.theater.landmap[0]:
|
||||
for inclusion_zone in self.game.theater.landmap.inclusion_zones:
|
||||
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in inclusion_zone.exterior.coords])
|
||||
if self.reference_point_setup_mode:
|
||||
scene.addPolygon(poly, CONST.COLORS["grey_transparent"], CONST.COLORS["dark_grey_transparent"])
|
||||
else:
|
||||
scene.addPolygon(poly, CONST.COLORS["grey"], CONST.COLORS["dark_grey"])
|
||||
|
||||
for exclusion_zone in self.game.theater.landmap[1]:
|
||||
for exclusion_zone in self.game.theater.landmap.exclusion_zones:
|
||||
poly = QPolygonF([QPointF(*self._transform_point(Point(point[0], point[1]))) for point in exclusion_zone.exterior.coords])
|
||||
if self.reference_point_setup_mode:
|
||||
scene.addPolygon(poly, CONST.COLORS["grey_transparent"], CONST.COLORS["dark_dark_grey_transparent"])
|
||||
@ -820,22 +1024,60 @@ class QLiberationMap(QGraphicsView):
|
||||
distance = self.selected_cp.control_point.position.distance_to_point(
|
||||
world_destination
|
||||
)
|
||||
if meter_to_nm(distance) > MAX_SHIP_DISTANCE:
|
||||
if meters(distance) > MAX_SHIP_DISTANCE:
|
||||
return False
|
||||
return self.game.theater.is_in_sea(world_destination)
|
||||
|
||||
def sceneMouseMovedEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
if self.game is None:
|
||||
return
|
||||
|
||||
mouse_position = Point(event.scenePos().x(), event.scenePos().y())
|
||||
if self.state == QLiberationMapState.MOVING_UNIT:
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
self.movement_line.setLine(
|
||||
QLineF(self.movement_line.line().p1(), event.scenePos()))
|
||||
|
||||
pos = Point(event.scenePos().x(), event.scenePos().y())
|
||||
if self.is_valid_ship_pos(pos):
|
||||
if self.is_valid_ship_pos(mouse_position):
|
||||
self.movement_line.setPen(CONST.COLORS["green"])
|
||||
else:
|
||||
self.movement_line.setPen(CONST.COLORS["red"])
|
||||
|
||||
mouse_world_pos = self._scene_to_dcs_coords(mouse_position)
|
||||
if DisplayOptions.navmeshes.blue_navmesh:
|
||||
self.highlight_mouse_navmesh(
|
||||
self.scene(), self.game.blue_navmesh,
|
||||
self._scene_to_dcs_coords(mouse_position))
|
||||
if DisplayOptions.path_debug.shortest_path:
|
||||
self.draw_shortest_path(self.scene(), self.game.blue_navmesh,
|
||||
mouse_world_pos, player=True)
|
||||
|
||||
if DisplayOptions.navmeshes.red_navmesh:
|
||||
self.highlight_mouse_navmesh(
|
||||
self.scene(), self.game.red_navmesh, mouse_world_pos)
|
||||
|
||||
debug_blue = DisplayOptions.path_debug_faction.blue
|
||||
if DisplayOptions.path_debug.shortest_path:
|
||||
self.draw_shortest_path(
|
||||
self.scene(), self.game.navmesh_for(player=debug_blue),
|
||||
mouse_world_pos, player=False)
|
||||
elif not DisplayOptions.path_debug.hide:
|
||||
if DisplayOptions.path_debug.barcap:
|
||||
task = FlightType.BARCAP
|
||||
elif DisplayOptions.path_debug.cas:
|
||||
task = FlightType.CAS
|
||||
elif DisplayOptions.path_debug.sweep:
|
||||
task = FlightType.SWEEP
|
||||
elif DisplayOptions.path_debug.strike:
|
||||
task = FlightType.STRIKE
|
||||
elif DisplayOptions.path_debug.tarcap:
|
||||
task = FlightType.TARCAP
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unexpected value for DisplayOptions.path_debug")
|
||||
self.draw_test_flight_plan(self.scene(), task, mouse_world_pos,
|
||||
player=debug_blue)
|
||||
|
||||
def sceneMousePressEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
if self.state == QLiberationMapState.MOVING_UNIT:
|
||||
if event.buttons() == Qt.RightButton:
|
||||
|
||||
@ -89,7 +89,7 @@ class QMapControlPoint(QMapObject):
|
||||
return
|
||||
|
||||
for connected in self.control_point.connected_points:
|
||||
if connected.captured:
|
||||
if connected.captured and self.game_model.game.settings.enable_base_capture_cheat:
|
||||
menu.addAction(self.capture_action)
|
||||
break
|
||||
|
||||
|
||||
@ -58,9 +58,11 @@ class QMapGroundObject(QMapObject):
|
||||
@property
|
||||
def production_per_turn(self) -> int:
|
||||
production = 0
|
||||
for g in self.control_point.ground_objects:
|
||||
if g.category in REWARDS.keys():
|
||||
production += REWARDS[g.category]
|
||||
for building in self.buildings:
|
||||
if building.is_dead:
|
||||
continue
|
||||
if building.category in REWARDS.keys():
|
||||
production += REWARDS[building.category]
|
||||
return production
|
||||
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
@ -85,10 +87,22 @@ class QMapGroundObject(QMapObject):
|
||||
is_dead = False
|
||||
break
|
||||
|
||||
if cat == "aa":
|
||||
has_threat = False
|
||||
for group in self.ground_object.groups:
|
||||
if self.ground_object.threat_range(group).distance_in_meters > 0:
|
||||
has_threat = True
|
||||
|
||||
if not is_dead and not self.control_point.captured:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + enemy_icons])
|
||||
if cat == "aa" and not has_threat:
|
||||
painter.drawPixmap(rect, const.ICONS["nothreat" + enemy_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + enemy_icons])
|
||||
elif not is_dead:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + player_icons])
|
||||
if cat == "aa" and not has_threat:
|
||||
painter.drawPixmap(rect, const.ICONS["nothreat" + player_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + player_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS["destroyed"])
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ class QDebriefingWindow(QDialog):
|
||||
for unit_type, count in player_air_losses.items():
|
||||
try:
|
||||
lostUnitsLayout.addWidget(
|
||||
QLabel(db.unit_type_name(unit_type)), row, 0)
|
||||
QLabel(db.unit_get_expanded_info(self.debriefing.player_country, unit_type, 'name')), row, 0)
|
||||
lostUnitsLayout.addWidget(QLabel(str(count)), row, 1)
|
||||
row += 1
|
||||
except AttributeError:
|
||||
@ -94,7 +94,7 @@ class QDebriefingWindow(QDialog):
|
||||
for unit_type, count in enemy_air_losses.items():
|
||||
try:
|
||||
enemylostUnitsLayout.addWidget(
|
||||
QLabel(db.unit_type_name(unit_type)), row, 0)
|
||||
QLabel(db.unit_get_expanded_info(self.debriefing.enemy_country, unit_type, 'name')), row, 0)
|
||||
enemylostUnitsLayout.addWidget(QLabel(str(count)), row, 1)
|
||||
row += 1
|
||||
except AttributeError:
|
||||
|
||||
@ -168,18 +168,21 @@ class QLiberationWindow(QMainWindow):
|
||||
|
||||
displayMenu = self.menu.addMenu("&Display")
|
||||
|
||||
last_was_group = True
|
||||
|
||||
last_was_group = False
|
||||
for item in DisplayOptions.menu_items():
|
||||
if isinstance(item, DisplayRule):
|
||||
if last_was_group:
|
||||
displayMenu.addSeparator()
|
||||
self.display_bar.addSeparator()
|
||||
action = self.make_display_rule_action(item)
|
||||
displayMenu.addAction(action)
|
||||
if action.icon():
|
||||
self.display_bar.addAction(action)
|
||||
last_was_group = False
|
||||
elif isinstance(item, DisplayGroup):
|
||||
if not last_was_group:
|
||||
displayMenu.addSeparator()
|
||||
self.display_bar.addSeparator()
|
||||
displayMenu.addSeparator()
|
||||
self.display_bar.addSeparator()
|
||||
group = QActionGroup(displayMenu)
|
||||
for display_rule in item:
|
||||
action = self.make_display_rule_action(display_rule, group)
|
||||
@ -257,8 +260,6 @@ class QLiberationWindow(QMainWindow):
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
try:
|
||||
if game is not None:
|
||||
game.on_load()
|
||||
self.game = game
|
||||
if self.info_panel is not None:
|
||||
self.info_panel.setGame(game)
|
||||
|
||||
118
qt_ui/windows/QUnitInfoWindow.py
Normal file
118
qt_ui/windows/QUnitInfoWindow.py
Normal file
@ -0,0 +1,118 @@
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from PySide2 import QtCore
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QIcon, QMovie, QPixmap
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QTextBrowser,
|
||||
QFrame,
|
||||
)
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from dcs.unittype import UnitType, FlyingType, VehicleType
|
||||
import dcs
|
||||
from qt_ui.uiconstants import AIRCRAFT_BANNERS, VEHICLE_BANNERS
|
||||
|
||||
from game.game import Game
|
||||
from game import db
|
||||
|
||||
import gen.flights.ai_flight_planner_db
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class QUnitInfoWindow(QDialog):
|
||||
|
||||
def __init__(self, game: Game, unit_type: Type[UnitType]) -> None:
|
||||
super(QUnitInfoWindow, self).__init__()
|
||||
self.setModal(True)
|
||||
self.game = game
|
||||
self.unit_type = unit_type
|
||||
self.setWindowTitle(f"Unit Info: {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'name')}")
|
||||
self.setWindowIcon(QIcon("./resources/icon.png"))
|
||||
self.setMinimumHeight(570)
|
||||
self.setMaximumWidth(640)
|
||||
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
|
||||
self.initUi()
|
||||
|
||||
def initUi(self):
|
||||
self.layout = QGridLayout()
|
||||
|
||||
header = QLabel(self)
|
||||
header.setGeometry(0, 0, 720, 360)
|
||||
if dcs.planes.plane_map.get(self.unit_type.id) is not None or dcs.helicopters.helicopter_map.get(self.unit_type.id) is not None:
|
||||
pixmap = AIRCRAFT_BANNERS.get(self.unit_type.id)
|
||||
elif dcs.vehicles.vehicle_map.get(self.unit_type.id) is not None:
|
||||
pixmap = VEHICLE_BANNERS.get(self.unit_type.id)
|
||||
if pixmap is None:
|
||||
pixmap = AIRCRAFT_BANNERS.get("Missing")
|
||||
header.setPixmap(pixmap.scaled(header.width(), header.height()))
|
||||
self.layout.addWidget(header, 0, 0)
|
||||
|
||||
self.gridLayout = QGridLayout()
|
||||
|
||||
# Build the topmost details grid.
|
||||
self.details_grid = QFrame()
|
||||
self.details_grid_layout = QGridLayout()
|
||||
self.details_grid_layout.setMargin(0)
|
||||
|
||||
self.name_box = QLabel(f"<b>Name:</b> {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'manufacturer')} {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'name')}")
|
||||
self.name_box.setProperty("style", "info-element")
|
||||
|
||||
self.country_box = QLabel(f"<b>Country of Origin:</b> {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'country-of-origin')}")
|
||||
self.country_box.setProperty("style", "info-element")
|
||||
|
||||
self.role_box = QLabel(f"<b>Role:</b> {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'role')}")
|
||||
self.role_box.setProperty("style", "info-element")
|
||||
|
||||
self.year_box = QLabel(f"<b>Variant Introduction:</b> {db.unit_get_expanded_info(self.game.player_country, self.unit_type, 'year-of-variant-introduction')}")
|
||||
self.year_box.setProperty("style", "info-element")
|
||||
|
||||
self.details_grid_layout.addWidget(self.name_box, 0, 0)
|
||||
self.details_grid_layout.addWidget(self.country_box, 0, 1)
|
||||
self.details_grid_layout.addWidget(self.role_box, 1, 0)
|
||||
self.details_grid_layout.addWidget(self.year_box, 1, 1)
|
||||
|
||||
self.details_grid.setLayout(self.details_grid_layout)
|
||||
|
||||
self.gridLayout.addWidget(self.details_grid, 1, 0)
|
||||
|
||||
# If it's an aircraft, include the task list.
|
||||
if dcs.planes.plane_map.get(self.unit_type.id) is not None or dcs.helicopters.helicopter_map.get(self.unit_type.id) is not None:
|
||||
self.tasks_box = QLabel(f"<b>In-Game Tasks:</b> {self.generateAircraftTasks()}")
|
||||
self.tasks_box.setProperty("style", "info-element")
|
||||
self.gridLayout.addWidget(self.tasks_box, 2, 0)
|
||||
|
||||
# Finally, add the description box.
|
||||
self.details_text = QTextBrowser()
|
||||
self.details_text.setProperty("style", "info-desc")
|
||||
self.details_text.setText(db.unit_get_expanded_info(self.game.player_country, self.unit_type, "text"))
|
||||
self.gridLayout.addWidget(self.details_text, 3, 0)
|
||||
|
||||
self.layout.addLayout(self.gridLayout, 1, 0)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def generateAircraftTasks(self) -> str:
|
||||
aircraft_tasks = ""
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.CAP_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.BARCAP}, {FlightType.ESCORT}, {FlightType.INTERCEPTION}, {FlightType.SWEEP}, {FlightType.TARCAP}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.CAS_CAPABLE or self.unit_type in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.CAS}, {FlightType.BAI}, {FlightType.OCA_AIRCRAFT}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.SEAD_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.SEAD}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.DEAD_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.DEAD}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.ANTISHIP_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.ANTISHIP}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.RUNWAY_ATTACK_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.OCA_RUNWAY}, "
|
||||
if self.unit_type in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE or self.unit_type in gen.flights.ai_flight_planner_db.TRANSPORT_CAPABLE:
|
||||
aircraft_tasks = aircraft_tasks + f"{FlightType.STRIKE}, "
|
||||
return aircraft_tasks[:-2]
|
||||
@ -2,6 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import timeit
|
||||
from datetime import timedelta
|
||||
|
||||
from PySide2 import QtCore
|
||||
from PySide2.QtCore import QObject, Qt, Signal
|
||||
@ -184,11 +186,14 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
lambda d: self.on_debriefing_update(d), self.game, self.unit_map)
|
||||
|
||||
def process_debriefing(self):
|
||||
start = timeit.default_timer()
|
||||
self.game.finish_event(event=self.gameEvent, debriefing=self.debriefing)
|
||||
self.game.pass_turn()
|
||||
|
||||
GameUpdateSignal.get_instance().sendDebriefing(self.debriefing)
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
end = timeit.default_timer()
|
||||
logging.info("Turn processing took %s", timedelta(seconds=end - start))
|
||||
self.close()
|
||||
|
||||
def debriefing_directory_location(self) -> str:
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import logging
|
||||
from typing import Type
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
@ -17,6 +18,7 @@ from game.event import UnitsDeliveryEvent
|
||||
from game.theater import ControlPoint
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.QUnitInfoWindow import QUnitInfoWindow
|
||||
|
||||
|
||||
class QRecruitBehaviour:
|
||||
@ -36,7 +38,6 @@ class QRecruitBehaviour:
|
||||
|
||||
@property
|
||||
def pending_deliveries(self) -> UnitsDeliveryEvent:
|
||||
assert self.cp.pending_unit_deliveries
|
||||
return self.cp.pending_unit_deliveries
|
||||
|
||||
@property
|
||||
@ -59,7 +60,7 @@ class QRecruitBehaviour:
|
||||
existing_units = self.cp.base.total_units_of_type(unit_type)
|
||||
scheduled_units = self.pending_deliveries.units.get(unit_type, 0)
|
||||
|
||||
unitName = QLabel("<b>" + db.unit_type_name_2(unit_type) + "</b>")
|
||||
unitName = QLabel("<b>" + db.unit_get_expanded_info(self.game_model.game.player_country, unit_type, 'name') + "</b>")
|
||||
unitName.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
|
||||
|
||||
existing_units = QLabel(str(existing_units))
|
||||
@ -97,6 +98,21 @@ class QRecruitBehaviour:
|
||||
sell.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||
sell.clicked.connect(lambda: self.sell(unit_type))
|
||||
|
||||
info = QGroupBox()
|
||||
info.setProperty("style", "buy-box")
|
||||
info.setMaximumHeight(36)
|
||||
info.setMinimumHeight(36)
|
||||
infolayout = QHBoxLayout()
|
||||
info.setLayout(infolayout)
|
||||
|
||||
unitInfo = QPushButton("i")
|
||||
unitInfo.setProperty("style", "btn-info")
|
||||
unitInfo.setDisabled(disabled)
|
||||
unitInfo.setMinimumSize(16, 16)
|
||||
unitInfo.setMaximumSize(16, 16)
|
||||
unitInfo.clicked.connect(lambda: self.info(unit_type))
|
||||
unitInfo.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||
|
||||
existLayout.addWidget(unitName)
|
||||
existLayout.addItem(QSpacerItem(20, 0, QSizePolicy.Minimum, QSizePolicy.Minimum))
|
||||
existLayout.addWidget(existing_units)
|
||||
@ -107,8 +123,11 @@ class QRecruitBehaviour:
|
||||
buysellayout.addWidget(amount_bought)
|
||||
buysellayout.addWidget(buy)
|
||||
|
||||
infolayout.addWidget(unitInfo)
|
||||
|
||||
layout.addWidget(exist, row, 1)
|
||||
layout.addWidget(buysell, row, 2)
|
||||
layout.addWidget(info, row, 3)
|
||||
|
||||
return row + 1
|
||||
|
||||
@ -128,7 +147,7 @@ class QRecruitBehaviour:
|
||||
def buy(self, unit_type: Type[UnitType]):
|
||||
price = db.PRICES[unit_type]
|
||||
if self.budget >= price:
|
||||
self.pending_deliveries.deliver({unit_type: 1})
|
||||
self.pending_deliveries.order({unit_type: 1})
|
||||
self.budget -= price
|
||||
else:
|
||||
# TODO : display modal warning
|
||||
@ -137,20 +156,19 @@ class QRecruitBehaviour:
|
||||
self.update_available_budget()
|
||||
|
||||
def sell(self, unit_type):
|
||||
if self.pending_deliveries.units.get(unit_type, 0) > 0:
|
||||
if self.pending_deliveries.available_next_turn(unit_type) > 0:
|
||||
price = db.PRICES[unit_type]
|
||||
self.budget += price
|
||||
self.pending_deliveries.units[unit_type] = self.pending_deliveries.units[unit_type] - 1
|
||||
self.pending_deliveries.sell({unit_type: 1})
|
||||
if self.pending_deliveries.units[unit_type] == 0:
|
||||
del self.pending_deliveries.units[unit_type]
|
||||
elif self.cp.base.total_units_of_type(unit_type) > 0:
|
||||
price = db.PRICES[unit_type]
|
||||
self.budget += price
|
||||
self.cp.base.commit_losses({unit_type: 1})
|
||||
|
||||
self._update_count_label(unit_type)
|
||||
self.update_available_budget()
|
||||
|
||||
def info(self, unit_type):
|
||||
self.info_window = QUnitInfoWindow(self.game_model.game, unit_type)
|
||||
self.info_window.show()
|
||||
|
||||
def set_maximum_units(self, maximum_units):
|
||||
"""
|
||||
Set the maximum number of units that can be bought
|
||||
|
||||
@ -65,7 +65,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
continue
|
||||
unit_types.add(unit)
|
||||
|
||||
sorted_units = sorted(unit_types, key=lambda u: db.unit_type_name_2(u))
|
||||
sorted_units = sorted(unit_types, key=lambda u: db.unit_get_expanded_info(self.game_model.game.player_country, u, 'name'))
|
||||
for unit_type in sorted_units:
|
||||
row = self.add_purchase_row(
|
||||
unit_type, task_box_layout, row,
|
||||
@ -92,7 +92,13 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
self, "No space for additional aircraft",
|
||||
f"There is no parking space left at {self.cp.name} to accommodate another plane.", QMessageBox.Ok)
|
||||
return
|
||||
|
||||
# If we change our mind about selling, we want the aircraft to be put
|
||||
# back in the inventory immediately.
|
||||
elif self.pending_deliveries.units.get(unit_type, 0) < 0:
|
||||
global_inventory = self.game_model.game.aircraft_inventory
|
||||
inventory = global_inventory.for_control_point(self.cp)
|
||||
inventory.add_aircraft(unit_type, 1)
|
||||
|
||||
super().buy(unit_type)
|
||||
self.hangar_status.update_label()
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ class QAirfieldCommand(QFrame):
|
||||
|
||||
def init_ui(self):
|
||||
layout = QGridLayout()
|
||||
layout.setHorizontalSpacing(1)
|
||||
layout.addWidget(QAircraftRecruitmentMenu(self.cp, self.game_model), 0, 0)
|
||||
|
||||
planned = QGroupBox("Planned Flights")
|
||||
|
||||
@ -5,8 +5,10 @@ from PySide2.QtWidgets import (
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QMessageBox,
|
||||
)
|
||||
from dcs.task import PinpointStrike
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.theater import ControlPoint
|
||||
@ -42,7 +44,7 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
for task_type in units.keys():
|
||||
units_column = list(set(units[task_type]))
|
||||
if len(units_column) == 0: continue
|
||||
units_column.sort(key=lambda x: db.PRICES[x])
|
||||
units_column.sort(key=lambda u: db.unit_get_expanded_info(self.game_model.game.player_country, u, 'name'))
|
||||
for unit_type in units_column:
|
||||
row = self.add_purchase_row(unit_type, task_box_layout, row)
|
||||
stretch = QVBoxLayout()
|
||||
@ -57,3 +59,12 @@ class QArmorRecruitmentMenu(QFrame, QRecruitBehaviour):
|
||||
scroll.setWidget(scroll_content)
|
||||
main_layout.addWidget(scroll)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
def sell(self, unit_type: UnitType):
|
||||
if self.pending_deliveries.available_next_turn(unit_type) <= 0:
|
||||
QMessageBox.critical(
|
||||
self, "Could not sell ground unit",
|
||||
f"Attempted to sell one {unit_type.id} at {self.cp.name} "
|
||||
"but none are available.", QMessageBox.Ok)
|
||||
return
|
||||
super().sell(unit_type)
|
||||
@ -4,7 +4,10 @@ from PySide2.QtWidgets import (
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
QScrollArea,
|
||||
QWidget,
|
||||
)
|
||||
from PySide2.QtCore import Qt
|
||||
from dcs.task import CAP, CAS, Embarking, PinpointStrike
|
||||
|
||||
from game import Game, db
|
||||
@ -21,10 +24,11 @@ class QIntelInfo(QFrame):
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
intel = QGroupBox("Intel")
|
||||
scroll_content = QWidget()
|
||||
intelLayout = QVBoxLayout()
|
||||
|
||||
|
||||
|
||||
units = {
|
||||
CAP: db.find_unittype(CAP, self.game.enemy_name),
|
||||
Embarking: db.find_unittype(Embarking, self.game.enemy_name),
|
||||
@ -46,14 +50,19 @@ class QIntelInfo(QFrame):
|
||||
existing_units = self.cp.base.total_units_of_type(unit_type)
|
||||
if existing_units == 0:
|
||||
continue
|
||||
groupLayout.addWidget(QLabel("<b>" + db.unit_type_name(unit_type) + "</b>"), row, 0)
|
||||
groupLayout.addWidget(QLabel("<b>" + db.unit_get_expanded_info(self.game.enemy_country, unit_type, 'name') + "</b>"), row, 0)
|
||||
groupLayout.addWidget(QLabel(str(existing_units)), row, 1)
|
||||
row += 1
|
||||
|
||||
intelLayout.addWidget(group)
|
||||
|
||||
intelLayout.addStretch()
|
||||
intel.setLayout(intelLayout)
|
||||
layout.addWidget(intel)
|
||||
layout.addStretch()
|
||||
scroll_content.setLayout(intelLayout)
|
||||
scroll = QScrollArea()
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(scroll_content)
|
||||
|
||||
layout.addWidget(scroll)
|
||||
|
||||
self.setLayout(layout)
|
||||
@ -1,3 +1,6 @@
|
||||
import itertools
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QDialog,
|
||||
QFrame,
|
||||
@ -8,7 +11,8 @@ from PySide2.QtWidgets import (
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.game import Game
|
||||
from game.income import Income
|
||||
from game.income import BuildingIncome, Income
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
class QHorizontalSeparationLine(QFrame):
|
||||
@ -25,44 +29,65 @@ class QHorizontalSeparationLine(QFrame):
|
||||
class FinancesLayout(QGridLayout):
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
super().__init__()
|
||||
self.row = itertools.count(0)
|
||||
|
||||
income = Income(game, player)
|
||||
|
||||
self.addWidget(QLabel("<b>Control Points</b>"), 0, 0)
|
||||
self.addWidget(QLabel(
|
||||
f"{len(income.control_points)} bases x {income.income_per_base}M"),
|
||||
0, 1)
|
||||
self.addWidget(QLabel(f"{income.from_bases}M"), 0, 2)
|
||||
control_points = reversed(
|
||||
sorted(income.control_points, key=lambda c: c.income_per_turn))
|
||||
for control_point in control_points:
|
||||
self.add_control_point(control_point)
|
||||
|
||||
self.addWidget(QHorizontalSeparationLine(), 1, 0, 1, 3)
|
||||
self.add_line()
|
||||
|
||||
buildings = reversed(sorted(income.buildings, key=lambda b: b.income))
|
||||
row = 2
|
||||
for row, building in enumerate(buildings, row):
|
||||
self.addWidget(
|
||||
QLabel(f"<b>{building.category.upper()} [{building.name}]</b>"),
|
||||
row, 0)
|
||||
self.addWidget(QLabel(
|
||||
f"{building.number} buildings x {building.income_per_building}M"),
|
||||
row, 1)
|
||||
rlabel = QLabel(f"{building.income}M")
|
||||
rlabel.setProperty("style", "green")
|
||||
self.addWidget(rlabel, row, 2)
|
||||
for building in buildings:
|
||||
self.add_building(building)
|
||||
|
||||
self.addWidget(QHorizontalSeparationLine(), row + 1, 0, 1, 3)
|
||||
self.addWidget(QLabel(
|
||||
f"Income multiplier: {income.multiplier:.1f}"),
|
||||
row + 2, 1
|
||||
)
|
||||
self.addWidget(QLabel(f"<b>{income.total}M</b>"), row + 2, 2)
|
||||
self.add_line()
|
||||
|
||||
self.add_row(middle=f"Income multiplier: {income.multiplier:.1f}",
|
||||
right=f"<b>{income.total}M</b>")
|
||||
|
||||
if player:
|
||||
budget = game.budget
|
||||
else:
|
||||
budget = game.enemy_budget
|
||||
self.addWidget(QLabel(f"Balance"), row + 3, 1)
|
||||
self.addWidget(QLabel(f"<b>{budget}M</b>"), row + 3, 2)
|
||||
self.setRowStretch(row + 4, 1)
|
||||
|
||||
self.add_row(middle="Balance", right=f"<b>{budget}M</b>")
|
||||
self.setRowStretch(next(self.row), 1)
|
||||
|
||||
def add_row(self, left: Optional[str] = None, middle: Optional[str] = None,
|
||||
right: Optional[str] = None) -> None:
|
||||
if not any([left, middle, right]):
|
||||
raise ValueError
|
||||
|
||||
row = next(self.row)
|
||||
if left is not None:
|
||||
self.addWidget(QLabel(left), row, 0)
|
||||
if middle is not None:
|
||||
self.addWidget(QLabel(middle), row, 1)
|
||||
if right is not None:
|
||||
self.addWidget(QLabel(right), row, 2)
|
||||
|
||||
def add_control_point(self, control_point: ControlPoint) -> None:
|
||||
self.add_row(left=f"<b>{control_point.name}</b>",
|
||||
right=f"{control_point.income_per_turn}M")
|
||||
|
||||
def add_building(self, building: BuildingIncome) -> None:
|
||||
row = next(self.row)
|
||||
self.addWidget(
|
||||
QLabel(f"<b>{building.category.upper()} [{building.name}]</b>"),
|
||||
row, 0)
|
||||
self.addWidget(QLabel(
|
||||
f"{building.number} buildings x {building.income_per_building}M"),
|
||||
row, 1)
|
||||
rlabel = QLabel(f"{building.income}M")
|
||||
rlabel.setProperty("style", "green")
|
||||
self.addWidget(rlabel, row, 2)
|
||||
|
||||
def add_line(self) -> None:
|
||||
self.addWidget(QHorizontalSeparationLine(), next(self.row), 0, 1, 3)
|
||||
|
||||
|
||||
class QFinancesMenu(QDialog):
|
||||
|
||||
@ -357,8 +357,7 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
# Generate SAM
|
||||
generator = sam_generator(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
generated_group = generator.get_generated_group()
|
||||
self.ground_object.groups = [generated_group]
|
||||
self.ground_object.groups = list(generator.groups)
|
||||
|
||||
GameUpdateSignal.get_instance().updateBudget(self.game)
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ from PySide2.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game.game import Game
|
||||
from game.game import Game, db
|
||||
from qt_ui.uiconstants import ICONS
|
||||
from qt_ui.windows.finances.QFinancesMenu import FinancesLayout
|
||||
|
||||
@ -81,7 +81,7 @@ class AircraftIntelLayout(IntelTableLayout):
|
||||
for airframe, count in base.aircraft.items():
|
||||
if not count:
|
||||
continue
|
||||
self.add_row(airframe.id, count)
|
||||
self.add_row(db.unit_get_expanded_info(game.enemy_country, airframe, 'name'), count)
|
||||
|
||||
self.add_spacer()
|
||||
self.add_row("<b>Total</b>", total)
|
||||
|
||||
@ -4,9 +4,8 @@ from PySide2.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.models import GameModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||
@ -15,17 +14,19 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
|
||||
class QEditFlightDialog(QDialog):
|
||||
"""Dialog window for editing flight plans and loadouts."""
|
||||
|
||||
def __init__(self, game_model: GameModel, package: Package, flight: Flight, parent=None) -> None:
|
||||
def __init__(self, game_model: GameModel, package_model: PackageModel,
|
||||
flight: Flight, parent=None) -> None:
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.game_model = game_model
|
||||
|
||||
self.setWindowTitle("Create flight")
|
||||
self.setWindowTitle("Edit flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
self.flight_planner = QFlightPlanner(package, flight, game_model.game)
|
||||
self.flight_planner = QFlightPlanner(package_model, flight,
|
||||
game_model.game)
|
||||
layout.addWidget(self.flight_planner)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
@ -3,8 +3,9 @@ import logging
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import QItemSelection, QTime, Signal
|
||||
from PySide2.QtCore import QItemSelection, QTime, Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@ -15,16 +16,15 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
from game.game import Game
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flightplan import FlightPlanBuilder, PlanningError
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import AtoModel, GameModel, PackageModel
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.ato import QFlightList
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.mission.flight.QFlightCreator import QFlightCreator
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
|
||||
|
||||
class QPackageDialog(QDialog):
|
||||
@ -78,15 +78,23 @@ class QPackageDialog(QDialog):
|
||||
self.tot_spinner.setMinimumTime(QTime(0, 0))
|
||||
self.tot_spinner.setDisplayFormat("T+hh:mm:ss")
|
||||
self.tot_spinner.timeChanged.connect(self.save_tot)
|
||||
self.tot_spinner.setToolTip("Package TOT relative to mission TOT")
|
||||
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
||||
self.tot_column.addWidget(self.tot_spinner)
|
||||
|
||||
self.reset_tot_button = QPushButton("ASAP")
|
||||
self.reset_tot_button.setToolTip(
|
||||
self.auto_asap = QCheckBox("ASAP")
|
||||
self.auto_asap.setToolTip(
|
||||
"Sets the package TOT to the earliest time that all flights can "
|
||||
"arrive at the target."
|
||||
)
|
||||
self.reset_tot_button.clicked.connect(self.reset_tot)
|
||||
self.tot_column.addWidget(self.reset_tot_button)
|
||||
self.auto_asap.setChecked(self.package_model.package.auto_asap)
|
||||
self.auto_asap.toggled.connect(self.set_asap)
|
||||
self.tot_column.addWidget(self.auto_asap)
|
||||
|
||||
self.tot_help_label = QLabel("<a href=\"https://github.com/Khopa/dcs_liberation/wiki/Mission-planning\"><span style=\"color:#FFFFFF;\">Help</span></a>")
|
||||
self.tot_help_label.setAlignment(Qt.AlignCenter)
|
||||
self.tot_help_label.setOpenExternalLinks(True)
|
||||
self.tot_column.addWidget(self.tot_help_label)
|
||||
|
||||
self.package_view = QFlightList(self.game_model, self.package_model)
|
||||
self.package_view.selectionModel().selectionChanged.connect(
|
||||
@ -107,6 +115,8 @@ class QPackageDialog(QDialog):
|
||||
self.delete_flight_button.setEnabled(model.rowCount() > 0)
|
||||
self.button_layout.addWidget(self.delete_flight_button)
|
||||
|
||||
self.package_model.tot_changed.connect(self.update_tot)
|
||||
|
||||
self.button_layout.addStretch()
|
||||
|
||||
self.setLayout(self.layout)
|
||||
@ -139,14 +149,14 @@ class QPackageDialog(QDialog):
|
||||
def save_tot(self) -> None:
|
||||
time = self.tot_spinner.time()
|
||||
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
|
||||
self.package_model.update_tot(timedelta(seconds=seconds))
|
||||
self.package_model.set_tot(timedelta(seconds=seconds))
|
||||
|
||||
def reset_tot(self) -> None:
|
||||
if not list(self.package_model.flights):
|
||||
self.package_model.update_tot(timedelta())
|
||||
else:
|
||||
self.package_model.update_tot(
|
||||
TotEstimator(self.package_model.package).earliest_tot())
|
||||
def set_asap(self, checked: bool) -> None:
|
||||
self.package_model.set_asap(checked)
|
||||
self.tot_spinner.setEnabled(not self.package_model.package.auto_asap)
|
||||
self.update_tot()
|
||||
|
||||
def update_tot(self) -> None:
|
||||
self.tot_spinner.setTime(self.tot_qtime())
|
||||
|
||||
def on_selection_changed(self, selected: QItemSelection,
|
||||
@ -177,6 +187,7 @@ class QPackageDialog(QDialog):
|
||||
QMessageBox.critical(
|
||||
self, "Could not create flight", str(ex), QMessageBox.Ok
|
||||
)
|
||||
self.package_model.update_tot()
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.package_changed.emit()
|
||||
|
||||
|
||||
@ -2,10 +2,13 @@ from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QLineEdit,
|
||||
)
|
||||
from dcs.planes import PlaneType
|
||||
|
||||
@ -31,6 +34,8 @@ class QFlightCreator(QDialog):
|
||||
|
||||
self.game = game
|
||||
self.package = package
|
||||
self.custom_name_text = None
|
||||
self.country = self.game.player_country
|
||||
|
||||
self.setWindowTitle("Create flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
@ -41,10 +46,12 @@ class QFlightCreator(QDialog):
|
||||
self.game.theater, package.target
|
||||
)
|
||||
self.task_selector.setCurrentIndex(0)
|
||||
self.task_selector.currentTextChanged.connect(
|
||||
self.on_task_changed)
|
||||
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
||||
|
||||
self.aircraft_selector = QAircraftTypeSelector(
|
||||
self.game.aircraft_inventory.available_types_for_player
|
||||
self.game.aircraft_inventory.available_types_for_player, self.game.player_country, self.task_selector.currentData()
|
||||
)
|
||||
self.aircraft_selector.setCurrentIndex(0)
|
||||
self.aircraft_selector.currentIndexChanged.connect(
|
||||
@ -57,6 +64,7 @@ class QFlightCreator(QDialog):
|
||||
self.aircraft_selector.currentData()
|
||||
)
|
||||
self.departure.availability_changed.connect(self.update_max_size)
|
||||
self.departure.currentIndexChanged.connect(self.on_departure_changed)
|
||||
layout.addLayout(QLabeledWidget("Departure:", self.departure))
|
||||
|
||||
self.arrival = QArrivalAirfieldSelector(
|
||||
@ -88,6 +96,28 @@ class QFlightCreator(QDialog):
|
||||
layout.addLayout(
|
||||
QLabeledWidget("Client Slots:", self.client_slots_spinner))
|
||||
|
||||
# When an off-map spawn overrides the start type to in-flight, we save
|
||||
# the selected type into this value. If a non-off-map spawn is selected
|
||||
# we restore the previous choice.
|
||||
self.restore_start_type: Optional[str] = None
|
||||
self.start_type = QComboBox()
|
||||
self.start_type.addItems(["Cold", "Warm", "Runway", "In Flight"])
|
||||
self.start_type.setCurrentText(self.game.settings.default_start_type)
|
||||
layout.addLayout(QLabeledWidget(
|
||||
"Start type:", self.start_type,
|
||||
tooltip="Selects the start type for this flight."))
|
||||
layout.addWidget(QLabel(
|
||||
"Any option other than Cold will make this flight " +
|
||||
"non-targetable<br />by OCA/Aircraft missions. This will affect " +
|
||||
"game balance."
|
||||
))
|
||||
|
||||
self.custom_name = QLineEdit()
|
||||
self.custom_name.textChanged.connect(self.set_custom_name_text)
|
||||
layout.addLayout(
|
||||
QLabeledWidget("Custom Flight Name (Optional)", self.custom_name)
|
||||
)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
self.create_button = QPushButton("Create")
|
||||
@ -96,12 +126,19 @@ class QFlightCreator(QDialog):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.on_departure_changed(self.departure.currentIndex())
|
||||
|
||||
def set_custom_name_text(self, text: str):
|
||||
self.custom_name_text = text
|
||||
|
||||
def verify_form(self) -> Optional[str]:
|
||||
aircraft: PlaneType = self.aircraft_selector.currentData()
|
||||
origin: ControlPoint = self.departure.currentData()
|
||||
arrival: ControlPoint = self.arrival.currentData()
|
||||
divert: ControlPoint = self.divert.currentData()
|
||||
size: int = self.flight_size_spinner.value()
|
||||
if aircraft is None:
|
||||
return "You must select an aircraft type."
|
||||
if not origin.captured:
|
||||
return f"{origin.name} is not owned by your coalition."
|
||||
if arrival is not None and not arrival.captured:
|
||||
@ -115,6 +152,8 @@ class QFlightCreator(QDialog):
|
||||
return f"{origin.name} has only {available} {aircraft.id} available."
|
||||
if size <= 0:
|
||||
return f"Flight must have at least one aircraft."
|
||||
if self.custom_name_text and "|" in self.custom_name_text:
|
||||
return f"Cannot include | in flight name"
|
||||
return None
|
||||
|
||||
def create_flight(self) -> None:
|
||||
@ -134,14 +173,9 @@ class QFlightCreator(QDialog):
|
||||
if arrival is None:
|
||||
arrival = origin
|
||||
|
||||
if isinstance(origin, OffMapSpawn):
|
||||
start_type = "In Flight"
|
||||
elif self.game.settings.perf_ai_parking_start:
|
||||
start_type = "Cold"
|
||||
else:
|
||||
start_type = "Warm"
|
||||
flight = Flight(self.package, aircraft, size, task, start_type, origin,
|
||||
arrival, divert)
|
||||
flight = Flight(self.package, self.country, aircraft, size, task,
|
||||
self.start_type.currentText(), origin, arrival, divert,
|
||||
custom_name=self.custom_name_text)
|
||||
flight.client_count = self.client_slots_spinner.value()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
@ -154,6 +188,23 @@ class QFlightCreator(QDialog):
|
||||
self.arrival.change_aircraft(new_aircraft)
|
||||
self.divert.change_aircraft(new_aircraft)
|
||||
|
||||
def on_departure_changed(self, index: int) -> None:
|
||||
departure = self.departure.itemData(index)
|
||||
if isinstance(departure, OffMapSpawn):
|
||||
previous_type = self.start_type.currentText()
|
||||
if previous_type != "In Flight":
|
||||
self.restore_start_type = previous_type
|
||||
self.start_type.setCurrentText("In Flight")
|
||||
self.start_type.setEnabled(False)
|
||||
else:
|
||||
self.start_type.setEnabled(True)
|
||||
if self.restore_start_type is not None:
|
||||
self.start_type.setCurrentText(self.restore_start_type)
|
||||
self.restore_start_type = None
|
||||
|
||||
def on_task_changed(self) -> None:
|
||||
self.aircraft_selector.updateItems(self.task_selector.currentData(), self.game.aircraft_inventory.available_types_for_player)
|
||||
|
||||
def update_max_size(self, available: int) -> None:
|
||||
self.flight_size_spinner.setMaximum(min(available, 4))
|
||||
if self.flight_size_spinner.maximum() >= 2:
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QTabWidget
|
||||
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import PackageModel
|
||||
from qt_ui.windows.mission.flight.payload.QFlightPayloadTab import \
|
||||
QFlightPayloadTab
|
||||
from qt_ui.windows.mission.flight.settings.QGeneralFlightSettingsTab import \
|
||||
@ -14,22 +13,15 @@ from qt_ui.windows.mission.flight.waypoints.QFlightWaypointTab import \
|
||||
|
||||
class QFlightPlanner(QTabWidget):
|
||||
|
||||
on_planned_flight_changed = Signal()
|
||||
|
||||
def __init__(self, package: Package, flight: Flight, game: Game):
|
||||
def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
|
||||
super().__init__()
|
||||
|
||||
self.general_settings_tab = QGeneralFlightSettingsTab(
|
||||
game, package, flight
|
||||
game, package_model, flight
|
||||
)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.general_settings_tab.on_flight_settings_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.payload_tab = QFlightPayloadTab(flight, game)
|
||||
self.waypoint_tab = QFlightWaypointTab(game, package, flight)
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.waypoint_tab.on_flight_changed.connect(
|
||||
lambda: self.on_planned_flight_changed.emit())
|
||||
self.waypoint_tab = QFlightWaypointTab(game, package_model.package,
|
||||
flight)
|
||||
self.addTab(self.general_settings_tab, "General Flight settings")
|
||||
self.addTab(self.payload_tab, "Payload")
|
||||
self.addTab(self.waypoint_tab, "Waypoints")
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout, QLabel
|
||||
from PySide2.QtCore import Qt
|
||||
|
||||
from game import Game
|
||||
from gen.flights.flight import Flight
|
||||
@ -15,5 +16,13 @@ class QFlightPayloadTab(QFrame):
|
||||
|
||||
def init_ui(self):
|
||||
layout = QGridLayout()
|
||||
|
||||
# Docs Link
|
||||
docsText = QLabel("<a href=\"https://github.com/Khopa/dcs_liberation/wiki/Custom-Loadouts\"><span style=\"color:#FFFFFF;\">How to create your own default loadout</span></a>")
|
||||
docsText.setAlignment(Qt.AlignCenter)
|
||||
docsText.setOpenExternalLinks(True)
|
||||
|
||||
layout.addWidget(self.payload_editor)
|
||||
layout.addWidget(docsText)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
@ -1,14 +1,21 @@
|
||||
import inspect
|
||||
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout, QVBoxLayout, QSizePolicy
|
||||
from PySide2.QtWidgets import (
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.data.weapons import Pylon
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.windows.mission.flight.payload.QPylonEditor import QPylonEditor
|
||||
|
||||
|
||||
class QLoadoutEditor(QGroupBox):
|
||||
|
||||
def __init__(self, flight, game):
|
||||
super(QLoadoutEditor, self).__init__("Use custom loadout")
|
||||
def __init__(self, flight: Flight, game: Game) -> None:
|
||||
super().__init__("Use custom loadout")
|
||||
self.flight = flight
|
||||
self.game = game
|
||||
self.setCheckable(True)
|
||||
@ -19,22 +26,23 @@ class QLoadoutEditor(QGroupBox):
|
||||
hboxLayout = QVBoxLayout(self)
|
||||
layout = QGridLayout(self)
|
||||
|
||||
pylons = [v for v in self.flight.unit_type.__dict__.values() if inspect.isclass(v) and v.__name__.startswith("Pylon")]
|
||||
for i, pylon in enumerate(pylons):
|
||||
label = QLabel("<b>{}</b>".format(pylon.__name__[len("Pylon"):]))
|
||||
label.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||
for i, pylon in enumerate(Pylon.iter_pylons(self.flight.unit_type)):
|
||||
label = QLabel(f"<b>{pylon.number}</b>")
|
||||
label.setSizePolicy(
|
||||
QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
|
||||
layout.addWidget(label, i, 0)
|
||||
try:
|
||||
pylon_number = int(pylon.__name__.split("Pylon")[1])
|
||||
except:
|
||||
pylon_number = i+1
|
||||
layout.addWidget(QPylonEditor(flight, pylon, pylon_number), i, 1)
|
||||
layout.addWidget(QPylonEditor(game, flight, pylon), i, 1)
|
||||
|
||||
hboxLayout.addLayout(layout)
|
||||
hboxLayout.addStretch()
|
||||
self.setLayout(hboxLayout)
|
||||
|
||||
if not self.isChecked():
|
||||
for i in self.findChildren(QPylonEditor):
|
||||
i.default_loadout()
|
||||
|
||||
def on_toggle(self):
|
||||
self.flight.use_custom_loadout = self.isChecked()
|
||||
|
||||
|
||||
if not self.isChecked():
|
||||
for i in self.findChildren(QPylonEditor):
|
||||
i.default_loadout(i.pylon.number)
|
||||
|
||||
@ -1,38 +1,80 @@
|
||||
import logging
|
||||
import operator
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtWidgets import QWidget, QSpinBox, QComboBox
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from game import Game, db
|
||||
from game.data.weapons import Pylon, Weapon
|
||||
from gen.flights.flight import Flight
|
||||
from dcs import weapons_data
|
||||
|
||||
|
||||
class QPylonEditor(QComboBox):
|
||||
|
||||
def __init__(self, flight, pylon, pylon_number):
|
||||
super(QPylonEditor, self).__init__()
|
||||
self.pylon = pylon
|
||||
self.pylon_number = pylon_number
|
||||
def __init__(self, game: Game, flight: Flight, pylon: Pylon) -> None:
|
||||
super().__init__()
|
||||
self.flight = flight
|
||||
self.pylon = pylon
|
||||
self.game = game
|
||||
|
||||
self.possible_loadout = [i for i in self.pylon.__dict__.keys() if i[:2] != '__']
|
||||
current = self.flight.loadout.get(self.pylon.number)
|
||||
|
||||
if not str(self.pylon_number) in self.flight.loadout.keys():
|
||||
self.flight.loadout[str(self.pylon_number)] = ""
|
||||
|
||||
self.addItem("None")
|
||||
for i,k in enumerate(self.possible_loadout):
|
||||
self.addItem(str(self.pylon.__dict__[k][1]["name"]))
|
||||
if self.flight.loadout[str(self.pylon_number)] == str(k):
|
||||
self.addItem("None", None)
|
||||
if self.game.settings.restrict_weapons_by_date:
|
||||
weapons = pylon.available_on(self.game.date)
|
||||
else:
|
||||
weapons = pylon.allowed
|
||||
allowed = sorted(weapons, key=operator.attrgetter("name"))
|
||||
for i, weapon in enumerate(allowed):
|
||||
self.addItem(weapon.name, weapon)
|
||||
if current == weapon:
|
||||
self.setCurrentIndex(i + 1)
|
||||
|
||||
self.currentTextChanged.connect(self.on_pylon_change)
|
||||
|
||||
self.currentIndexChanged.connect(self.on_pylon_change)
|
||||
|
||||
def on_pylon_change(self):
|
||||
selected = self.currentText()
|
||||
if selected == "None":
|
||||
logging.info("Pylon " + str(self.pylon_number) + " emptied")
|
||||
self.flight.loadout[str(self.pylon_number)] = ""
|
||||
else:
|
||||
logging.info("Pylon " + str(self.pylon_number) + " changed to " + selected)
|
||||
for i, k in enumerate(self.possible_loadout):
|
||||
if selected == str(self.pylon.__dict__[k][1]["name"]):
|
||||
self.flight.loadout[str(self.pylon_number)] = str(k)
|
||||
break
|
||||
selected: Optional[Weapon] = self.currentData()
|
||||
self.flight.loadout[self.pylon.number] = selected
|
||||
|
||||
if selected is None:
|
||||
logging.debug(f"Pylon {self.pylon.number} emptied")
|
||||
else:
|
||||
logging.debug(
|
||||
f"Pylon {self.pylon.number} changed to {selected.name}")
|
||||
|
||||
def default_loadout(self) -> None:
|
||||
self.flight.unit_type.load_payloads()
|
||||
self.setCurrentText("None")
|
||||
pylon_default_weapon = None
|
||||
historical_weapon = None
|
||||
loadout = None
|
||||
# Iterate through each possible payload type for a given aircraft.
|
||||
# Some aircraft have custom loadouts that in aren't the standard set.
|
||||
for payload_override in db.EXPANDED_TASK_PAYLOAD_OVERRIDE.get(self.flight.flight_type.name):
|
||||
if loadout is None:
|
||||
loadout = self.flight.unit_type.loadout_by_name(payload_override)
|
||||
if loadout is not None:
|
||||
for i in loadout:
|
||||
if i[0] == self.pylon.number:
|
||||
pylon_default_weapon = i[1]["clsid"]
|
||||
# TODO: Handle removed pylons better.
|
||||
if pylon_default_weapon == "<CLEAN>":
|
||||
pylon_default_weapon = None
|
||||
if pylon_default_weapon is not None:
|
||||
if self.game.settings.restrict_weapons_by_date:
|
||||
orig_weapon = Weapon.from_clsid(pylon_default_weapon)
|
||||
if not orig_weapon.available_on(self.game.date):
|
||||
for fallback in orig_weapon.fallbacks:
|
||||
if historical_weapon is None:
|
||||
if not self.pylon.can_equip(fallback):
|
||||
continue
|
||||
if not fallback.available_on(self.game.date):
|
||||
continue
|
||||
historical_weapon = fallback
|
||||
else:
|
||||
historical_weapon = orig_weapon
|
||||
if historical_weapon is not None:
|
||||
self.setCurrentText(weapons_data.weapon_ids.get(historical_weapon.cls_id).get("name"))
|
||||
else:
|
||||
self.setCurrentText(weapons_data.weapon_ids.get(pylon_default_weapon).get("name"))
|
||||
@ -0,0 +1,99 @@
|
||||
import logging
|
||||
|
||||
from PySide2.QtWidgets import (QGroupBox, QLabel, QMessageBox, QVBoxLayout)
|
||||
|
||||
from game import Game
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flightplan import FlightPlanBuilder, PlanningError
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.models import PackageModel
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.widgets.combos.QArrivalAirfieldSelector import \
|
||||
QArrivalAirfieldSelector
|
||||
|
||||
|
||||
class FlightAirfieldDisplay(QGroupBox):
|
||||
|
||||
def __init__(self, game: Game, package_model: PackageModel,
|
||||
flight: Flight) -> None:
|
||||
super().__init__("Departure/Arrival")
|
||||
self.game = game
|
||||
self.package_model = package_model
|
||||
self.flight = flight
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
self.departure_time = QLabel()
|
||||
layout.addLayout(QLabeledWidget(
|
||||
f"Departing from <b>{flight.from_cp.name}</b>",
|
||||
self.departure_time))
|
||||
self.package_model.tot_changed.connect(self.update_departure_time)
|
||||
self.update_departure_time()
|
||||
|
||||
layout.addWidget(QLabel("Determined based on the package TOT. Edit the "
|
||||
"package to adjust the TOT."))
|
||||
|
||||
self.arrival = QArrivalAirfieldSelector(
|
||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
||||
flight.unit_type, "Same as departure"
|
||||
)
|
||||
self.arrival.currentIndexChanged.connect(self.set_arrival)
|
||||
if flight.arrival != flight.departure:
|
||||
self.arrival.setCurrentText(flight.arrival.name)
|
||||
layout.addLayout(QLabeledWidget("Arrival:", self.arrival))
|
||||
|
||||
self.divert = QArrivalAirfieldSelector(
|
||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
||||
flight.unit_type, "None"
|
||||
)
|
||||
self.divert.currentIndexChanged.connect(self.set_divert)
|
||||
if flight.divert is not None:
|
||||
self.divert.setCurrentText(flight.divert.name)
|
||||
layout.addLayout(QLabeledWidget("Divert:", self.divert))
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def update_departure_time(self) -> None:
|
||||
estimator = TotEstimator(self.package_model.package)
|
||||
delay = estimator.mission_start_time(self.flight)
|
||||
self.departure_time.setText(f"At T+{delay}")
|
||||
|
||||
def set_arrival(self, index: int) -> None:
|
||||
old_arrival = self.flight.arrival
|
||||
arrival = self.arrival.itemData(index)
|
||||
if arrival == old_arrival:
|
||||
return
|
||||
|
||||
if arrival is None:
|
||||
arrival = self.flight.departure
|
||||
|
||||
self.flight.arrival = arrival
|
||||
try:
|
||||
self.update_flight_plan()
|
||||
except PlanningError as ex:
|
||||
self.flight.arrival = old_arrival
|
||||
logging.exception("Could not change arrival airfield")
|
||||
QMessageBox.critical(
|
||||
self, "Could not update flight plan", str(ex), QMessageBox.Ok
|
||||
)
|
||||
|
||||
def set_divert(self, index: int) -> None:
|
||||
old_divert = self.flight.divert
|
||||
divert = self.divert.itemData(index)
|
||||
if divert == old_divert:
|
||||
return
|
||||
|
||||
self.flight.divert = divert
|
||||
try:
|
||||
self.update_flight_plan()
|
||||
except PlanningError as ex:
|
||||
self.flight.divert = old_divert
|
||||
logging.exception("Could not change divert airfield")
|
||||
QMessageBox.critical(
|
||||
self, "Could not update flight plan", str(ex), QMessageBox.Ok
|
||||
)
|
||||
|
||||
def update_flight_plan(self) -> None:
|
||||
planner = FlightPlanBuilder(self.game, self.package_model.package,
|
||||
is_player=True)
|
||||
planner.populate_flight_plan(self.flight)
|
||||
16
qt_ui/windows/mission/flight/settings/QCustomName.py
Normal file
16
qt_ui/windows/mission/flight/settings/QCustomName.py
Normal file
@ -0,0 +1,16 @@
|
||||
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
class QFlightCustomName(QGroupBox):
|
||||
|
||||
def __init__(self, flight: Flight):
|
||||
super(QFlightCustomName, self).__init__()
|
||||
|
||||
self.flight = flight
|
||||
|
||||
self.layout = QHBoxLayout()
|
||||
self.custom_name_label = QLabel(f"Custom Name: {flight.custom_name}")
|
||||
self.layout.addWidget(self.custom_name_label)
|
||||
self.setLayout(self.layout)
|
||||
@ -1,32 +0,0 @@
|
||||
import datetime
|
||||
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout
|
||||
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
|
||||
# TODO: Remove?
|
||||
class QFlightDepartureDisplay(QGroupBox):
|
||||
|
||||
def __init__(self, package: Package, flight: Flight):
|
||||
super().__init__("Departure")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
departure_row = QHBoxLayout()
|
||||
layout.addLayout(departure_row)
|
||||
|
||||
estimator = TotEstimator(package)
|
||||
delay = estimator.mission_start_time(flight)
|
||||
|
||||
departure_row.addWidget(QLabel(
|
||||
f"Departing from <b>{flight.from_cp.name}</b>"
|
||||
))
|
||||
departure_row.addWidget(QLabel(f"At T+{delay}"))
|
||||
|
||||
layout.addWidget(QLabel("Determined based on the package TOT. Edit the "
|
||||
"package to adjust the TOT."))
|
||||
|
||||
self.setLayout(layout)
|
||||
@ -1,15 +1,20 @@
|
||||
import logging
|
||||
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout
|
||||
from PySide2.QtWidgets import QLabel, QGroupBox, QSpinBox, QGridLayout
|
||||
|
||||
from game import Game
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import PackageModel
|
||||
|
||||
|
||||
class QFlightSlotEditor(QGroupBox):
|
||||
|
||||
changed = Signal()
|
||||
|
||||
def __init__(self, flight, game):
|
||||
super(QFlightSlotEditor, self).__init__("Slots")
|
||||
def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
|
||||
super().__init__("Slots")
|
||||
self.package_model = package_model
|
||||
self.flight = flight
|
||||
self.game = game
|
||||
self.inventory = self.game.aircraft_inventory.for_control_point(
|
||||
@ -22,14 +27,14 @@ class QFlightSlotEditor(QGroupBox):
|
||||
|
||||
layout = QGridLayout()
|
||||
|
||||
self.aircraft_count = QLabel("Aircraft count :")
|
||||
self.aircraft_count = QLabel("Aircraft count:")
|
||||
self.aircraft_count_spinner = QSpinBox()
|
||||
self.aircraft_count_spinner.setMinimum(1)
|
||||
self.aircraft_count_spinner.setMaximum(max_count)
|
||||
self.aircraft_count_spinner.setValue(flight.count)
|
||||
self.aircraft_count_spinner.valueChanged.connect(self._changed_aircraft_count)
|
||||
|
||||
self.client_count = QLabel("Client slots count :")
|
||||
self.client_count = QLabel("Client slots count:")
|
||||
self.client_count_spinner = QSpinBox()
|
||||
self.client_count_spinner.setMinimum(0)
|
||||
self.client_count_spinner.setMaximum(max_count)
|
||||
@ -70,6 +75,7 @@ class QFlightSlotEditor(QGroupBox):
|
||||
def _changed_client_count(self):
|
||||
self.flight.client_count = int(self.client_count_spinner.value())
|
||||
self._cap_client_count()
|
||||
self.package_model.update_tot()
|
||||
self.changed.emit()
|
||||
|
||||
def _cap_client_count(self):
|
||||
|
||||
@ -1,18 +1,25 @@
|
||||
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QComboBox, QLabel
|
||||
from dcs.mission import StartType
|
||||
from PySide2.QtWidgets import (
|
||||
QComboBox,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import PackageModel
|
||||
|
||||
|
||||
class QFlightStartType(QGroupBox):
|
||||
|
||||
def __init__(self, flight:Flight):
|
||||
super(QFlightStartType, self).__init__()
|
||||
|
||||
def __init__(self, package_model: PackageModel, flight: Flight):
|
||||
super().__init__()
|
||||
self.package_model = package_model
|
||||
self.flight = flight
|
||||
|
||||
self.layout = QHBoxLayout()
|
||||
self.start_type_label = QLabel("Start type : ")
|
||||
self.layout = QVBoxLayout()
|
||||
self.main_row = QHBoxLayout()
|
||||
self.start_type_label = QLabel("Start type:")
|
||||
self.start_type = QComboBox()
|
||||
|
||||
for i, st in enumerate([b for b in ["Cold", "Warm", "Runway", "In Flight"]]):
|
||||
@ -21,13 +28,17 @@ class QFlightStartType(QGroupBox):
|
||||
self.start_type.setCurrentIndex(i)
|
||||
|
||||
self.start_type.currentTextChanged.connect(self._on_start_type_selected)
|
||||
self.layout.addWidget(self.start_type_label)
|
||||
self.layout.addWidget(self.start_type)
|
||||
self.main_row.addWidget(self.start_type_label)
|
||||
self.main_row.addWidget(self.start_type)
|
||||
|
||||
self.layout.addLayout(self.main_row)
|
||||
self.layout.addWidget(QLabel(
|
||||
"Any option other than Cold will make this flight non-targetable " +
|
||||
"by OCA/Aircraft missions. This will affect game balance."
|
||||
))
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def _on_start_type_selected(self):
|
||||
selected = self.start_type.currentData()
|
||||
self.flight.start_type = selected
|
||||
|
||||
|
||||
|
||||
self.package_model.update_tot()
|
||||
|
||||
@ -2,38 +2,34 @@ from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout, QVBoxLayout
|
||||
|
||||
from game import Game
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.windows.mission.flight.settings.QFlightDepartureDisplay import \
|
||||
QFlightDepartureDisplay
|
||||
from qt_ui.models import PackageModel
|
||||
from qt_ui.windows.mission.flight.settings.FlightAirfieldDisplay import \
|
||||
FlightAirfieldDisplay
|
||||
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import \
|
||||
QFlightSlotEditor
|
||||
from qt_ui.windows.mission.flight.settings.QFlightStartType import \
|
||||
QFlightStartType
|
||||
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import \
|
||||
QFlightTypeTaskInfo
|
||||
from qt_ui.windows.mission.flight.settings.QCustomName import \
|
||||
QFlightCustomName
|
||||
|
||||
|
||||
class QGeneralFlightSettingsTab(QFrame):
|
||||
on_flight_settings_changed = Signal()
|
||||
|
||||
def __init__(self, game: Game, package: Package, flight: Flight):
|
||||
def __init__(self, game: Game, package_model: PackageModel, flight: Flight):
|
||||
super().__init__()
|
||||
|
||||
layout = QGridLayout()
|
||||
flight_info = QFlightTypeTaskInfo(flight)
|
||||
flight_departure = QFlightDepartureDisplay(package, flight)
|
||||
flight_slots = QFlightSlotEditor(flight, game)
|
||||
flight_start_type = QFlightStartType(flight)
|
||||
layout.addWidget(flight_info, 0, 0)
|
||||
layout.addWidget(flight_departure, 1, 0)
|
||||
layout.addWidget(flight_slots, 2, 0)
|
||||
layout.addWidget(flight_start_type, 3, 0)
|
||||
layout.addWidget(QFlightTypeTaskInfo(flight), 0, 0)
|
||||
layout.addWidget(FlightAirfieldDisplay(game, package_model, flight), 1,
|
||||
0)
|
||||
layout.addWidget(QFlightSlotEditor(package_model, flight, game), 2, 0)
|
||||
layout.addWidget(QFlightStartType(package_model, flight), 3, 0)
|
||||
layout.addWidget(QFlightCustomName(flight), 4, 0)
|
||||
vstretch = QVBoxLayout()
|
||||
vstretch.addStretch()
|
||||
layout.addLayout(vstretch, 3, 0)
|
||||
layout.addLayout(vstretch, 5, 0)
|
||||
self.setLayout(layout)
|
||||
|
||||
flight_start_type.setEnabled(flight.client_count > 0)
|
||||
flight_slots.changed.connect(
|
||||
lambda: flight_start_type.setEnabled(flight.client_count > 0))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user