mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3060645a26 | ||
|
|
0c6c578e35 | ||
|
|
e08f134c32 | ||
|
|
23038671b3 | ||
|
|
ae24b5e1ea | ||
|
|
918903f831 | ||
|
|
f3241c286c | ||
|
|
e682176040 | ||
|
|
8bea0c5cf8 | ||
|
|
497fd56efc | ||
|
|
100f4b66fd | ||
|
|
e2797fd796 | ||
|
|
e827d6d1d0 | ||
|
|
b9609a00f6 | ||
|
|
aaa06a5e88 | ||
|
|
26851520c8 | ||
|
|
cf0b5dc931 | ||
|
|
b2592b506f | ||
|
|
e84551b301 | ||
|
|
cebcee8d32 | ||
|
|
d75c6d7e9a | ||
|
|
eed514a432 | ||
|
|
218e783bb1 | ||
|
|
e1e1689d23 | ||
|
|
5c7a87dbfd | ||
|
|
5e7f5a68c2 | ||
|
|
5b19e0fdcd | ||
|
|
648fad33d8 | ||
|
|
843bb30b99 | ||
|
|
0f093d5f54 | ||
|
|
3a09a15970 | ||
|
|
83acf8b93d | ||
|
|
1793703e47 | ||
|
|
6713ee155c | ||
|
|
17f8830fbf | ||
|
|
24d995828f | ||
|
|
ae379bd8f5 | ||
|
|
f2cebc82cc | ||
|
|
2fa2e16b21 | ||
|
|
f670ccd8a6 | ||
|
|
9ffaec6d16 | ||
|
|
60de35ddf3 | ||
|
|
41faf794cc | ||
|
|
be37b4b3e7 | ||
|
|
3598b07185 | ||
|
|
602f88dab9 | ||
|
|
a4f5c290b5 | ||
|
|
8e42df11f3 | ||
|
|
8c6daa0976 | ||
|
|
bb629a7be2 | ||
|
|
80a7d7da2d | ||
|
|
c45795434a | ||
|
|
04971f3f65 | ||
|
|
6bf5356d00 | ||
|
|
0cd7db633b | ||
|
|
d9967bda8f | ||
|
|
34b9d8c334 | ||
|
|
12ff6679e4 | ||
|
|
4f9a250a9e | ||
|
|
c622cacadc | ||
|
|
786fb4e1c0 | ||
|
|
75bc521fa5 | ||
|
|
71296f9180 | ||
|
|
86672d882e | ||
|
|
3fc82af3ac | ||
|
|
22835e7535 | ||
|
|
f979a0c23d | ||
|
|
5f1f2b34b7 | ||
|
|
9ab2fc1714 | ||
|
|
b25f484ef3 | ||
|
|
dd0b565de9 | ||
|
|
29d88fd61d | ||
|
|
7625725fd8 | ||
|
|
cef25bd2f3 | ||
|
|
1e9cee1003 | ||
|
|
15bcc2d947 | ||
|
|
763dbebf13 | ||
|
|
667addf25b | ||
|
|
1033923f68 | ||
|
|
d410f9d3c4 | ||
|
|
a1cc3412e8 | ||
|
|
a98c9ffe8d | ||
|
|
5d506ca97e | ||
|
|
51c0bfa970 | ||
|
|
5c8e783980 | ||
|
|
b7d95819c1 | ||
|
|
bb0bf66eac | ||
|
|
be7008722b | ||
|
|
5535282894 | ||
|
|
2fb5f2b738 | ||
|
|
e87621576d | ||
|
|
531625ff08 | ||
|
|
0d57f710aa | ||
|
|
0da431fe33 | ||
|
|
88f984b0a8 | ||
|
|
ddb9d6968b | ||
|
|
3313535b27 | ||
|
|
cccc6e5a44 | ||
|
|
8b802a9cbc | ||
|
|
0a3a56d4bc | ||
|
|
7bae6cd562 | ||
|
|
e8151678dd | ||
|
|
9dd6022907 | ||
|
|
48e0d579cf | ||
|
|
cc9f61005b | ||
|
|
48ba8ff255 | ||
|
|
3f570cce5e | ||
|
|
01ee24c8cf | ||
|
|
1deff6fa49 | ||
|
|
4404d31c7b | ||
|
|
989562b90f | ||
|
|
bd63e4484d | ||
|
|
df642fe7a1 | ||
|
|
cb8f2619f2 | ||
|
|
2aed461b3a | ||
|
|
f4d75a2c6f | ||
|
|
e36d51a7c2 | ||
|
|
7267b70066 | ||
|
|
62d1a89269 | ||
|
|
6d0ac603f2 | ||
|
|
37f2e5bf3a | ||
|
|
9ddaf50ad4 | ||
|
|
42f488df0b | ||
|
|
d2a7de2775 | ||
|
|
ee1fc53dd3 | ||
|
|
192741af36 | ||
|
|
cd4ace4ad5 | ||
|
|
39426dde45 | ||
|
|
bd4eb3e105 | ||
|
|
c37c56c879 | ||
|
|
d26fc84316 | ||
|
|
33586aaf4f | ||
|
|
650b06d7c8 | ||
|
|
09807b25a8 | ||
|
|
13d42c3f83 | ||
|
|
6966b14c6b | ||
|
|
00ab0c4be2 | ||
|
|
5c06e74659 | ||
|
|
b0a0050725 | ||
|
|
c5a7fbd15a | ||
|
|
fa093ea237 |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -3,7 +3,7 @@ name: Bug report
|
||||
description: >
|
||||
Use for any bug that happens after campaign generation. If the New Game wizard
|
||||
failed, use the "New Game wizard failed" template instead.
|
||||
labels: bug
|
||||
labels: ["Bug", "Priority Medium"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
2
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Campaign update submission
|
||||
about: Submit an update to a campaign you maintain.
|
||||
title: 'Update for <campaign name>'
|
||||
labels: campaign-update-submission
|
||||
labels: Campaign Update Submission
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/code_architecture.yml
vendored
2
.github/ISSUE_TEMPLATE/code_architecture.yml
vendored
@@ -2,7 +2,7 @@
|
||||
name: Code Architecture
|
||||
description: >
|
||||
Suggest a refactor, or other issues related to the codebase.
|
||||
labels: ["refactor", "python"]
|
||||
labels: ["Refactor", "Python", "Priority Low"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature Request
|
||||
description: >
|
||||
Suggest an idea for this project.
|
||||
labels: enhancement
|
||||
labels: ["Enhancement", "Priority Medium"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/mod_support.md
vendored
2
.github/ISSUE_TEMPLATE/mod_support.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Mod support request
|
||||
about: Request Retribution support for new mods, or updates to existing mods
|
||||
title: Add/update <mod name>
|
||||
labels: mod support
|
||||
labels: Mod Request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -3,7 +3,7 @@ name: New Game wizard failed
|
||||
description: >
|
||||
Use for bugs that prevent the "New Game" wizard from completing successfully.
|
||||
If the wizard completes without issue, use the normal bug report template.
|
||||
labels: bug
|
||||
labels: ["Bug", "Priority High"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -49,4 +49,15 @@ jobs:
|
||||
include_image: true
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
username: GitHub Build Notifier
|
||||
if: startsWith(github.ref_name, 'test')
|
||||
if: startsWith(github.ref_name, 'test')
|
||||
|
||||
- name: Send status to Discord
|
||||
uses: stegzilla/discord-notify@v2
|
||||
with:
|
||||
webhook_url: ${{ secrets.discord_webhook_alphas }}
|
||||
title: "Build #${{ github.run_number }} available!"
|
||||
message: "**Branch:** ${{ github.ref_name }}\n**Download link:\n** https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
|
||||
include_image: true
|
||||
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
|
||||
username: GitHub Build Notifier
|
||||
if: github.ref_name == 'dev'
|
||||
|
||||
68
changelog.md
68
changelog.md
@@ -1,3 +1,54 @@
|
||||
# Retribution 1.1.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Mission Generation]** Given a CAS flight was planned, delay ground force attack until first CAS flight is on station
|
||||
* **[Mission Generation]** Add option to switch ATFLIR to LITENING automatically for ground based F/A-18C flights
|
||||
* **[Mission Generation]** Add option to configure OPFOR autoplanner aggressiveness and have the AI take risks and plan missions against defended targets
|
||||
* **[Mission Generation]** Add option to configure the desired tanker on-station time in settings
|
||||
* **[Mission Generation]** Reserve GUARD frequency on VHF/UHF
|
||||
* **[Mission Generation]** Randomization in radio frequency allocation
|
||||
* **[Mission Generation]** Configurable number of Combined Arms slots
|
||||
* **[Mission Generation]** Enable spectating & F11 free camera when the "Allow external views" option is selected
|
||||
* **[Cheat Menu]** Option to instantly transfer squadrons across bases.
|
||||
* **[Modding]** Support for IDF Mod Project F-16I Sufa & F-16D v3.2 mod
|
||||
* **[Modding]** Support for F/A-18E/F/G mod version 2.1
|
||||
* **[Modding]** Support for Swedish Military Assets for DCS by Currenthill Version 1.10
|
||||
* **[UI]** Add selectable units in faction overview during campaign generation.
|
||||
* **[UI]** Add button to rename pilots in Air Wing's Squadron dialog.
|
||||
* **[UI]** Add clone buttons for flights & packages.
|
||||
* **[UI]** Editing of flight's custom name.
|
||||
* **[UI]** Introduce custom names for packages (purely for organizational purposes).
|
||||
* **[UI]** Configurable UHF frequency (225-400MHz) for Packages, Carriers, LHAs, FOBs & FARPs.
|
||||
* **[UI]** Configurable Intra-Flight frequency for Flights.
|
||||
* **[UI]** Configurable TACAN for Carriers, LHAs & Tankers.
|
||||
* **[UI]** Configurable ICLS for capable Carriers & LHAs.
|
||||
* **[UI]** Configurable LINK4 for Carriers.
|
||||
* **[Kneeboard]** Show package information in Support page
|
||||
* **[Kneeboard]** Show extra weather information on 'Mission info' page
|
||||
* **[Kneeboard]** Show BRC in 'RWY' column for aircraft carriers on 'Mission info' page
|
||||
* **[Campaign Design]** Ability to define designated CTLD zones for Control Points (Airbases & FOBs/FARPs)
|
||||
* **[Campaign Design]** Ability to define preset groups for specific TGOs, given the preset group is accessible for the faction and the task matches.
|
||||
* **[Campaign Management]** Additional options for automated budget management.
|
||||
* **[Campaign Management]** New options to allow more control of randomized flight sizes (applicable for BARCAP/CAS/OCA/ANTI-SHIP).
|
||||
* **[Plugins]** Updated Splash Damage script to v2.0 by RotorOps.
|
||||
* **[Mission Generation]** Improvements to DEAD & STRIKE flights, allowing AI to handle a larger variety of weapons.
|
||||
|
||||
## Fixes
|
||||
* **[UI]** Removed deprecated options
|
||||
* **[UI]** Add missing icon & banner for C130 Hercules mod
|
||||
* **[Mission Generation]** Avoid aircraft from being assigned to helicopter parking spots, resulting into air starts that usually crash.
|
||||
* **[Mission Generation]** Use stacking algorithm to create vertical separation between flights spawning mid-mission over their departure, usually resulting into mid-air collisions.
|
||||
* **[Mission Generation]** Fixed all callsigns being "Enfield 1-1" on dedicated servers.
|
||||
* **[Mission Generation]** Fixed AI ferry flights for helicopters when transferring to a FOB/FARP.
|
||||
* **[Mission Generation]** Fixed 'Uninitialized flight' exception when adding flights after aborted take-off.
|
||||
* **[Modding]** Fixed conflicts caused by HDS units
|
||||
* **[UX]** Gracefully handle corrupted preferences file.
|
||||
* **[Mission Generation]** Aircraft not using decoys during SEAD.
|
||||
* **[Campaign]** New campaign (1968 Yankee Station) by Adecarcer
|
||||
|
||||
# Retribution 1.0.1 (hotfix)
|
||||
* **[Mission Generation]** Fix serialization issue when STRIKE flight has no escorts
|
||||
|
||||
# Retribution 1.0.0
|
||||
|
||||
## Features/Improvements
|
||||
@@ -44,8 +95,25 @@ Saves from 6.x are not compatible with 7.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.3.37556.
|
||||
* **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports.
|
||||
* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation.
|
||||
* **[Mission Generation]** Units on the front line are now hidden on MFDs.
|
||||
* **[Mission Generation]** Preset radio channels will now be configured for both A-10C modules.
|
||||
* **[Mission Generation]** Both A-10C modules now use separate radios for inter- and intra-flight comms (similar to other modern aircraft).
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
|
||||
* **[Modding]** Add support for VSN F-4B and F-4C mod.
|
||||
|
||||
## Fixes
|
||||
|
||||
# 6.1.1
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Data]** Fixed unit ID for the KS-19 AAA. KS-19 would not previously generate correctly in missions. A new game is required for this fix to take effect.
|
||||
* **[Flight Planning]** Automatic flight planning will no longer accidentally plan a recovery tanker instead of a theater refueling package. This fixes a potential crash during mission generation when opfor plans a refueling task at a sunk carrier. You'll need to skip the current turn to force opfor to replan their flights to get the fix.
|
||||
* **[Mission Generation]** Using heliports (airports without any runways) will no longer cause mission generation to fail.
|
||||
|
||||
# 6.1.0
|
||||
|
||||
Saves from 6.0.0 are compatible with 6.1.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DCS Liberation Client
|
||||
# DCS Retribution Client
|
||||
|
||||
This is a React app for the front-end of DCS Liberation. It is a work in
|
||||
This is a React app for the front-end of DCS Retribution. It is a work in
|
||||
progress that just barely implements the map. This is not useful for players
|
||||
yet.
|
||||
|
||||
|
||||
19589
client/package-lock.json
generated
19589
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -135,12 +135,14 @@ from pydcs_extensions.f16i_idf.f16i_idf import (
|
||||
F_16D_52,
|
||||
F_16D_50_NS,
|
||||
F_16D_52_NS,
|
||||
F_16D_Barak,
|
||||
F_16D_Barak_30,
|
||||
F_16D_Barak_40,
|
||||
F_16I,
|
||||
)
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.f4b.f4b import VSN_F4B
|
||||
from pydcs_extensions.f4b.f4b import VSN_F4B, VSN_F4C
|
||||
from pydcs_extensions.f84g.f84g import VSN_F84G
|
||||
from pydcs_extensions.fa18efg.fa18efg import FA_18E, FA_18F, EA_18G
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.su30.su30 import Su_30MKA, Su_30MKI, Su_30MKM, Su_30SM
|
||||
@@ -175,17 +177,21 @@ ESCORT_CAPABLE = [
|
||||
MiG_29S,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
JAS39Gripen,
|
||||
F_16A_MLU,
|
||||
F_16A,
|
||||
F_4E,
|
||||
VSN_F4C,
|
||||
VSN_F4B,
|
||||
MiG_31,
|
||||
MiG_25PD,
|
||||
@@ -249,11 +255,14 @@ CAS_CAPABLE = [
|
||||
F_15E,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
Tornado_GR4,
|
||||
Tornado_IDS,
|
||||
@@ -332,13 +341,14 @@ CAS_CAPABLE = [
|
||||
MQ_9_Reaper,
|
||||
RQ_1A_Predator,
|
||||
VSN_A6A,
|
||||
VSN_F4C,
|
||||
VSN_F4B,
|
||||
VSN_F100,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F104S_AG,
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F4B,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
@@ -348,11 +358,15 @@ SEAD_ESCORT_CAPABLE = [
|
||||
JF_17,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
EA_18G,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
Tornado_IDS,
|
||||
Su_25T,
|
||||
@@ -432,11 +446,14 @@ STRIKE_CAPABLE = [
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
@@ -465,6 +482,8 @@ STRIKE_CAPABLE = [
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
VSN_F4C,
|
||||
VSN_F4B,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
Bronco_OV_10A,
|
||||
@@ -506,7 +525,6 @@ STRIKE_CAPABLE = [
|
||||
VSN_F104C,
|
||||
VSN_F105G,
|
||||
VSN_F105D,
|
||||
VSN_F4B,
|
||||
VSN_F84G,
|
||||
]
|
||||
|
||||
@@ -516,6 +534,8 @@ ANTISHIP_CAPABLE = [
|
||||
Tu_142,
|
||||
Tu_22M3,
|
||||
H_6J,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
JAS39Gripen_AG,
|
||||
F_16A_MLU,
|
||||
@@ -565,11 +585,14 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
AJS37,
|
||||
F_16C_50,
|
||||
F_16I,
|
||||
F_16D_Barak,
|
||||
F_16D_Barak_40,
|
||||
F_16D_Barak_30,
|
||||
F_16D_50,
|
||||
F_16D_50_NS,
|
||||
F_16D_52,
|
||||
F_16D_52_NS,
|
||||
FA_18E,
|
||||
FA_18F,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
@@ -598,6 +621,8 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
VSN_F4C,
|
||||
VSN_F4B,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
Bronco_OV_10A,
|
||||
@@ -631,7 +656,6 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
VSN_F104G,
|
||||
VSN_F104C,
|
||||
VSN_F100,
|
||||
VSN_F4B,
|
||||
]
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
@@ -667,7 +691,6 @@ AIR_ASSAULT_CAPABLE = [
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Hercules,
|
||||
C_47,
|
||||
]
|
||||
|
||||
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
|
||||
|
||||
@@ -5,12 +5,17 @@ from datetime import datetime, timedelta
|
||||
from typing import Any, List, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs import Point
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33
|
||||
from dcs.planes import C_101CC, C_101EB, Su_33, FA_18C_hornet
|
||||
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from .flightroster import FlightRoster
|
||||
from .flightstate import FlightState, Navigating, Uninitialized
|
||||
from .flightstate.killed import Killed
|
||||
from .loadouts import Loadout
|
||||
from .loadouts import Loadout, Weapon
|
||||
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from ..radio.TacanContainer import TacanContainer
|
||||
from ..radio.radios import RadioFrequency
|
||||
from ..radio.tacan import TacanChannel
|
||||
from ..sidc import (
|
||||
Entity,
|
||||
SidcDescribable,
|
||||
@@ -18,9 +23,9 @@ from ..sidc import (
|
||||
Status,
|
||||
SymbolSet,
|
||||
)
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.sim.gameupdateevents import GameUpdateEvents
|
||||
from game.sim.simulationresults import SimulationResults
|
||||
from game.squadrons import Squadron, Pilot
|
||||
@@ -32,8 +37,10 @@ if TYPE_CHECKING:
|
||||
from .package import Package
|
||||
from .starttype import StartType
|
||||
|
||||
F18_TGP_PYLON: int = 4
|
||||
|
||||
class Flight(SidcDescribable):
|
||||
|
||||
class Flight(SidcDescribable, RadioFrequencyContainer, TacanContainer):
|
||||
def __init__(
|
||||
self,
|
||||
package: Package,
|
||||
@@ -46,6 +53,9 @@ class Flight(SidcDescribable):
|
||||
custom_name: Optional[str] = None,
|
||||
cargo: Optional[TransferOrder] = None,
|
||||
roster: Optional[FlightRoster] = None,
|
||||
frequency: Optional[RadioFrequency] = None,
|
||||
channel: Optional[TacanChannel] = None,
|
||||
callsign: Optional[str] = None,
|
||||
) -> None:
|
||||
self.id = uuid.uuid4()
|
||||
self.package = package
|
||||
@@ -65,6 +75,11 @@ class Flight(SidcDescribable):
|
||||
self.custom_name = custom_name
|
||||
self.group_id: int = 0
|
||||
|
||||
self.frequency = frequency
|
||||
if self.unit_type.dcs_unit_type.tacan:
|
||||
self.tacan = channel
|
||||
self.tcn_name = callsign
|
||||
|
||||
# Only used by transport missions.
|
||||
self.cargo = cargo
|
||||
|
||||
@@ -87,6 +102,15 @@ class Flight(SidcDescribable):
|
||||
|
||||
self._flight_plan_builder = FlightPlanBuilderTypes.for_flight(self)(self)
|
||||
|
||||
is_f18 = self.squadron.aircraft.dcs_unit_type.id == FA_18C_hornet.id
|
||||
on_land = not self.squadron.location.is_fleet
|
||||
if on_land and is_f18 and self.coalition.game.settings.atflir_autoswap:
|
||||
self.loadout.pylons[F18_TGP_PYLON] = Weapon.with_clsid(
|
||||
str(
|
||||
FA_18C_hornet.Pylon4.AN_AAQ_28_LITENING___Targeting_Pod_[1]["clsid"]
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def flight_plan(self) -> FlightPlan[Any]:
|
||||
return self._flight_plan_builder.get_or_build()
|
||||
@@ -150,6 +174,10 @@ class Flight(SidcDescribable):
|
||||
def is_helo(self) -> bool:
|
||||
return self.unit_type.dcs_unit_type.helicopter
|
||||
|
||||
@property
|
||||
def is_hercules(self) -> bool:
|
||||
return self.unit_type == AircraftType.named("C-130J-30 Super Hercules")
|
||||
|
||||
@property
|
||||
def from_cp(self) -> ControlPoint:
|
||||
return self.departure
|
||||
@@ -186,17 +214,18 @@ class Flight(SidcDescribable):
|
||||
return Su_33.fuel_max * 0.8
|
||||
elif unit_type in {C_101EB, C_101CC}:
|
||||
return unit_type.fuel_max * 0.5
|
||||
elif unit_type == Hercules:
|
||||
return unit_type.fuel_max * 0.75
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
string = f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
return f"{self.custom_name} - {string}"
|
||||
return string
|
||||
|
||||
def abort(self) -> None:
|
||||
from .flightplans.rtb import RtbFlightPlan
|
||||
@@ -254,3 +283,15 @@ class Flight(SidcDescribable):
|
||||
|
||||
def recreate_flight_plan(self) -> None:
|
||||
self._flight_plan_builder.regenerate()
|
||||
|
||||
@staticmethod
|
||||
def clone_flight(flight: Flight) -> Flight:
|
||||
return Flight(
|
||||
flight.package,
|
||||
flight.country,
|
||||
flight.squadron,
|
||||
flight.count,
|
||||
flight.flight_type,
|
||||
flight.start_type,
|
||||
flight.divert,
|
||||
)
|
||||
|
||||
20
game/ato/flightplans/_common_ctld.py
Normal file
20
game/ato/flightplans/_common_ctld.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Union, TYPE_CHECKING, Tuple
|
||||
|
||||
from game.theater.interfaces.CTLD import CTLD
|
||||
from game.theater import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dcs import Point
|
||||
|
||||
|
||||
def generate_random_ctld_point(cp: Union[ControlPoint, CTLD]) -> Point:
|
||||
if isinstance(cp, CTLD) and cp.ctld_zones:
|
||||
zone: Tuple[Point, float] = random.choice(cp.ctld_zones)
|
||||
pos, radius = zone
|
||||
return pos.random_point_within(radius)
|
||||
elif isinstance(cp, CTLD) and isinstance(cp, ControlPoint):
|
||||
return cp.position.random_point_within(2000, 200)
|
||||
raise RuntimeError("Could not generate CTLD point")
|
||||
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from ._common_ctld import generate_random_ctld_point
|
||||
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
|
||||
from game.theater.controlpoint import ControlPointType
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
@@ -13,8 +14,10 @@ from .planningerror import PlanningError
|
||||
from .uizonedisplay import UiZone, UiZoneDisplay
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ..flightwaypoint import FlightWaypointType
|
||||
from ...theater.interfaces.CTLD import CTLD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dcs import Point
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@@ -25,7 +28,7 @@ class AirAssaultLayout(StandardLayout):
|
||||
pickup: FlightWaypoint | None
|
||||
nav_to_ingress: list[FlightWaypoint]
|
||||
ingress: FlightWaypoint
|
||||
drop_off: FlightWaypoint
|
||||
drop_off: FlightWaypoint | None
|
||||
# This is an implementation detail used by CTLD. The aircraft will not go to this
|
||||
# waypoint. It is used by CTLD as the destination for unloaded troops.
|
||||
target: FlightWaypoint
|
||||
@@ -37,7 +40,8 @@ class AirAssaultLayout(StandardLayout):
|
||||
yield self.pickup
|
||||
yield from self.nav_to_ingress
|
||||
yield self.ingress
|
||||
yield self.drop_off
|
||||
if self.drop_off is not None:
|
||||
yield self.drop_off
|
||||
yield self.target
|
||||
yield from self.nav_to_home
|
||||
yield self.arrival
|
||||
@@ -53,7 +57,9 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.drop_off
|
||||
if self.flight.is_helo and self.layout.drop_off is not None:
|
||||
return self.layout.drop_off
|
||||
return self.layout.target
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
@@ -80,8 +86,10 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
||||
|
||||
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
def layout(self) -> AirAssaultLayout:
|
||||
if not self.flight.is_helo:
|
||||
raise PlanningError("Air assault is only usable by helicopters")
|
||||
if not self.flight.is_helo and not self.flight.is_hercules:
|
||||
raise PlanningError(
|
||||
"Air assault is only usable by helicopters and Anubis' C-130 mod"
|
||||
)
|
||||
assert self.package.waypoints is not None
|
||||
|
||||
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
|
||||
@@ -89,7 +97,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
if self.flight.departure.cptype in [
|
||||
if self.flight.is_hercules or self.flight.departure.cptype in [
|
||||
ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
ControlPointType.LHA_GROUP,
|
||||
ControlPointType.OFF_MAP,
|
||||
@@ -99,27 +107,25 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
pickup = None
|
||||
pickup_position = self.flight.departure.position
|
||||
else:
|
||||
# TODO The calculation of the Pickup LZ is currently randomized. This
|
||||
# leads to the problem that we can not gurantee that the LZ is clear of
|
||||
# obstacles. This has to be improved in the future so that the Mission can
|
||||
# be autoplanned. In the current state the User has to check the created
|
||||
# Waypoints for the Pickup and Dropoff LZs are free of obstacles.
|
||||
# Create a special pickup zone for Helos from Airbase / FOB
|
||||
pickup = builder.pickup_zone(
|
||||
MissionTarget(
|
||||
"Pickup Zone",
|
||||
self.flight.departure.position.random_point_within(1200, 600),
|
||||
self._generate_ctld_pickup(),
|
||||
)
|
||||
)
|
||||
pickup_position = pickup.position
|
||||
assault_area = builder.assault_area(self.package.target)
|
||||
heading = self.package.target.position.heading_between_point(pickup_position)
|
||||
if self.flight.is_hercules:
|
||||
assault_area.only_for_player = False
|
||||
assault_area.alt = feet(1000)
|
||||
|
||||
# TODO we can not gurantee a safe LZ for DropOff. See comment above.
|
||||
# TODO: define CTLD dropoff zones in campaign miz?
|
||||
drop_off_zone = MissionTarget(
|
||||
"Dropoff zone",
|
||||
self.package.target.position.point_from_heading(heading, 1200),
|
||||
)
|
||||
dz = builder.dropoff_zone(drop_off_zone) if self.flight.is_helo else None
|
||||
|
||||
return AirAssaultLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
@@ -135,7 +141,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
self.package.waypoints.ingress,
|
||||
self.package.target,
|
||||
),
|
||||
drop_off=builder.dropoff_zone(drop_off_zone),
|
||||
drop_off=dz,
|
||||
target=assault_area,
|
||||
nav_to_home=builder.nav_path(
|
||||
drop_off_zone.position,
|
||||
@@ -150,3 +156,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
||||
|
||||
def build(self) -> AirAssaultFlightPlan:
|
||||
return AirAssaultFlightPlan(self.flight, self.layout())
|
||||
|
||||
def _generate_ctld_pickup(self) -> Point:
|
||||
assert isinstance(self.flight.departure, CTLD)
|
||||
return generate_random_ctld_point(self.flight.departure)
|
||||
|
||||
@@ -5,15 +5,18 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from ._common_ctld import generate_random_ctld_point
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.utils import feet
|
||||
from .ibuilder import IBuilder
|
||||
from .planningerror import PlanningError
|
||||
from .standard import StandardFlightPlan, StandardLayout
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from ...theater.interfaces.CTLD import CTLD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..flightwaypoint import FlightWaypoint
|
||||
from dcs import Point
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -106,15 +109,10 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
if self.flight.is_helo:
|
||||
# Create CTLD Zones for Helo flights
|
||||
pickup_zone = builder.pickup_zone(
|
||||
MissionTarget(
|
||||
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
|
||||
)
|
||||
MissionTarget("Pickup Zone", self._generate_ctld_pickup())
|
||||
)
|
||||
drop_off_zone = builder.dropoff_zone(
|
||||
MissionTarget(
|
||||
"Dropoff zone",
|
||||
cargo.next_stop.position.random_point_within(1000, 200),
|
||||
)
|
||||
MissionTarget("Dropoff zone", self._generate_ctld_dropoff())
|
||||
)
|
||||
# Show the zone waypoints only to the player
|
||||
pickup_zone.only_for_player = True
|
||||
@@ -153,3 +151,15 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
|
||||
|
||||
def build(self) -> AirliftFlightPlan:
|
||||
return AirliftFlightPlan(self.flight, self.layout())
|
||||
|
||||
def _generate_ctld_pickup(self) -> Point:
|
||||
cargo = self.flight.cargo
|
||||
if cargo and cargo.origin and isinstance(cargo.origin, CTLD):
|
||||
return generate_random_ctld_point(cargo.origin)
|
||||
raise RuntimeError("Could not generate CTLD pickup")
|
||||
|
||||
def _generate_ctld_dropoff(self) -> Point:
|
||||
cargo = self.flight.cargo
|
||||
if cargo and cargo.transport and isinstance(cargo.transport.destination, CTLD):
|
||||
return generate_random_ctld_point(cargo.transport.destination)
|
||||
raise RuntimeError("Could not generate CTLD dropoff")
|
||||
|
||||
@@ -8,8 +8,6 @@ from .formationattack import (
|
||||
FormationAttackLayout,
|
||||
)
|
||||
from .waypointbuilder import WaypointBuilder
|
||||
from .. import FlightType
|
||||
from ...utils import Distance
|
||||
|
||||
|
||||
class EscortFlightPlan(FormationAttackFlightPlan):
|
||||
@@ -32,11 +30,9 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
||||
join = builder.join(self.package.waypoints.join)
|
||||
split = builder.split(self.package.waypoints.split)
|
||||
refuel = builder.refuel(self.package.waypoints.refuel)
|
||||
initial = None
|
||||
if self.package.primary_task == FlightType.STRIKE:
|
||||
initial = builder.escort_hold(
|
||||
self.package.waypoints.initial, Distance.from_feet(20000)
|
||||
)
|
||||
initial = builder.escort_hold(
|
||||
self.package.waypoints.initial, self.doctrine.ingress_altitude
|
||||
)
|
||||
|
||||
return FormationAttackLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
|
||||
@@ -72,6 +72,15 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
"""A list of all waypoints in the flight plan, in order."""
|
||||
return list(self.iter_waypoints())
|
||||
|
||||
def get_index_of_wpt_by_type(self, wpt_type: FlightWaypointType) -> int:
|
||||
index = 0
|
||||
for wpt in self.waypoints:
|
||||
if wpt and not wpt.only_for_player:
|
||||
index += 1
|
||||
if wpt.waypoint_type == wpt_type:
|
||||
return index
|
||||
return -1
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
"""Iterates over all waypoints in the flight plan, in order."""
|
||||
yield from self.layout.iter_waypoints()
|
||||
|
||||
@@ -26,12 +26,13 @@ from .theaterrefueling import TheaterRefuelingFlightPlan
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight
|
||||
from game.theater import FrontLine
|
||||
|
||||
|
||||
class FlightPlanBuilderTypes:
|
||||
@staticmethod
|
||||
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
|
||||
from game.theater import FrontLine
|
||||
|
||||
if flight.flight_type is FlightType.REFUELING:
|
||||
if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
|
||||
flight.package.target, FrontLine
|
||||
|
||||
@@ -17,7 +17,7 @@ class TheaterRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
return timedelta(hours=1)
|
||||
return self.flight.coalition.game.settings.desired_tanker_on_station_time
|
||||
|
||||
|
||||
class Builder(IBuilder[TheaterRefuelingFlightPlan, PatrollingLayout]):
|
||||
|
||||
@@ -14,6 +14,7 @@ from typing import (
|
||||
|
||||
from dcs.mapping import Point, Vector2
|
||||
|
||||
from game.ato.flightplans._common_ctld import generate_random_ctld_point
|
||||
from game.ato.flightwaypoint import AltitudeReference, FlightWaypoint
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.theater import (
|
||||
@@ -23,7 +24,8 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
TheaterUnit,
|
||||
)
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.theater.interfaces.CTLD import CTLD
|
||||
from game.utils import Distance, meters, nautical_miles, feet
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
@@ -127,10 +129,9 @@ class WaypointBuilder:
|
||||
position = divert.position
|
||||
altitude_type: AltitudeReference
|
||||
if isinstance(divert, OffMapSpawn):
|
||||
if self.is_helo:
|
||||
altitude = meters(500)
|
||||
else:
|
||||
altitude = self.doctrine.rendezvous_altitude
|
||||
altitude = (
|
||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude
|
||||
)
|
||||
altitude_type = "BARO"
|
||||
else:
|
||||
altitude = meters(0)
|
||||
@@ -225,15 +226,17 @@ class WaypointBuilder:
|
||||
position: Point,
|
||||
objective: MissionTarget,
|
||||
) -> FlightWaypoint:
|
||||
alt = self.doctrine.ingress_altitude
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
if self.is_helo or self.flight.is_hercules:
|
||||
alt_type = "RADIO"
|
||||
alt = meters(60) if self.is_helo else feet(1000)
|
||||
|
||||
return FlightWaypoint(
|
||||
"INGRESS",
|
||||
ingress_type,
|
||||
position,
|
||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
||||
alt,
|
||||
alt_type,
|
||||
description=f"INGRESS on {objective.name}",
|
||||
pretty_name=f"INGRESS on {objective.name}",
|
||||
@@ -555,10 +558,14 @@ class WaypointBuilder:
|
||||
"""Creates a cargo stop waypoint.
|
||||
This waypoint is used by AirLift as a landing and stopover waypoint
|
||||
"""
|
||||
if isinstance(control_point, CTLD) and control_point.ctld_zones:
|
||||
pos = generate_random_ctld_point(control_point)
|
||||
else:
|
||||
pos = control_point.position
|
||||
return FlightWaypoint(
|
||||
"CARGOSTOP",
|
||||
FlightWaypointType.CARGO_STOP,
|
||||
control_point.position,
|
||||
pos,
|
||||
meters(0),
|
||||
"RADIO",
|
||||
description=f"Stop for cargo at {control_point.name}",
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import datetime
|
||||
import logging
|
||||
from collections.abc import Iterable
|
||||
from typing import Iterator, Mapping, Optional, TYPE_CHECKING, Type, Dict
|
||||
from typing import Iterator, Optional, TYPE_CHECKING, Type, Dict
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
@@ -19,14 +19,14 @@ class Loadout:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
pylons: Mapping[int, Optional[Weapon]],
|
||||
pylons: Dict[int, Optional[Weapon]],
|
||||
date: Optional[datetime.date],
|
||||
is_custom: bool = False,
|
||||
) -> None:
|
||||
self.name = name
|
||||
# We clear unused pylon entries on initialization, but UI actions can still
|
||||
# cause a pylon to be emptied, so make the optional type explicit.
|
||||
self.pylons: Mapping[int, Optional[Weapon]] = {
|
||||
self.pylons: Dict[int, Optional[Weapon]] = {
|
||||
k: v for k, v in pylons.items() if v is not None
|
||||
}
|
||||
self.date = date
|
||||
|
||||
@@ -13,19 +13,28 @@ from .flightplans.formation import FormationFlightPlan
|
||||
from .flighttype import FlightType
|
||||
from .packagewaypoints import PackageWaypoints
|
||||
from .traveltime import TotEstimator
|
||||
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from ..radio.radios import RadioFrequency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
|
||||
|
||||
class Package:
|
||||
class Package(RadioFrequencyContainer):
|
||||
"""A mission package."""
|
||||
|
||||
def __init__(
|
||||
self, target: MissionTarget, db: Database[Flight], auto_asap: bool = False
|
||||
self,
|
||||
target: MissionTarget,
|
||||
db: Database[Flight],
|
||||
auto_asap: bool = False,
|
||||
custom_name: str | None = None,
|
||||
frequency: RadioFrequency | None = None,
|
||||
) -> None:
|
||||
self.target = target
|
||||
self._db = db
|
||||
self.frequency = frequency
|
||||
self.custom_name = custom_name
|
||||
|
||||
# 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
|
||||
@@ -214,3 +223,12 @@ class Package:
|
||||
if flight.departure == airfield:
|
||||
return airfield
|
||||
raise RuntimeError("Could not find any airfield assigned to this package")
|
||||
|
||||
@staticmethod
|
||||
def clone_package(package: Package) -> Package:
|
||||
clone = Package(package.target, package._db, package.auto_asap)
|
||||
clone.time_over_target = package.time_over_target
|
||||
for f in package.flights:
|
||||
cf = Flight.clone_flight(f)
|
||||
clone.add_flight(cf)
|
||||
return clone
|
||||
|
||||
@@ -15,7 +15,7 @@ def callsign_for_support_unit(group: FlyingGroup[Any]) -> str:
|
||||
try:
|
||||
return f"Flight {int(raw_callsign)}"
|
||||
except ValueError:
|
||||
return raw_callsign.rstrip("1234567890")
|
||||
return f"{raw_callsign[:-2]} {raw_callsign[-2]}"
|
||||
|
||||
|
||||
def create_group_callsign_from_unit(lead: FlyingUnit) -> str:
|
||||
|
||||
@@ -19,6 +19,7 @@ from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
|
||||
from game.theater.theaterloader import TheaterLoader
|
||||
from game.version import CAMPAIGN_FORMAT_VERSION
|
||||
from .campaignairwingconfig import CampaignAirWingConfig
|
||||
from .campaigngroundconfig import TgoConfig
|
||||
from .mizcampaignloader import MizCampaignLoader
|
||||
|
||||
PERF_FRIENDLY = 0
|
||||
@@ -32,6 +33,7 @@ DEFAULT_BUDGET = 2000
|
||||
class Campaign:
|
||||
name: str
|
||||
menu_thumbnail_dcs_relative_path: Path
|
||||
fallback_icon_path: Path
|
||||
authors: str
|
||||
description: str
|
||||
|
||||
@@ -90,6 +92,7 @@ class Campaign:
|
||||
return cls(
|
||||
data["name"],
|
||||
TheaterLoader(data["theater"].lower()).menu_thumbnail_dcs_relative_path,
|
||||
TheaterLoader(data["theater"].lower()).icon_path,
|
||||
data.get("authors", "???"),
|
||||
data.get("description", ""),
|
||||
(version.major, version.minor),
|
||||
@@ -135,6 +138,13 @@ class Campaign:
|
||||
return CampaignAirWingConfig({})
|
||||
return CampaignAirWingConfig.from_campaign_data(squadron_data, theater)
|
||||
|
||||
def load_ground_forces_config(self) -> TgoConfig:
|
||||
ground_forces = self.data.get("ground_forces", {})
|
||||
if not ground_forces:
|
||||
logging.warning(f"Campaign {self.name} does not define any squadrons")
|
||||
return TgoConfig({})
|
||||
return TgoConfig.from_campaign_data(ground_forces)
|
||||
|
||||
@property
|
||||
def is_out_of_date(self) -> bool:
|
||||
"""Returns True if this campaign is not up to date with the latest format.
|
||||
@@ -171,7 +181,7 @@ class Campaign:
|
||||
@classmethod
|
||||
def iter_campaign_defs(cls) -> Iterator[Path]:
|
||||
yield from cls.iter_campaigns_in_dir(
|
||||
Path(persistency.base_path()) / "Liberation/Campaigns"
|
||||
Path(persistency.base_path()) / "Retribution/Campaigns"
|
||||
)
|
||||
yield from cls.iter_campaigns_in_dir(Path("resources/campaigns"))
|
||||
|
||||
|
||||
25
game/campaignloader/campaigngroundconfig.py
Normal file
25
game/campaignloader/campaigngroundconfig.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, Optional
|
||||
|
||||
from game.armedforces.forcegroup import ForceGroup
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TgoConfig:
|
||||
by_original_name: dict[str, ForceGroup]
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return self.by_original_name.__iter__()
|
||||
|
||||
def __getitem__(self, name: str) -> Optional[ForceGroup]:
|
||||
return self.by_original_name.get(name)
|
||||
|
||||
@classmethod
|
||||
def from_campaign_data(cls, data: dict[str, str]) -> TgoConfig:
|
||||
by_original_name: dict[str, ForceGroup] = defaultdict()
|
||||
for tgo_name, force_group in data.items():
|
||||
by_original_name[tgo_name] = ForceGroup.from_preset_group(force_group)
|
||||
return TgoConfig(by_original_name)
|
||||
@@ -29,7 +29,6 @@ class DefaultSquadronAssigner:
|
||||
self.coalition.player
|
||||
):
|
||||
for squadron_config in self.config.by_location[control_point]:
|
||||
|
||||
squadron_def = self.override_squadron_defaults(
|
||||
self.find_squadron_for(squadron_config, control_point),
|
||||
squadron_config,
|
||||
@@ -53,7 +52,6 @@ class DefaultSquadronAssigner:
|
||||
def find_squadron_for(
|
||||
self, config: SquadronConfig, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
|
||||
for preferred_aircraft in config.aircraft:
|
||||
squadron_def = self.find_preferred_squadron(
|
||||
preferred_aircraft, config.primary, control_point
|
||||
@@ -94,8 +92,9 @@ class DefaultSquadronAssigner:
|
||||
if aircraft not in self.coalition.faction.aircrafts:
|
||||
return None
|
||||
|
||||
lo = self.coalition.faction.liveries_overrides
|
||||
squadron_def = self.find_squadron_for_airframe(aircraft, task, control_point)
|
||||
if squadron_def is not None:
|
||||
if squadron_def is not None and lo.get(aircraft) is None:
|
||||
return squadron_def
|
||||
|
||||
# No premade squadron available for this aircraft that meets the requirements,
|
||||
@@ -153,7 +152,6 @@ class DefaultSquadronAssigner:
|
||||
def override_squadron_defaults(
|
||||
squadron_def: Optional[SquadronDef], config: SquadronConfig
|
||||
) -> Optional[SquadronDef]:
|
||||
|
||||
if squadron_def is None:
|
||||
return None
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ from game.utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.conflicttheater import ConflictTheater
|
||||
from dcs import Point
|
||||
|
||||
|
||||
class MizCampaignLoader:
|
||||
@@ -107,8 +108,12 @@ class MizCampaignLoader:
|
||||
if self.mission.country(self.RED_COUNTRY.name) is None:
|
||||
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
||||
|
||||
def control_point_from_airport(self, airport: Airport) -> ControlPoint:
|
||||
cp = Airfield(airport, self.theater, starts_blue=airport.is_blue())
|
||||
def control_point_from_airport(
|
||||
self, airport: Airport, ctld_zones: List[Tuple[Point, float]]
|
||||
) -> ControlPoint:
|
||||
cp = Airfield(
|
||||
airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones
|
||||
)
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
@@ -245,7 +250,8 @@ class MizCampaignLoader:
|
||||
control_points = {}
|
||||
for airport in self.mission.terrain.airport_list():
|
||||
if airport.is_blue() or airport.is_red():
|
||||
control_point = self.control_point_from_airport(airport)
|
||||
ctld_zones = self.get_ctld_zones(airport.name)
|
||||
control_point = self.control_point_from_airport(airport, ctld_zones)
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for blue in (False, True):
|
||||
@@ -268,8 +274,13 @@ class MizCampaignLoader:
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for fob in self.fobs(blue):
|
||||
ctld_zones = self.get_ctld_zones(fob.name)
|
||||
control_point = Fob(
|
||||
str(fob.name), fob.position, self.theater, starts_blue=blue
|
||||
str(fob.name),
|
||||
fob.position,
|
||||
self.theater,
|
||||
starts_blue=blue,
|
||||
ctld_zones=ctld_zones,
|
||||
)
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
@@ -460,3 +471,9 @@ class MizCampaignLoader:
|
||||
self.add_preset_locations()
|
||||
self.add_supply_routes()
|
||||
self.add_shipping_lanes()
|
||||
|
||||
def get_ctld_zones(self, prefix: str) -> List[Tuple[Point, float]]:
|
||||
zones = [t for t in self.mission.triggers.zones() if prefix + " CTLD" in t.name]
|
||||
for z in zones:
|
||||
self.mission.triggers.zones().remove(z)
|
||||
return [(z.position, z.radius) for z in zones]
|
||||
|
||||
@@ -5,6 +5,7 @@ import operator
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import IntEnum, auto, unique
|
||||
import random
|
||||
from typing import Generic, Iterator, Optional, TYPE_CHECKING, TypeVar, Union
|
||||
|
||||
from game.ato.flighttype import FlightType
|
||||
@@ -170,7 +171,24 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
|
||||
if not ignore_iads:
|
||||
for iads_threat in self.iter_iads_threats(state):
|
||||
threatened = True
|
||||
# Only consider blue faction flights threatened.
|
||||
# Red might still plan missions into hostile territory,
|
||||
# depending on the aggressiveness setting.
|
||||
if (
|
||||
state.context.coalition.player
|
||||
or random.randint(1, 100)
|
||||
> state.context.coalition.game.settings.opfor_autoplanner_aggressiveness
|
||||
):
|
||||
threatened = True
|
||||
if iads_threat not in state.threatening_air_defenses:
|
||||
state.threatening_air_defenses.append(iads_threat)
|
||||
return not threatened
|
||||
|
||||
def get_flight_size(self) -> int:
|
||||
settings = self.target.coalition.game.settings
|
||||
weights = [
|
||||
settings.fpa_2ship_weight,
|
||||
settings.fpa_3ship_weight,
|
||||
settings.fpa_4ship_weight,
|
||||
]
|
||||
return random.choices([2, 3, 4], weights, k=1)[0]
|
||||
|
||||
@@ -23,5 +23,6 @@ class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||
state.eliminate_ship(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.ANTISHIP, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.ANTISHIP, size)
|
||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||
|
||||
@@ -22,5 +22,6 @@ class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
||||
state.enemy_shipping.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.ANTISHIP, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.ANTISHIP, size)
|
||||
self.propose_common_escorts()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from random import randint
|
||||
|
||||
@@ -22,7 +23,8 @@ class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
||||
state.barcaps_needed[self.target] -= 1
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.BARCAP, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.BARCAP, size)
|
||||
|
||||
@property
|
||||
def purchase_multiplier(self) -> int:
|
||||
|
||||
@@ -30,5 +30,6 @@ class PlanCas(PackagePlanningTask[FrontLine]):
|
||||
state.vulnerable_front_lines.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.CAS, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.CAS, size)
|
||||
self.propose_flight(FlightType.TARCAP, 2)
|
||||
|
||||
@@ -24,7 +24,8 @@ class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
|
||||
state.oca_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.OCA_RUNWAY, randint(2, 4))
|
||||
size = self.get_flight_size()
|
||||
self.propose_flight(FlightType.OCA_RUNWAY, size)
|
||||
if self.aircraft_cold_start:
|
||||
self.propose_flight(FlightType.OCA_AIRCRAFT, 2)
|
||||
self.propose_common_escorts()
|
||||
|
||||
@@ -19,6 +19,18 @@ PydcsWeapon = Any
|
||||
PydcsWeaponAssignment = tuple[int, PydcsWeapon]
|
||||
|
||||
|
||||
def weapons_migrator(name: str) -> str:
|
||||
migration_map = {
|
||||
"AGM-88C HARM - High Speed Anti-Radiation Missile": "AGM-88C HARM",
|
||||
"Kh-25MPU (Updated AS-12 Kegler) - 320kg, ARM, IN & Pas Rdr": "Kh-25MPU",
|
||||
"Kh-31P (AS-17 Krypton) - 600kg, ARM, IN & Pas Rdr": "Kh-31P",
|
||||
"Kh-58U (AS-11 Kilter) - 640kg, ARM, IN & Pas Rdr": "Kh-58U",
|
||||
}
|
||||
while name in migration_map:
|
||||
name = migration_map[name]
|
||||
return name
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Weapon:
|
||||
"""Wrapper for DCS weapons."""
|
||||
@@ -139,7 +151,7 @@ class WeaponGroup:
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Update any existing models with new data on load.
|
||||
updated = WeaponGroup.named(state["name"])
|
||||
updated = WeaponGroup.named(weapons_migrator(state["name"]))
|
||||
state.update(updated.__dict__)
|
||||
self.__dict__.update(state)
|
||||
|
||||
@@ -161,16 +173,21 @@ class WeaponGroup:
|
||||
|
||||
@classmethod
|
||||
def _each_weapon_group(cls) -> Iterator[WeaponGroup]:
|
||||
names = []
|
||||
links = []
|
||||
for group_file_path in Path("resources/weapons").glob("**/*.yaml"):
|
||||
with group_file_path.open(encoding="utf8") as group_file:
|
||||
data = yaml.safe_load(group_file)
|
||||
name = data["name"]
|
||||
names.append(name)
|
||||
try:
|
||||
weapon_type = WeaponType(data["type"])
|
||||
except KeyError:
|
||||
weapon_type = WeaponType.UNKNOWN
|
||||
year = data.get("year")
|
||||
fallback_name = data.get("fallback")
|
||||
if fallback_name:
|
||||
links.append((name, fallback_name))
|
||||
group = WeaponGroup(name, weapon_type, year, fallback_name)
|
||||
for clsid in data["clsids"]:
|
||||
weapon = Weapon(clsid, group)
|
||||
@@ -178,6 +195,17 @@ class WeaponGroup:
|
||||
group.weapons.append(weapon)
|
||||
yield group
|
||||
|
||||
for name, fb in links:
|
||||
if fb not in names:
|
||||
sn = "Unknown"
|
||||
for n in names:
|
||||
if fb in n:
|
||||
sn = n
|
||||
break
|
||||
logging.error(
|
||||
f"Weapon '{name}' has invalid fallback '{fb}': suggested = {sn}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def register_clean_pylon(cls) -> None:
|
||||
group = WeaponGroup(
|
||||
|
||||
@@ -348,9 +348,11 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
|
||||
# Replace slashes with underscores because slashes are not allowed in filenames
|
||||
aircraft_id = aircraft.id.replace("/", "_")
|
||||
data_path = Path("resources/units/aircraft") / f"{aircraft_id}.yaml"
|
||||
if not data_path.exists():
|
||||
logging.warning(f"No data for {aircraft.id}; it will not be available")
|
||||
logging.warning(f"No data for {aircraft_id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open(encoding="utf-8") as data_file:
|
||||
@@ -371,7 +373,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
nautical_miles(50) if aircraft.helicopter else nautical_miles(150)
|
||||
)
|
||||
logging.warning(
|
||||
f"{aircraft.id} does not specify a max_range. Defaulting to "
|
||||
f"{aircraft_id} does not specify a max_range. Defaulting to "
|
||||
f"{mission_range.nautical_miles}NM"
|
||||
)
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class Faction:
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[AircraftType] = field(default_factory=list)
|
||||
aircraft: List[AircraftType] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[AircraftType] = field(default_factory=list)
|
||||
@@ -178,6 +178,10 @@ class Faction:
|
||||
)
|
||||
return sorted(air_defenses)
|
||||
|
||||
@cached_property
|
||||
def aircrafts(self) -> list[UnitType[Any]]:
|
||||
return list(self.aircraft + self.awacs + self.tankers)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
faction = Faction(locales=json.get("locales"))
|
||||
@@ -206,14 +210,10 @@ class Faction:
|
||||
faction.authors = json.get("authors", "")
|
||||
faction.description = json.get("description", "")
|
||||
|
||||
faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
|
||||
faction.aircraft = [AircraftType.named(n) for n in json.get("aircrafts", [])]
|
||||
faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
|
||||
faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])]
|
||||
|
||||
faction.aircrafts = list(
|
||||
set(faction.aircrafts + faction.awacs + faction.tankers)
|
||||
)
|
||||
|
||||
faction.frontline_units = [
|
||||
GroundUnitType.named(n) for n in json.get("frontline_units", [])
|
||||
]
|
||||
@@ -335,8 +335,9 @@ class Faction:
|
||||
if not mod_settings.uh_60l:
|
||||
self.remove_aircraft("UH-60L")
|
||||
self.remove_aircraft("KC130J")
|
||||
if not mod_settings.f4b_phantom:
|
||||
if not mod_settings.f4bc_phantom:
|
||||
self.remove_aircraft("VSN_F4B")
|
||||
self.remove_aircraft("VSN_F4C")
|
||||
if not mod_settings.f15d_baz:
|
||||
self.remove_aircraft("F-15D")
|
||||
if not mod_settings.f_16_idf:
|
||||
@@ -345,10 +346,10 @@ class Faction:
|
||||
self.remove_aircraft("F_16D_50")
|
||||
self.remove_aircraft("F_16D_50_NS")
|
||||
self.remove_aircraft("F_16D_52_NS")
|
||||
self.remove_aircraft("F-16D_Barak_30")
|
||||
self.remove_aircraft("F-16D_Barak_40")
|
||||
else:
|
||||
inject_F16I()
|
||||
# Remove the stock Viper because that DCS ID is now used by the Sufa
|
||||
self.remove_aircraft_by_name("F-16CM Fighting Falcon (Block 50)")
|
||||
if not mod_settings.f22_raptor:
|
||||
self.remove_aircraft("F-22A")
|
||||
if not mod_settings.f84g_thunderjet:
|
||||
@@ -423,13 +424,56 @@ class Faction:
|
||||
self.remove_preset("SA-20B/S-300PMU-2")
|
||||
self.remove_preset("SA-23/S-300VM")
|
||||
self.remove_preset("SA-17")
|
||||
self.remove_preset("KS-19")
|
||||
self.remove_preset("KS-19_HDS")
|
||||
self.remove_preset("HQ-2")
|
||||
self.remove_preset("SA-2/S-75 V-759/5V23")
|
||||
self.remove_preset("SA-3/S-125 V-601P/5V27")
|
||||
self.remove_vehicle("SAM SA-14 Strela-3 manpad")
|
||||
self.remove_vehicle("SAM SA-24 Igla-S manpad")
|
||||
self.remove_vehicle("Polyana-D4M1 C2 node")
|
||||
if not mod_settings.fa_18efg:
|
||||
self.remove_aircraft("FA-18E")
|
||||
self.remove_aircraft("FA-18F")
|
||||
self.remove_aircraft("EA-18G")
|
||||
# swedish military assets pack
|
||||
if not mod_settings.swedishmilitaryassetspack:
|
||||
self.remove_vehicle("BV410_RBS70")
|
||||
self.remove_vehicle("BV410_RBS90")
|
||||
self.remove_vehicle("LvS_103_Lavett103_Rb103A")
|
||||
self.remove_vehicle("LvS_103_Lavett103_Rb103B")
|
||||
self.remove_vehicle("LvS_103_Lavett103_HX_Rb103A")
|
||||
self.remove_vehicle("LvS_103_Lavett103_HX_Rb103B")
|
||||
self.remove_vehicle("LvS_103_StriE103")
|
||||
self.remove_vehicle("LvS_103_PM103")
|
||||
self.remove_vehicle("LvS_103_PM103_HX")
|
||||
self.remove_vehicle("LvS_103_Elverk103")
|
||||
self.remove_vehicle("LvKv9040")
|
||||
self.remove_vehicle("RBS_70")
|
||||
self.remove_vehicle("RBS_90")
|
||||
self.remove_vehicle("RBS_98")
|
||||
self.remove_vehicle("UndE23")
|
||||
self.remove_vehicle("BV410")
|
||||
self.remove_vehicle("CV9040")
|
||||
self.remove_vehicle("Strv103")
|
||||
self.remove_vehicle("Strv121")
|
||||
self.remove_vehicle("Strv122")
|
||||
self.remove_vehicle("Strv2000")
|
||||
self.remove_vehicle("Volvo740")
|
||||
self.remove_vehicle("RBS_15KA")
|
||||
self.remove_vehicle("AG_90")
|
||||
self.remove_vehicle("SwedishinfantryAK4")
|
||||
self.remove_vehicle("SwedishinfantryAK5")
|
||||
self.remove_vehicle("SwedishinfantryAK5GT")
|
||||
self.remove_vehicle("SwedishinfantryKSP90")
|
||||
self.remove_vehicle("SwedishinfantryKSP58")
|
||||
self.remove_vehicle("SwedishinfantryPskott86")
|
||||
self.remove_vehicle("RBS_57")
|
||||
self.remove_vehicle("RBS_58")
|
||||
self.remove_vehicle("Artillerisystem08")
|
||||
self.remove_vehicle("Grkpbv90")
|
||||
self.remove_ship("HSwMS_Visby")
|
||||
self.remove_ship("Strb90")
|
||||
self.remove_aircraft("HKP15B")
|
||||
|
||||
def remove_aircraft(self, name: str) -> None:
|
||||
for i in self.aircrafts:
|
||||
@@ -451,6 +495,11 @@ class Faction:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.frontline_units.remove(i)
|
||||
|
||||
def remove_ship(self, name: str) -> None:
|
||||
for i in self.naval_units:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.naval_units.remove(i)
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
if (ship := getattr(dcs.ships, name, None)) is not None:
|
||||
|
||||
@@ -31,7 +31,7 @@ class FactionLoader:
|
||||
|
||||
@classmethod
|
||||
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
|
||||
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions"
|
||||
user_faction_path = Path(persistency.base_path()) / "Retribution/Factions"
|
||||
files = cls.find_faction_files_in(
|
||||
FACTION_DIRECTORY
|
||||
) + cls.find_faction_files_in(user_faction_path)
|
||||
|
||||
@@ -89,7 +89,6 @@ class GroundPlanner:
|
||||
self.reserve: List[CombatGroup] = []
|
||||
|
||||
def plan_groundwar(self) -> None:
|
||||
|
||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||
|
||||
remaining_available_frontline_units = ground_unit_limit
|
||||
@@ -139,7 +138,6 @@ class GroundPlanner:
|
||||
remaining_available_frontline_units -= available
|
||||
|
||||
while available > 0:
|
||||
|
||||
if role == CombatGroupRole.SHORAD:
|
||||
count = 1
|
||||
else:
|
||||
|
||||
@@ -238,7 +238,6 @@ class AntiAirLayout(TgoLayout):
|
||||
location: PresetLocation,
|
||||
control_point: ControlPoint,
|
||||
) -> IadsGroundObject:
|
||||
|
||||
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
|
||||
return EwrGroundObject(name, location, control_point)
|
||||
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
|
||||
|
||||
@@ -132,7 +132,6 @@ class LayoutLoader:
|
||||
temp_mis.country(country.name).ship_group,
|
||||
temp_mis.country(country.name).static_group,
|
||||
):
|
||||
|
||||
try:
|
||||
g_id, u_id, group_name, group_mapping = mapping.group_for_name(
|
||||
dcs_group.name
|
||||
|
||||
@@ -19,4 +19,4 @@ def init_logging(version: str) -> None:
|
||||
with log_config.open() as log_file:
|
||||
logging.config.dictConfig(yaml.safe_load(log_file))
|
||||
|
||||
logging.info(f"DCS Liberation {version}")
|
||||
logging.info(f"DCS Retribution {version}")
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from game.ato.packagewaypoints import PackageWaypoints
|
||||
from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
|
||||
from game.utils import knots, feet, nautical_miles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
def try_set_attr(obj: Any, attr_name: str, val: Any = None) -> None:
|
||||
if not hasattr(obj, attr_name):
|
||||
setattr(obj, attr_name, val)
|
||||
|
||||
|
||||
class Migrator:
|
||||
def __init__(self, game: Game):
|
||||
self.game = game
|
||||
@@ -18,6 +22,9 @@ class Migrator:
|
||||
def _migrate_game(self) -> None:
|
||||
self._update_doctrine()
|
||||
self._update_packagewaypoints()
|
||||
self._update_package_attributes()
|
||||
self._update_control_points()
|
||||
self._update_flights()
|
||||
|
||||
def _update_doctrine(self) -> None:
|
||||
doctrines = [
|
||||
@@ -40,5 +47,32 @@ class Migrator:
|
||||
def _update_packagewaypoints(self) -> None:
|
||||
for c in self.game.coalitions:
|
||||
for p in c.ato.packages:
|
||||
if not hasattr(p.waypoints, "initial"):
|
||||
p.waypoints = PackageWaypoints.create(p, c)
|
||||
if p.flights:
|
||||
try_set_attr(p.waypoints, "initial", PackageWaypoints.create(p, c))
|
||||
|
||||
def _update_package_attributes(self) -> None:
|
||||
for c in self.game.coalitions:
|
||||
for p in c.ato.packages:
|
||||
try_set_attr(p, "custom_name")
|
||||
try_set_attr(p, "frequency")
|
||||
|
||||
def _update_control_points(self) -> None:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
is_carrier = cp.is_carrier
|
||||
is_lha = cp.is_lha
|
||||
is_fob = cp.category == "fob"
|
||||
radio_configurable = is_carrier or is_lha or is_fob
|
||||
if radio_configurable:
|
||||
try_set_attr(cp, "frequency")
|
||||
if is_carrier or is_lha:
|
||||
try_set_attr(cp, "tacan")
|
||||
try_set_attr(cp, "tcn_name")
|
||||
try_set_attr(cp, "icls_channel")
|
||||
try_set_attr(cp, "icls_name")
|
||||
try_set_attr(cp, "link4")
|
||||
|
||||
def _update_flights(self) -> None:
|
||||
for f in self.game.db.flights.objects.values():
|
||||
try_set_attr(f, "frequency")
|
||||
try_set_attr(f, "tacan")
|
||||
try_set_attr(f, "tcn_name")
|
||||
|
||||
@@ -29,6 +29,7 @@ from dcs.unitgroup import FlyingGroup
|
||||
from game.ato import Flight, FlightType
|
||||
from game.ato.flightplans.aewc import AewcFlightPlan
|
||||
from game.ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
|
||||
|
||||
class AircraftBehavior:
|
||||
@@ -178,9 +179,9 @@ class AircraftBehavior:
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
# ASM includes ARMs and TALDs (among other things, but those are the useful
|
||||
# Guided includes ARMs and TALDs (among other things, but those are the useful
|
||||
# weapons for SEAD).
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.Guided,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
@@ -272,8 +273,14 @@ class AircraftBehavior:
|
||||
# Search Then Engage task, which we have to use instead of the Escort
|
||||
# task for the reasons explained in JoinPointBuilder.
|
||||
group.task = Escort.name
|
||||
if flight.package.primary_task == FlightType.STRIKE:
|
||||
group.add_trigger_action(SwitchWaypoint(None, 5))
|
||||
if flight.flight_plan.is_formation(flight.flight_plan):
|
||||
index = flight.flight_plan.get_index_of_wpt_by_type(
|
||||
FlightWaypointType.SPLIT
|
||||
)
|
||||
if index > 0:
|
||||
group.add_trigger_action(SwitchWaypoint(None, index))
|
||||
else:
|
||||
logging.warning(f"Couldn't determine SPLIT for {group.name}")
|
||||
self.configure_behavior(
|
||||
flight, group, roe=OptROE.Values.OpenFire, restrict_jettison=True
|
||||
)
|
||||
@@ -283,26 +290,33 @@ class AircraftBehavior:
|
||||
# available aircraft, and F-14s are not able to be SEAD despite having TALDs.
|
||||
# https://forums.eagle.ru/topic/272112-cannot-assign-f-14-to-sead/
|
||||
group.task = SEAD.name
|
||||
if flight.package.primary_task == FlightType.STRIKE:
|
||||
group.add_trigger_action(SwitchWaypoint(None, 5))
|
||||
index = flight.flight_plan.get_index_of_wpt_by_type(FlightWaypointType.SPLIT)
|
||||
if index > 0 and flight.flight_plan.is_formation(flight.flight_plan):
|
||||
group.add_trigger_action(SwitchWaypoint(None, index))
|
||||
if index < 1:
|
||||
logging.warning(f"Couldn't determine SPLIT for {group.name}")
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
# ASM includes ARMs and TALDs (among other things, but those are the useful
|
||||
# Guided includes ARMs and TALDs (among other things, but those are the useful
|
||||
# weapons for SEAD).
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.Guided,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
|
||||
def configure_transport(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
group.task = Transport.name
|
||||
roe = OptROE.Values.WeaponHold
|
||||
if flight.is_hercules:
|
||||
group.task = GroundAttack.name
|
||||
roe = OptROE.Values.OpenFire
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
roe=roe,
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from game.settings import Settings
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
Fob,
|
||||
)
|
||||
from game.unitmap import UnitMap
|
||||
from .aircraftpainter import AircraftPainter
|
||||
@@ -104,7 +105,9 @@ class AircraftGenerator:
|
||||
ato: The ATO to spawn aircraft for.
|
||||
dynamic_runways: Runway data for carriers and FARPs.
|
||||
"""
|
||||
for package in ato.packages:
|
||||
self._reserve_frequencies_and_tacan(ato)
|
||||
|
||||
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
|
||||
if not package.flights:
|
||||
continue
|
||||
for flight in package.flights:
|
||||
@@ -115,8 +118,10 @@ class AircraftGenerator:
|
||||
)
|
||||
self.unit_map.add_aircraft(group, flight)
|
||||
if (
|
||||
package.primary_task == FlightType.STRIKE
|
||||
and package.primary_flight is not None
|
||||
package.primary_flight is not None
|
||||
and package.primary_flight.flight_plan.is_formation(
|
||||
package.primary_flight.flight_plan
|
||||
)
|
||||
):
|
||||
splittrigger = TriggerOnce(Event.NoEvent, f"Split-{id(package)}")
|
||||
splittrigger.add_condition(FlagIsTrue(flag=f"split-{id(package)}"))
|
||||
@@ -124,9 +129,13 @@ class AircraftGenerator:
|
||||
splittrigger.add_condition(FlagIsFalse(flag=f"split-{id(package)}"))
|
||||
splittrigger.add_condition(GroupDead(package.primary_flight.group_id))
|
||||
for flight in package.flights:
|
||||
if flight is not package.primary_flight:
|
||||
if flight.flight_type in [
|
||||
FlightType.ESCORT,
|
||||
FlightType.SEAD_ESCORT,
|
||||
]:
|
||||
splittrigger.add_action(AITaskPush(flight.group_id, 1))
|
||||
self.mission.triggerrules.triggers.append(splittrigger)
|
||||
if len(splittrigger.actions) > 0:
|
||||
self.mission.triggerrules.triggers.append(splittrigger)
|
||||
|
||||
def spawn_unused_aircraft(
|
||||
self, player_country: Country, enemy_country: Country
|
||||
@@ -151,9 +160,6 @@ class AircraftGenerator:
|
||||
def _spawn_unused_for(
|
||||
self, squadron: Squadron, country: Country, faction: Faction
|
||||
) -> None:
|
||||
if self.game.settings.perf_disable_idle_aircraft:
|
||||
return
|
||||
|
||||
assert isinstance(squadron.location, Airfield)
|
||||
for _ in range(squadron.untasked_aircraft):
|
||||
# Creating a flight even those this isn't a fragged mission lets us
|
||||
@@ -198,4 +204,27 @@ class AircraftGenerator:
|
||||
self.use_client,
|
||||
).configure()
|
||||
)
|
||||
|
||||
wpt = group.waypoint("LANDING")
|
||||
if flight.is_helo and isinstance(flight.arrival, Fob) and wpt:
|
||||
hpad = self.helipads[flight.arrival].units.pop(0)
|
||||
wpt.helipad_id = hpad.id
|
||||
wpt.link_unit = hpad.id
|
||||
self.helipads[flight.arrival].units.append(hpad)
|
||||
|
||||
return group
|
||||
|
||||
def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None:
|
||||
for package in ato.packages:
|
||||
if package.frequency is None:
|
||||
continue
|
||||
if package.frequency not in self.radio_registry.allocated_channels:
|
||||
self.radio_registry.reserve(package.frequency)
|
||||
for f in package.flights:
|
||||
if (
|
||||
f.frequency
|
||||
and f.frequency not in self.radio_registry.allocated_channels
|
||||
):
|
||||
self.radio_registry.reserve(f.frequency)
|
||||
if f.tacan and f.tacan not in self.tacan_registy.allocated_channels:
|
||||
self.tacan_registy.mark_unavailable(f.tacan)
|
||||
|
||||
@@ -133,14 +133,20 @@ class FlightGroupConfigurator:
|
||||
laser_codes.append(None)
|
||||
|
||||
def setup_radios(self) -> RadioFrequency:
|
||||
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
|
||||
channel = self.radio_registry.alloc_uhf()
|
||||
self.register_air_support(channel)
|
||||
else:
|
||||
channel = self.flight.unit_type.alloc_flight_radio(self.radio_registry)
|
||||
freq = self.flight.frequency
|
||||
if freq is None and (freq := self.flight.package.frequency) is None:
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
self.flight.package.frequency = freq
|
||||
if freq not in self.radio_registry.allocated_channels:
|
||||
self.radio_registry.reserve(freq)
|
||||
|
||||
self.group.set_frequency(channel.mhz)
|
||||
return channel
|
||||
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
|
||||
self.register_air_support(freq)
|
||||
elif self.flight.frequency is None and self.flight.client_count:
|
||||
freq = self.flight.unit_type.alloc_flight_radio(self.radio_registry)
|
||||
|
||||
self.group.set_frequency(freq.mhz)
|
||||
return freq
|
||||
|
||||
def register_air_support(self, channel: RadioFrequency) -> None:
|
||||
callsign = callsign_for_support_unit(self.group)
|
||||
@@ -151,13 +157,18 @@ class FlightGroupConfigurator:
|
||||
callsign=callsign,
|
||||
freq=channel,
|
||||
depature_location=self.flight.departure.name,
|
||||
end_time=self.flight.flight_plan.mission_departure_time,
|
||||
start_time=self.flight.flight_plan.takeoff_time(),
|
||||
start_time=self.flight.flight_plan.patrol_start_time,
|
||||
end_time=self.flight.flight_plan.patrol_end_time,
|
||||
blue=self.flight.departure.captured,
|
||||
)
|
||||
)
|
||||
elif isinstance(self.flight.flight_plan, TheaterRefuelingFlightPlan):
|
||||
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
|
||||
if self.flight.tacan is None:
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.Y, TacanUsage.AirToAir
|
||||
)
|
||||
else:
|
||||
tacan = self.flight.tacan
|
||||
self.mission_data.tankers.append(
|
||||
TankerInfo(
|
||||
group_name=str(self.group.name),
|
||||
|
||||
@@ -135,7 +135,6 @@ class FlightGroupSpawner:
|
||||
"No room on runway or parking slots. Starting from the air."
|
||||
)
|
||||
group = self._generate_over_departure(name, cp)
|
||||
group.points[0].alt = 1500
|
||||
return group
|
||||
|
||||
def generate_mid_mission(self) -> FlyingGroup[Any]:
|
||||
@@ -208,7 +207,11 @@ class FlightGroupSpawner:
|
||||
elif self.flight.unit_type.helicopter:
|
||||
alt = WARM_START_HELI_ALT
|
||||
else:
|
||||
alt = WARM_START_ALTITUDE
|
||||
if origin.id not in self.mission_data.cp_stack:
|
||||
min_alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL
|
||||
self.mission_data.cp_stack[origin.id] = min_alt
|
||||
alt = self.mission_data.cp_stack[origin.id]
|
||||
self.mission_data.cp_stack[origin.id] += STACK_SEPARATION
|
||||
|
||||
speed = GroundSpeed.for_flight(self.flight, alt)
|
||||
pos = at + Vector2(random.randint(100, 1000), random.randint(100, 1000))
|
||||
@@ -244,16 +247,18 @@ class FlightGroupSpawner:
|
||||
def _generate_at_cp_helipad(self, name: str, cp: ControlPoint) -> FlyingGroup[Any]:
|
||||
try:
|
||||
helipad = self.helipads[cp]
|
||||
except IndexError as ex:
|
||||
except IndexError:
|
||||
raise NoParkingSlotError()
|
||||
|
||||
group = self._generate_at_group(name, helipad)
|
||||
|
||||
if self.start_type is not StartType.COLD:
|
||||
if self.start_type is StartType.WARM:
|
||||
group.points[0].type = "TakeOffParkingHot"
|
||||
hpad = helipad.units[0]
|
||||
for i in range(self.flight.count):
|
||||
group.units[i].position = helipad.units[i].position
|
||||
group.units[i].heading = helipad.units[i].heading
|
||||
group.units[i].position = hpad.position
|
||||
group.units[i].heading = hpad.heading
|
||||
group.units[i].parking_id = str(i + 1)
|
||||
return group
|
||||
|
||||
def dcs_start_type(self) -> DcsStartType:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import Expend, WeaponType, CarpetBombing, OptROE
|
||||
|
||||
from game.ato.flightwaypointtype import FlightWaypointType
|
||||
from game.utils import feet, knots
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
class AirAssaultIngressBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
air_drop = self.group.units[0].unit_type in [Hercules]
|
||||
if air_drop:
|
||||
waypoint.speed = knots(230).meters_per_second
|
||||
waypoint.speed_locked = True
|
||||
waypoint.ETA_locked = False
|
||||
tgt = self.flight.flight_plan.package.target.position
|
||||
for wpt in self.flight.flight_plan.waypoints:
|
||||
if wpt.waypoint_type == FlightWaypointType.TARGET_GROUP_LOC:
|
||||
tgt = wpt.position
|
||||
break
|
||||
bombing = CarpetBombing(
|
||||
tgt,
|
||||
weapon_type=WeaponType.Bombs,
|
||||
expend=Expend.All,
|
||||
carpet_length=feet(9000).meters,
|
||||
)
|
||||
waypoint.add_task(bombing)
|
||||
@@ -1,7 +1,8 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import AttackGroup, OptECMUsing, WeaponType
|
||||
from dcs.task import AttackGroup, OptECMUsing, WeaponType, Expend
|
||||
|
||||
from game.theater import TheaterGroundObject
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
@@ -28,10 +29,24 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
continue
|
||||
|
||||
task = AttackGroup(
|
||||
miz_group.id, weapon_type=WeaponType.Auto, group_attack=True
|
||||
miz_group.id,
|
||||
weapon_type=WeaponType.Guided,
|
||||
altitude=round(self.flight.coalition.doctrine.ingress_altitude.meters),
|
||||
)
|
||||
waypoint.tasks.append(task)
|
||||
|
||||
dir = target.position.heading_between_point(waypoint.position)
|
||||
|
||||
task = AttackGroup(
|
||||
miz_group.id,
|
||||
weapon_type=WeaponType.Unguided,
|
||||
expend=Expend.All,
|
||||
direction=math.radians(dir),
|
||||
altitude=round(self.flight.coalition.doctrine.ingress_altitude.meters),
|
||||
)
|
||||
task.params["altitudeEnabled"] = False
|
||||
waypoint.tasks.append(task)
|
||||
|
||||
# Preemptively use ECM to better avoid getting swatted.
|
||||
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
|
||||
waypoint.tasks.append(ecm_option)
|
||||
|
||||
@@ -71,10 +71,9 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
max_dist: float = 30.0,
|
||||
vertical_spacing: float = 2000.0,
|
||||
) -> None:
|
||||
|
||||
rx = (random.random() + 0.1) * 333
|
||||
ry = feet(vertical_spacing).meters
|
||||
rz = (random.random() + 0.1) * 50 * random.choice([-1, 1])
|
||||
rz = (random.random() + 0.1) * 166 * random.choice([-1, 1])
|
||||
pos = {"x": rx, "y": ry, "z": rz}
|
||||
|
||||
group_id = None
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import timedelta
|
||||
from typing import Any, Iterable, Union
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.planes import AJS37, F_14B, JF_17
|
||||
from dcs.planes import AJS37, F_14A_135_GR, F_14B, JF_17
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
|
||||
@@ -91,5 +91,5 @@ class PydcsWaypointBuilder:
|
||||
for i, t in enumerate(targets):
|
||||
if self.group.units[0].unit_type == JF_17 and i < 4:
|
||||
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
|
||||
if self.group.units[0].unit_type == F_14B and i == 0:
|
||||
if self.group.units[0].unit_type in [F_14B, F_14A_135_GR] and i == 0:
|
||||
self.group.add_nav_target_point(t.position, "ST")
|
||||
|
||||
@@ -71,11 +71,20 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
if self.flight.unit_type.dcs_unit_type.tacan:
|
||||
tanker_info = self.mission_data.tankers[-1]
|
||||
tacan = tanker_info.tacan
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(tanker_info.callsign)
|
||||
if self.flight.tcn_name is None:
|
||||
cs = tanker_info.callsign[:-2]
|
||||
csn = tanker_info.callsign[-1]
|
||||
tacan_callsign = {
|
||||
"Texaco": "TX",
|
||||
"Arco": "AC",
|
||||
"Shell": "SH",
|
||||
}.get(cs)
|
||||
if tacan_callsign:
|
||||
tacan_callsign = tacan_callsign + csn
|
||||
else:
|
||||
tacan_callsign = cs[0:2] + csn
|
||||
else:
|
||||
tacan_callsign = self.flight.tcn_name
|
||||
|
||||
waypoint.add_task(
|
||||
ActivateBeaconCommand(
|
||||
@@ -85,5 +94,6 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
bearing=True,
|
||||
unit_id=self.group.units[0].id,
|
||||
aa=True,
|
||||
tanker=True,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -49,9 +49,10 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
# All non ARM types like Decoys will use the normal AttackGroup Task
|
||||
attack_task = AttackGroup(
|
||||
miz_group.id,
|
||||
weapon_type=DcsWeaponType.Guided,
|
||||
weapon_type=DcsWeaponType.Decoy,
|
||||
group_attack=True,
|
||||
expend=Expend.All,
|
||||
altitude=waypoint.alt,
|
||||
)
|
||||
waypoint.tasks.append(attack_task)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
class SplitPointBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
|
||||
if not self.flight.flight_type.is_air_to_air:
|
||||
# Capture any non A/A type to avoid issues with SPJs that use the primary radar such as the F/A-18C.
|
||||
# You can bully them with STT to not be able to fire radar guided missiles at you,
|
||||
|
||||
@@ -3,9 +3,9 @@ import copy
|
||||
from dcs import Point
|
||||
from dcs.planes import B_17G, B_52H, Tu_22M3, B_1B
|
||||
from dcs.point import MovingPoint
|
||||
from dcs.task import Bombing, Expend, OptFormation, WeaponType
|
||||
from dcs.task import Bombing, Expend, OptFormation, WeaponType, CarpetBombing
|
||||
|
||||
from game.utils import mach, Distance
|
||||
from game.utils import mach
|
||||
from .pydcswaypointbuilder import PydcsWaypointBuilder
|
||||
|
||||
|
||||
@@ -13,15 +13,12 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||
bomber = self.group.units[0].unit_type in [B_17G, Tu_22M3]
|
||||
bomber_guided = self.group.units[0].unit_type in [B_1B, B_52H]
|
||||
if bomber_guided:
|
||||
self.add_strike_tasks(waypoint, WeaponType.Guided)
|
||||
self.add_bombing_tasks(waypoint)
|
||||
elif bomber:
|
||||
self.add_bombing_tasks(waypoint)
|
||||
else:
|
||||
self.add_strike_tasks(waypoint)
|
||||
|
||||
waypoint.tasks.append(OptFormation.trail_open())
|
||||
if bomber_guided or not bomber:
|
||||
self.add_strike_tasks(waypoint, WeaponType.Guided)
|
||||
|
||||
waypoint.tasks.append(OptFormation.ww2_bomber_element_close())
|
||||
self.add_bombing_tasks(waypoint)
|
||||
|
||||
def add_bombing_tasks(self, waypoint: MovingPoint) -> None:
|
||||
targets = self.waypoint.targets
|
||||
@@ -32,9 +29,16 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
for target in targets[1:]:
|
||||
center += target.position
|
||||
center /= len(targets)
|
||||
ga = True if self.flight.count > 1 else False
|
||||
bombing = Bombing(
|
||||
center, weapon_type=WeaponType.Bombs, expend=Expend.All, group_attack=ga
|
||||
avg_spacing = 0.0
|
||||
for t in targets:
|
||||
avg_spacing += center.distance_to_point(t.position)
|
||||
avg_spacing /= len(targets)
|
||||
bombing = CarpetBombing(
|
||||
center,
|
||||
weapon_type=WeaponType.Bombs,
|
||||
expend=Expend.All,
|
||||
carpet_length=avg_spacing,
|
||||
altitude=round(self.flight.coalition.doctrine.ingress_altitude.meters),
|
||||
)
|
||||
waypoint.tasks.append(bombing)
|
||||
|
||||
@@ -49,7 +53,8 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
|
||||
bombing.params["expend"] = Expend.All.value
|
||||
waypoint.tasks.append(bombing)
|
||||
|
||||
waypoint.speed = mach(0.85, Distance.from_feet(20000)).meters_per_second
|
||||
doctrine = self.flight.coalition.doctrine
|
||||
waypoint.speed = mach(0.85, doctrine.ingress_altitude).meters_per_second
|
||||
|
||||
# Register special waypoints
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
|
||||
@@ -20,6 +20,7 @@ from game.missiongenerator.aircraft.waypoints.cargostop import CargoStopBuilder
|
||||
from game.missiongenerator.missiondata import MissionData
|
||||
from game.settings import Settings
|
||||
from game.utils import pairwise
|
||||
from .airassaultingress import AirAssaultIngressBuilder
|
||||
from .baiingress import BaiIngressBuilder
|
||||
from .landingzone import LandingZoneBuilder
|
||||
from .casingress import CasIngressBuilder
|
||||
@@ -136,6 +137,7 @@ class WaypointGenerator:
|
||||
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
|
||||
FlightWaypointType.REFUEL: RefuelPointBuilder,
|
||||
FlightWaypointType.CARGO_STOP: CargoStopBuilder,
|
||||
FlightWaypointType.INGRESS_AIR_ASSAULT: AirAssaultIngressBuilder,
|
||||
}
|
||||
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
|
||||
return builder(
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import List, TYPE_CHECKING, Tuple, Type
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
ActivateBeaconCommand,
|
||||
MainTask,
|
||||
Refueling,
|
||||
SetImmortalCommand,
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game.ato.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from game.callsigns import callsign_for_support_unit
|
||||
from game.naming import namegen
|
||||
from game.radio.radios import RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
|
||||
from game.utils import Heading
|
||||
from .airconflictdescription import AirConflictDescription
|
||||
from .missiondata import AwacsInfo, MissionData, TankerInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
TANKER_DISTANCE = 15000
|
||||
TANKER_ALT = 4572
|
||||
TANKER_HEADING_OFFSET = 45
|
||||
|
||||
AWACS_DISTANCE = 150000
|
||||
AWACS_ALT = 13000
|
||||
|
||||
|
||||
class AirSupportGenerator:
|
||||
def __init__(
|
||||
self,
|
||||
mission: Mission,
|
||||
conflict: AirConflictDescription,
|
||||
game: Game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
mission_data: MissionData,
|
||||
) -> None:
|
||||
self.mission = mission
|
||||
self.conflict = conflict
|
||||
self.game = game
|
||||
self.radio_registry = radio_registry
|
||||
self.tacan_registry = tacan_registry
|
||||
self.mission_data = mission_data
|
||||
|
||||
@classmethod
|
||||
def support_tasks(cls) -> List[Type[MainTask]]:
|
||||
return [Refueling, AWACS]
|
||||
|
||||
@staticmethod
|
||||
def _get_tanker_params(unit_type: Type[UnitType]) -> Tuple[int, int]:
|
||||
if unit_type is KC130:
|
||||
return TANKER_ALT - 500, 596
|
||||
elif unit_type is KC_135:
|
||||
return TANKER_ALT, 770
|
||||
elif unit_type is KC135MPRS:
|
||||
return TANKER_ALT + 500, 596
|
||||
return TANKER_ALT, 574
|
||||
|
||||
def generate(self) -> None:
|
||||
player_cp = (
|
||||
self.conflict.blue_cp
|
||||
if self.conflict.blue_cp.captured
|
||||
else self.conflict.red_cp
|
||||
)
|
||||
|
||||
country = self.mission.country(self.game.blue.country_name)
|
||||
|
||||
if not self.game.settings.disable_legacy_tanker:
|
||||
fallback_tanker_number = 0
|
||||
|
||||
for i, tanker_unit_type in enumerate(
|
||||
self.game.faction_for(player=True).tankers
|
||||
):
|
||||
unit_type = tanker_unit_type.dcs_unit_type
|
||||
if not issubclass(unit_type, PlaneType):
|
||||
logging.warning(f"Refueling aircraft {unit_type} must be a plane")
|
||||
continue
|
||||
|
||||
# TODO: Make loiter altitude a property of the unit type.
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.Y, TacanUsage.AirToAir
|
||||
)
|
||||
tanker_heading = Heading.from_degrees(
|
||||
self.conflict.red_cp.position.heading_between_point(
|
||||
self.conflict.blue_cp.position
|
||||
)
|
||||
+ TANKER_HEADING_OFFSET * i
|
||||
)
|
||||
tanker_position = player_cp.position.point_from_heading(
|
||||
tanker_heading.degrees, TANKER_DISTANCE
|
||||
)
|
||||
tanker_group = self.mission.refuel_flight(
|
||||
country=country,
|
||||
name=namegen.next_tanker_name(country, tanker_unit_type),
|
||||
airport=None,
|
||||
plane_type=unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
speed=airspeed,
|
||||
tacanchannel=str(tacan),
|
||||
)
|
||||
tanker_group.set_frequency(freq.mhz)
|
||||
|
||||
callsign = callsign_for_support_unit(tanker_group)
|
||||
tacan_callsign = {
|
||||
"Texaco": "TEX",
|
||||
"Arco": "ARC",
|
||||
"Shell": "SHL",
|
||||
}.get(callsign)
|
||||
if tacan_callsign is None:
|
||||
# The dict above is all the callsigns currently in the game, but
|
||||
# non-Western countries don't use the callsigns and instead just
|
||||
# use numbers. It's possible that none of those nations have
|
||||
# TACAN compatible refueling aircraft, but fallback just in
|
||||
# case.
|
||||
tacan_callsign = f"TK{fallback_tanker_number}"
|
||||
fallback_tanker_number += 1
|
||||
|
||||
if tanker_unit_type != IL_78M:
|
||||
# Override PyDCS tacan channel.
|
||||
tanker_group.points[0].tasks.pop()
|
||||
tanker_group.points[0].tasks.append(
|
||||
ActivateBeaconCommand(
|
||||
tacan.number,
|
||||
tacan.band.value,
|
||||
tacan_callsign,
|
||||
True,
|
||||
tanker_group.units[0].id,
|
||||
True,
|
||||
)
|
||||
)
|
||||
|
||||
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.mission_data.tankers.append(
|
||||
TankerInfo(
|
||||
group_name=str(tanker_group.name),
|
||||
callsign=callsign,
|
||||
variant=tanker_unit_type.name,
|
||||
freq=freq,
|
||||
tacan=tacan,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
|
||||
if not self.game.settings.disable_legacy_aewc:
|
||||
possible_awacs = [
|
||||
a
|
||||
for a in self.game.faction_for(player=True).aircrafts
|
||||
if a in AEWC_CAPABLE
|
||||
]
|
||||
|
||||
if not possible_awacs:
|
||||
logging.warning("No AWACS for faction")
|
||||
return
|
||||
|
||||
awacs_unit = possible_awacs[0]
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
|
||||
unit_type = awacs_unit.dcs_unit_type
|
||||
if not issubclass(unit_type, PlaneType):
|
||||
logging.warning(f"AWACS aircraft {unit_type} must be a plane")
|
||||
return
|
||||
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=country,
|
||||
name=namegen.next_awacs_name(country),
|
||||
plane_type=unit_type,
|
||||
altitude=AWACS_ALT,
|
||||
airport=None,
|
||||
position=self.conflict.center.random_point_within(
|
||||
AWACS_DISTANCE, AWACS_DISTANCE
|
||||
),
|
||||
frequency=freq.mhz,
|
||||
start_type=StartType.Warm,
|
||||
)
|
||||
awacs_flight.set_frequency(freq.mhz)
|
||||
|
||||
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
|
||||
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
|
||||
|
||||
self.mission_data.awacs.append(
|
||||
AwacsInfo(
|
||||
group_name=str(awacs_flight.name),
|
||||
callsign=callsign_for_support_unit(awacs_flight),
|
||||
freq=freq,
|
||||
depature_location=None,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
@@ -17,7 +17,7 @@ from game.radio.radios import RadioFrequency
|
||||
from game.runways import RunwayData
|
||||
from game.theater import ControlPoint, FrontLine
|
||||
from .aircraft.flightdata import FlightData
|
||||
from .airsupportgenerator import AwacsInfo, TankerInfo
|
||||
from .missiondata import AwacsInfo, TankerInfo
|
||||
from .flotgenerator import JtacInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -59,7 +59,6 @@ class DrawingsGenerator:
|
||||
if destination in seen:
|
||||
continue
|
||||
else:
|
||||
|
||||
# Determine path color
|
||||
if cp.captured and destination.captured:
|
||||
color = BLUE_PATH_COLOR
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from datetime import timedelta
|
||||
from typing import List, Optional, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs import Mission
|
||||
@@ -15,7 +16,6 @@ from dcs.task import (
|
||||
AFAC,
|
||||
AttackGroup,
|
||||
ControlledTask,
|
||||
EPLRS,
|
||||
FAC,
|
||||
FireAtPoint,
|
||||
GoToWaypoint,
|
||||
@@ -47,6 +47,7 @@ from .frontlineconflictdescription import FrontLineConflictDescription
|
||||
from .groundforcepainter import GroundForcePainter
|
||||
from .lasercoderegistry import LaserCodeRegistry
|
||||
from .missiondata import JtacInfo, MissionData
|
||||
from ..ato import FlightType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -203,7 +204,6 @@ class FlotGenerator:
|
||||
side: Country,
|
||||
forward_heading: Heading,
|
||||
) -> None:
|
||||
|
||||
infantry_position = self.conflict.find_ground_position(
|
||||
group.points[0].position.random_point_within(250, 50),
|
||||
500,
|
||||
@@ -276,8 +276,29 @@ class FlotGenerator:
|
||||
vehicle = vg.units[0]
|
||||
GroundForcePainter(faction, vehicle).apply_livery()
|
||||
|
||||
def _earliest_tot_on_flot(self, player: bool) -> timedelta:
|
||||
tots = [
|
||||
x.time_over_target
|
||||
for x in self.game.ato_for(player).packages
|
||||
if x.primary_task == FlightType.CAS
|
||||
]
|
||||
return (
|
||||
timedelta(seconds=random.randint(300, 1800))
|
||||
if len(tots) == 0
|
||||
else min(
|
||||
[
|
||||
x.time_over_target
|
||||
for x in self.game.ato_for(player).packages
|
||||
if x.primary_task == FlightType.CAS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def _set_reform_waypoint(
|
||||
self, dcs_group: VehicleGroup, forward_heading: Heading
|
||||
self,
|
||||
dcs_group: VehicleGroup,
|
||||
forward_heading: Heading,
|
||||
hold_duration: timedelta = timedelta(),
|
||||
) -> None:
|
||||
"""Setting a waypoint close to the spawn position allows the group to reform gracefully
|
||||
rather than spin
|
||||
@@ -285,7 +306,10 @@ class FlotGenerator:
|
||||
reform_point = dcs_group.position.point_from_heading(
|
||||
forward_heading.degrees, 50
|
||||
)
|
||||
dcs_group.add_waypoint(reform_point)
|
||||
rp = dcs_group.add_waypoint(reform_point)
|
||||
hold = ControlledTask(Hold())
|
||||
hold.stop_after_duration(hold_duration.seconds)
|
||||
rp.add_task(hold)
|
||||
|
||||
def _plan_artillery_action(
|
||||
self,
|
||||
@@ -319,11 +343,10 @@ class FlotGenerator:
|
||||
|
||||
# Artillery will fall back when under attack
|
||||
if stance != CombatStance.RETREAT:
|
||||
|
||||
# Hold position
|
||||
dcs_group.points[1].tasks.append(Hold())
|
||||
retreat = self.find_retreat_point(
|
||||
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 3)
|
||||
dcs_group, forward_heading, int(RETREAT_DISTANCE / 3)
|
||||
)
|
||||
dcs_group.add_waypoint(
|
||||
dcs_group.position.point_from_heading(forward_heading.degrees, 1),
|
||||
@@ -370,7 +393,10 @@ class FlotGenerator:
|
||||
Handles adding the DCS tasks for tank and IFV groups for all combat stances.
|
||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
duration = timedelta()
|
||||
if stance != CombatStance.RETREAT:
|
||||
duration = self._earliest_tot_on_flot(not to_cp.coalition.player)
|
||||
self._set_reform_waypoint(dcs_group, forward_heading, duration)
|
||||
if stance == CombatStance.AGGRESSIVE:
|
||||
# Attack nearest enemy if any
|
||||
# Then move forward OR Attack enemy base if it is not too far away
|
||||
@@ -457,7 +483,10 @@ class FlotGenerator:
|
||||
Handles adding the DCS tasks for APC and ATGM groups for all combat stances.
|
||||
Returns True if tasking was added, returns False if the stance was not a combat stance.
|
||||
"""
|
||||
self._set_reform_waypoint(dcs_group, forward_heading)
|
||||
duration = timedelta()
|
||||
if stance != CombatStance.RETREAT:
|
||||
duration = self._earliest_tot_on_flot(not to_cp.coalition.player)
|
||||
self._set_reform_waypoint(dcs_group, forward_heading, duration)
|
||||
if stance in [
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.BREAKTHROUGH,
|
||||
@@ -491,14 +520,10 @@ class FlotGenerator:
|
||||
from_cp: ControlPoint,
|
||||
to_cp: ControlPoint,
|
||||
) -> None:
|
||||
|
||||
if not self.game.settings.perf_moving_units:
|
||||
return
|
||||
|
||||
for dcs_group, group in ally_groups:
|
||||
if group.unit_type.eplrs_capable:
|
||||
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
|
||||
|
||||
if group.role == CombatGroupRole.ARTILLERY:
|
||||
if self.game.settings.perf_artillery:
|
||||
target = self.get_artillery_target_in_range(
|
||||
@@ -519,7 +544,7 @@ class FlotGenerator:
|
||||
|
||||
if stance == CombatStance.RETREAT:
|
||||
# In retreat mode, the units will fall back
|
||||
# If the ally base is close enough, the units will even regroup there
|
||||
# If the allied base is close enough, the units will even regroup there
|
||||
if (
|
||||
from_cp.position.distance_to_point(dcs_group.points[0].position)
|
||||
<= RETREAT_DISTANCE
|
||||
@@ -537,7 +562,7 @@ class FlotGenerator:
|
||||
self, dcs_group: VehicleGroup, forward_heading: Heading
|
||||
) -> None:
|
||||
"""
|
||||
This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
|
||||
This adds a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
|
||||
"""
|
||||
|
||||
if len(dcs_group.units) == 1:
|
||||
@@ -554,7 +579,7 @@ class FlotGenerator:
|
||||
# We add a new retreat waypoint
|
||||
dcs_group.add_waypoint(
|
||||
self.find_retreat_point(
|
||||
dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)
|
||||
dcs_group, forward_heading, int(RETREAT_DISTANCE / 8)
|
||||
),
|
||||
self.wpt_pointaction,
|
||||
)
|
||||
@@ -684,7 +709,7 @@ class FlotGenerator:
|
||||
@staticmethod
|
||||
def get_artilery_group_distance_from_frontline(group: CombatGroup) -> int:
|
||||
"""
|
||||
For artilery group, decide the distance from frontline with the range of the unit
|
||||
For artillery group, decide the distance from frontline with the range of the unit
|
||||
"""
|
||||
rg = group.unit_type.dcs_unit_type.threat_range - 7500
|
||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
|
||||
@@ -43,10 +43,10 @@ from game.radio.radios import RadioFrequency
|
||||
from game.runways import RunwayData
|
||||
from game.theater import TheaterGroundObject, TheaterUnit
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import Distance, UnitSystem, meters, mps, pounds
|
||||
from game.utils import Distance, UnitSystem, meters, mps, pounds, knots, feet
|
||||
from game.weather import Weather
|
||||
from .aircraft.flightdata import FlightData
|
||||
from .airsupportgenerator import AwacsInfo, TankerInfo
|
||||
from .missiondata import AwacsInfo, TankerInfo
|
||||
from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -191,7 +191,6 @@ class NumberedWaypoint:
|
||||
|
||||
|
||||
class FlightPlanBuilder:
|
||||
|
||||
WAYPOINT_DESC_MAX_LEN = 25
|
||||
|
||||
def __init__(self, start_time: datetime.datetime, units: UnitSystem) -> None:
|
||||
@@ -336,7 +335,7 @@ class BriefingPage(KneeboardPage):
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
if self.flight.custom_name:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
@@ -403,6 +402,19 @@ class BriefingPage(KneeboardPage):
|
||||
writer.text(
|
||||
f"Turbulence: {round(self.weather.atmospheric.turbulence_per_10cm)} per 10cm at ground level."
|
||||
)
|
||||
writer.text(
|
||||
f"Wind: {self.weather.wind.at_0m.direction}°"
|
||||
f" / {round(mps(self.weather.wind.at_0m.speed).knots)}kts (0ft)"
|
||||
f" ; {self.weather.wind.at_2000m.direction}°"
|
||||
f" / {round(mps(self.weather.wind.at_2000m.speed).knots)}kts (~6500ft)"
|
||||
f" ; {self.weather.wind.at_8000m.direction}°"
|
||||
f" / {round(mps(self.weather.wind.at_8000m.speed).knots)}kts (~26000ft)"
|
||||
)
|
||||
c = self.weather.clouds
|
||||
writer.text(
|
||||
f'Cloud base: {f"{int(round(meters(c.base).feet, -2))}ft" if c else "CAVOK"}'
|
||||
f'{f", {c.preset.ui_name[:-2]}" if c and c.preset else ""}'
|
||||
)
|
||||
|
||||
fl = self.flight
|
||||
|
||||
@@ -456,7 +468,7 @@ class BriefingPage(KneeboardPage):
|
||||
ils = ""
|
||||
return [
|
||||
row_title,
|
||||
"\n".join(textwrap.wrap(runway.airfield_name, width=24)),
|
||||
"\n".join(textwrap.wrap(runway.airfield_name, width=17)),
|
||||
atc,
|
||||
tacan,
|
||||
ils,
|
||||
@@ -482,6 +494,7 @@ class SupportPage(KneeboardPage):
|
||||
def __init__(
|
||||
self,
|
||||
flight: FlightData,
|
||||
package_flights: List[FlightData],
|
||||
comms: List[CommInfo],
|
||||
awacs: List[AwacsInfo],
|
||||
tankers: List[TankerInfo],
|
||||
@@ -490,6 +503,7 @@ class SupportPage(KneeboardPage):
|
||||
dark_kneeboard: bool,
|
||||
) -> None:
|
||||
self.flight = flight
|
||||
self.package_flights = package_flights
|
||||
self.comms = list(comms)
|
||||
self.awacs = awacs
|
||||
self.tankers = tankers
|
||||
@@ -500,60 +514,92 @@ class SupportPage(KneeboardPage):
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
if self.flight.custom_name:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
writer.title(f"{self.flight.callsign} Support Info{custom_name_title}")
|
||||
|
||||
# Package Section
|
||||
package = self.flight.package
|
||||
custom = f' "{package.custom_name}"' if package.custom_name else ""
|
||||
writer.heading(f"{package.package_description} Package{custom}")
|
||||
freq = self.format_frequency(package.frequency).replace("\n", " - ")
|
||||
writer.text(f" FREQ: {freq}", font=writer.table_font)
|
||||
tot = self._format_time(package.time_over_target)
|
||||
writer.text(f" TOT: {tot}", font=writer.table_font)
|
||||
comm_ladder = []
|
||||
for comm in self.comms:
|
||||
comm_ladder.append(
|
||||
[
|
||||
comm.name,
|
||||
"",
|
||||
"",
|
||||
str(len(self.flight.units)),
|
||||
self.format_frequency(comm.freq),
|
||||
]
|
||||
)
|
||||
for f in self.package_flights:
|
||||
comm_ladder.append(
|
||||
[
|
||||
f.callsign,
|
||||
str(f.flight_type),
|
||||
KneeboardPageWriter.wrap_line(str(f.aircraft_type), 23),
|
||||
str(len(f.units)),
|
||||
self.format_frequency(f.intra_flight_channel),
|
||||
]
|
||||
)
|
||||
|
||||
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "#A/C", "FREQ"])
|
||||
|
||||
# AEW&C
|
||||
writer.heading("AEW&C")
|
||||
aewc_ladder = []
|
||||
|
||||
for single_aewc in self.awacs:
|
||||
|
||||
if single_aewc.depature_location is None:
|
||||
dep = "-"
|
||||
arr = "-"
|
||||
tot = "-"
|
||||
tos = "-"
|
||||
else:
|
||||
dep = self._format_time(single_aewc.start_time)
|
||||
arr = self._format_time(single_aewc.end_time)
|
||||
tot = self._format_time(single_aewc.start_time)
|
||||
tos = self._format_duration(
|
||||
single_aewc.end_time - single_aewc.start_time
|
||||
)
|
||||
|
||||
aewc_ladder.append(
|
||||
[
|
||||
str(single_aewc.callsign),
|
||||
self.format_frequency(single_aewc.freq),
|
||||
str(single_aewc.depature_location),
|
||||
str(dep),
|
||||
str(arr),
|
||||
"TOT: " + tot + "\n" + "TOS: " + tos,
|
||||
]
|
||||
)
|
||||
|
||||
writer.table(
|
||||
aewc_ladder,
|
||||
headers=["Callsign", "FREQ", "Depature", "ETD", "ETA"],
|
||||
headers=["Callsign", "FREQ", "Departure", "TOT / TOS"],
|
||||
)
|
||||
|
||||
# Package Section
|
||||
writer.heading("Comm ladder")
|
||||
comm_ladder = []
|
||||
for comm in self.comms:
|
||||
comm_ladder.append(
|
||||
[comm.name, "", "", "", self.format_frequency(comm.freq)]
|
||||
)
|
||||
|
||||
writer.heading("Tankers:")
|
||||
for tanker in self.tankers:
|
||||
tot = self._format_time(tanker.start_time)
|
||||
tos = self._format_duration(tanker.end_time - tanker.start_time)
|
||||
comm_ladder.append(
|
||||
[
|
||||
tanker.callsign,
|
||||
"Tanker",
|
||||
tanker.variant,
|
||||
KneeboardPageWriter.wrap_line(tanker.variant, 21),
|
||||
str(tanker.tacan),
|
||||
self.format_frequency(tanker.freq),
|
||||
"TOT: " + tot + "\n" + "TOS: " + tos,
|
||||
]
|
||||
)
|
||||
|
||||
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "TACAN", "FREQ"])
|
||||
writer.table(
|
||||
comm_ladder,
|
||||
headers=["Callsign", "Task", "Type", "TACAN", "FREQ", "TOT / TOS"],
|
||||
)
|
||||
|
||||
writer.heading("JTAC")
|
||||
jtacs = []
|
||||
@@ -573,7 +619,9 @@ class SupportPage(KneeboardPage):
|
||||
|
||||
writer.write(path)
|
||||
|
||||
def format_frequency(self, frequency: RadioFrequency) -> str:
|
||||
def format_frequency(self, frequency: Optional[RadioFrequency]) -> str:
|
||||
if frequency is None:
|
||||
return ""
|
||||
channel = self.flight.channel_for(frequency)
|
||||
if channel is None:
|
||||
return str(frequency)
|
||||
@@ -589,6 +637,13 @@ class SupportPage(KneeboardPage):
|
||||
local_time = self.start_time + time
|
||||
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(time: Optional[datetime.timedelta]) -> str:
|
||||
if time is None:
|
||||
return ""
|
||||
time -= datetime.timedelta(microseconds=time.microseconds)
|
||||
return f"{time}"
|
||||
|
||||
|
||||
class SeadTaskPage(KneeboardPage):
|
||||
"""A kneeboard page containing SEAD/DEAD target information."""
|
||||
@@ -611,7 +666,7 @@ class SeadTaskPage(KneeboardPage):
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
if self.flight.custom_name:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
@@ -653,7 +708,7 @@ class StrikeTaskPage(KneeboardPage):
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
if self.flight.custom_name is not None:
|
||||
if self.flight.custom_name:
|
||||
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
|
||||
else:
|
||||
custom_name_title = ""
|
||||
@@ -733,8 +788,13 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
for flight in self.flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
package_flights = [
|
||||
f
|
||||
for f in self.flights
|
||||
if f.package is flight.package and f is not flight
|
||||
]
|
||||
all_flights[flight.aircraft_type].extend(
|
||||
self.generate_flight_kneeboard(flight)
|
||||
self.generate_flight_kneeboard(flight, package_flights)
|
||||
)
|
||||
return all_flights
|
||||
|
||||
@@ -745,7 +805,9 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
return StrikeTaskPage(flight, self.dark_kneeboard)
|
||||
return None
|
||||
|
||||
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
||||
def generate_flight_kneeboard(
|
||||
self, flight: FlightData, package_flights: List[FlightData]
|
||||
) -> List[KneeboardPage]:
|
||||
"""Returns a list of kneeboard pages for the given flight."""
|
||||
|
||||
if flight.aircraft_type.utc_kneeboard:
|
||||
@@ -765,6 +827,7 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
),
|
||||
SupportPage(
|
||||
flight,
|
||||
package_flights,
|
||||
self.comms,
|
||||
self.awacs,
|
||||
self.tankers,
|
||||
|
||||
@@ -33,8 +33,8 @@ class AwacsInfo(GroupInfo):
|
||||
"""AWACS information for the kneeboard."""
|
||||
|
||||
depature_location: Optional[str]
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
start_time: timedelta
|
||||
end_time: timedelta
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -43,8 +43,8 @@ class TankerInfo(GroupInfo):
|
||||
|
||||
variant: str
|
||||
tacan: TacanChannel
|
||||
start_time: Optional[timedelta]
|
||||
end_time: Optional[timedelta]
|
||||
start_time: timedelta
|
||||
end_time: timedelta
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -17,13 +17,11 @@ from game.missiongenerator.aircraft.aircraftgenerator import (
|
||||
AircraftGenerator,
|
||||
)
|
||||
from game.naming import namegen
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry, MHz
|
||||
from game.radio.tacan import TacanRegistry
|
||||
from game.theater import Airfield
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.unitmap import UnitMap
|
||||
from .airconflictdescription import AirConflictDescription
|
||||
from .airsupportgenerator import AirSupportGenerator
|
||||
from .briefinggenerator import BriefingGenerator, MissionInfoGenerator
|
||||
from .cargoshipgenerator import CargoShipGenerator
|
||||
from .convoygenerator import ConvoyGenerator
|
||||
@@ -39,14 +37,12 @@ from .missiondata import MissionData
|
||||
from .tgogenerator import TgoGenerator
|
||||
from .triggergenerator import TriggerGenerator
|
||||
from .visualsgenerator import VisualsGenerator
|
||||
from ..radio.TacanContainer import TacanContainer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
COMBINED_ARMS_SLOTS = 3
|
||||
|
||||
|
||||
def country_id_from_name(name: str) -> int:
|
||||
for k, v in country_dict.items():
|
||||
if v.name == name:
|
||||
@@ -70,7 +66,11 @@ class MissionGenerator:
|
||||
self.generation_started = False
|
||||
|
||||
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
|
||||
self.mission.options.load_from_dict(dcs.lua.loads(f.read())["options"])
|
||||
options = dcs.lua.loads(f.read())["options"]
|
||||
ext_view = game.settings.external_views_allowed
|
||||
options["miscellaneous"]["f11_free_camera"] = ext_view
|
||||
options["difficulty"]["spectatorExternalViews"] = ext_view
|
||||
self.mission.options.load_from_dict(options)
|
||||
|
||||
def generate_miz(self, output: Path) -> UnitMap:
|
||||
if self.generation_started:
|
||||
@@ -159,6 +159,9 @@ class MissionGenerator:
|
||||
unique_map_frequencies: set[RadioFrequency] = set()
|
||||
self.initialize_tacan_registry(unique_map_frequencies)
|
||||
self.initialize_radio_registry(unique_map_frequencies)
|
||||
# Allocate UHF/VHF Guard Freq first!
|
||||
unique_map_frequencies.add(MHz(243))
|
||||
unique_map_frequencies.add(MHz(121, 500))
|
||||
for frequency in unique_map_frequencies:
|
||||
self.radio_registry.reserve(frequency)
|
||||
|
||||
@@ -176,6 +179,9 @@ class MissionGenerator:
|
||||
logging.warning(f"TACAN beacon has no channel: {beacon.callsign}")
|
||||
else:
|
||||
self.tacan_registry.mark_unavailable(beacon.tacan_channel)
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if isinstance(cp, TacanContainer) and cp.tacan is not None:
|
||||
self.tacan_registry.mark_unavailable(cp.tacan)
|
||||
|
||||
def initialize_radio_registry(
|
||||
self, unique_map_frequencies: set[RadioFrequency]
|
||||
@@ -220,17 +226,6 @@ class MissionGenerator:
|
||||
def generate_air_units(self, tgo_generator: TgoGenerator) -> None:
|
||||
"""Generate the air units for the Operation"""
|
||||
|
||||
# Air Support (Tanker & Awacs)
|
||||
air_support_generator = AirSupportGenerator(
|
||||
self.mission,
|
||||
AirConflictDescription.for_theater(self.game.theater),
|
||||
self.game,
|
||||
self.radio_registry,
|
||||
self.tacan_registry,
|
||||
self.mission_data,
|
||||
)
|
||||
air_support_generator.generate()
|
||||
|
||||
# Generate Aircraft Activity on the map
|
||||
aircraft_generator = AircraftGenerator(
|
||||
self.mission,
|
||||
@@ -241,7 +236,7 @@ class MissionGenerator:
|
||||
self.tacan_registry,
|
||||
self.laser_code_registry,
|
||||
self.unit_map,
|
||||
mission_data=air_support_generator.mission_data,
|
||||
mission_data=self.mission_data,
|
||||
helipads=tgo_generator.helipads,
|
||||
)
|
||||
|
||||
@@ -257,19 +252,18 @@ class MissionGenerator:
|
||||
self.game.red.ato,
|
||||
tgo_generator.runways,
|
||||
)
|
||||
aircraft_generator.spawn_unused_aircraft(
|
||||
self.mission.country(self.game.blue.country_name),
|
||||
self.mission.country(self.game.red.country_name),
|
||||
)
|
||||
if not self.game.settings.perf_disable_idle_aircraft:
|
||||
aircraft_generator.spawn_unused_aircraft(
|
||||
self.mission.country(self.game.blue.country_name),
|
||||
self.mission.country(self.game.red.country_name),
|
||||
)
|
||||
|
||||
self.mission_data.flights = aircraft_generator.flights
|
||||
|
||||
for flight in aircraft_generator.flights:
|
||||
if not flight.client_units:
|
||||
continue
|
||||
flight.aircraft_type.assign_channels_for_flight(
|
||||
flight, air_support_generator.mission_data
|
||||
)
|
||||
|
||||
self.mission_data.flights = aircraft_generator.flights
|
||||
flight.aircraft_type.assign_channels_for_flight(flight, self.mission_data)
|
||||
|
||||
def generate_destroyed_units(self) -> None:
|
||||
"""Add destroyed units to the Mission"""
|
||||
@@ -330,12 +324,11 @@ class MissionGenerator:
|
||||
gen.generate()
|
||||
|
||||
def setup_combined_arms(self) -> None:
|
||||
observers = 3
|
||||
self.mission.groundControl.pilot_can_control_vehicles = COMBINED_ARMS_SLOTS > 0
|
||||
self.mission.groundControl.blue_game_masters = 1
|
||||
self.mission.groundControl.blue_tactical_commander = COMBINED_ARMS_SLOTS
|
||||
self.mission.groundControl.blue_observer = observers
|
||||
self.mission.groundControl.red_game_masters = 1
|
||||
self.mission.groundControl.red_tactical_commander = COMBINED_ARMS_SLOTS
|
||||
self.mission.groundControl.red_observer = observers
|
||||
self.mission.groundControl.neutrals_observer = observers
|
||||
settings = self.game.settings
|
||||
commanders = settings.tactical_commander_count
|
||||
self.mission.groundControl.pilot_can_control_vehicles = commanders > 0
|
||||
|
||||
self.mission.groundControl.blue_game_masters = settings.game_masters_count
|
||||
self.mission.groundControl.blue_tactical_commander = commanders
|
||||
self.mission.groundControl.blue_jtac = settings.jtac_count
|
||||
self.mission.groundControl.blue_observer = settings.observer_count
|
||||
|
||||
@@ -22,6 +22,8 @@ from dcs.ships import (
|
||||
CVN_73,
|
||||
CVN_75,
|
||||
Stennis,
|
||||
Forrestal,
|
||||
LHA_Tarawa,
|
||||
)
|
||||
from dcs.statics import Fortification
|
||||
from dcs.task import (
|
||||
@@ -35,7 +37,7 @@ from dcs.task import (
|
||||
)
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerStart, TriggerZone
|
||||
from dcs.unit import Unit, InvisibleFARP
|
||||
from dcs.unit import Unit, InvisibleFARP, BaseFARP
|
||||
from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unittype import ShipType, VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
@@ -45,10 +47,16 @@ from game.missiongenerator.groundforcepainter import (
|
||||
GroundForcePainter,
|
||||
)
|
||||
from game.missiongenerator.missiondata import CarrierInfo, MissionData
|
||||
from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||
from game.runways import RunwayData
|
||||
from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
TheaterGroundObject,
|
||||
TheaterUnit,
|
||||
NavalControlPoint,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
CarrierGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
@@ -351,7 +359,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
ground_object: GenericCarrierGroundObject,
|
||||
control_point: ControlPoint,
|
||||
control_point: NavalControlPoint,
|
||||
country: Country,
|
||||
game: Game,
|
||||
mission: Mission,
|
||||
@@ -372,9 +380,12 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
self.mission_data = mission_data
|
||||
|
||||
def generate(self) -> None:
|
||||
|
||||
# This can also be refactored as the general generation was updated
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
if self.control_point.frequency is not None:
|
||||
atc = self.control_point.frequency
|
||||
if atc not in self.radio_registry.allocated_channels:
|
||||
self.radio_registry.reserve(atc)
|
||||
else:
|
||||
atc = self.radio_registry.alloc_uhf()
|
||||
|
||||
for g_id, group in enumerate(self.ground_object.groups):
|
||||
if not group.units:
|
||||
@@ -409,15 +420,33 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
f"Error generating carrier group for {self.control_point.name}"
|
||||
)
|
||||
ship_group.units[0].type = carrier_type.id
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.X, TacanUsage.TransmitReceive
|
||||
)
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
icls = next(self.icls_alloc)
|
||||
if self.control_point.tacan is None:
|
||||
tacan = self.tacan_registry.alloc_for_band(
|
||||
TacanBand.X, TacanUsage.TransmitReceive
|
||||
)
|
||||
else:
|
||||
tacan = self.control_point.tacan
|
||||
if self.control_point.tcn_name is None:
|
||||
tacan_callsign = self.tacan_callsign()
|
||||
else:
|
||||
tacan_callsign = self.control_point.tcn_name
|
||||
link4 = None
|
||||
if carrier_type in [Stennis, CVN_71, CVN_72, CVN_73, CVN_75]:
|
||||
link4 = self.radio_registry.alloc_uhf()
|
||||
self.activate_beacons(ship_group, tacan, tacan_callsign, icls, link4)
|
||||
link4carriers = [Stennis, CVN_71, CVN_72, CVN_73, CVN_75, Forrestal]
|
||||
if carrier_type in link4carriers:
|
||||
if self.control_point.link4 is None:
|
||||
link4 = self.radio_registry.alloc_uhf()
|
||||
else:
|
||||
link4 = self.control_point.link4
|
||||
icls = None
|
||||
icls_name = self.control_point.icls_name
|
||||
if carrier_type in link4carriers or carrier_type == LHA_Tarawa:
|
||||
if self.control_point.icls_channel is None:
|
||||
icls = next(self.icls_alloc)
|
||||
else:
|
||||
icls = self.control_point.icls_channel
|
||||
self.activate_beacons(
|
||||
ship_group, tacan, tacan_callsign, icls, icls_name, link4
|
||||
)
|
||||
self.add_runway_data(
|
||||
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
|
||||
)
|
||||
@@ -461,7 +490,8 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
group: ShipGroup,
|
||||
tacan: TacanChannel,
|
||||
callsign: str,
|
||||
icls: int,
|
||||
icls: Optional[int] = None,
|
||||
icls_name: Optional[str] = None,
|
||||
link4: Optional[RadioFrequency] = None,
|
||||
) -> None:
|
||||
group.points[0].tasks.append(
|
||||
@@ -473,12 +503,14 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
aa=False,
|
||||
)
|
||||
)
|
||||
group.points[0].tasks.append(
|
||||
ActivateICLSCommand(icls, unit_id=group.units[0].id)
|
||||
)
|
||||
if icls is not None:
|
||||
icls_name = "" if icls_name is None else icls_name
|
||||
group.points[0].tasks.append(
|
||||
ActivateICLSCommand(icls, group.units[0].id, icls_name)
|
||||
)
|
||||
if link4 is not None:
|
||||
group.points[0].tasks.append(
|
||||
ActivateLink4Command(int(link4.mhz), group.units[0].id)
|
||||
ActivateLink4Command(link4.hertz, group.units[0].id)
|
||||
)
|
||||
group.points[0].tasks.append(ActivateACLSCommand(unit_id=group.units[0].id))
|
||||
|
||||
@@ -488,9 +520,8 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
atc: RadioFrequency,
|
||||
tacan: TacanChannel,
|
||||
callsign: str,
|
||||
icls: int,
|
||||
icls: Optional[int],
|
||||
) -> None:
|
||||
# TODO: Make unit name usable.
|
||||
# This relies on one control point mapping exactly
|
||||
# to one LHA, carrier, or other usable "runway".
|
||||
# This isn't wholly true, since the DD escorts of
|
||||
@@ -498,10 +529,10 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
||||
# they aren't exposed as such to the game. Should
|
||||
# clean this up so that's possible. We can't use the
|
||||
# unit name since it's an arbitrary ID.
|
||||
self.runways[self.control_point.name] = RunwayData(
|
||||
self.runways[self.control_point.full_name] = RunwayData(
|
||||
self.control_point.name,
|
||||
brc,
|
||||
"N/A",
|
||||
f"{brc.degrees:03}",
|
||||
atc=atc,
|
||||
tacan=tacan,
|
||||
tacan_callsign=callsign,
|
||||
@@ -590,6 +621,13 @@ class HelipadGenerator:
|
||||
self.helipads.add_unit(
|
||||
InvisibleFARP(self.m.terrain, self.m.next_unit_id(), name_i)
|
||||
)
|
||||
|
||||
# Set FREQ
|
||||
if isinstance(self.cp, RadioFrequencyContainer) and self.cp.frequency:
|
||||
for hp in self.helipads.units:
|
||||
if isinstance(hp, BaseFARP):
|
||||
hp.heliport_frequency = self.cp.frequency.mhz
|
||||
|
||||
pad = self.helipads.units[-1]
|
||||
pad.position = helipad
|
||||
pad.heading = heading
|
||||
@@ -661,7 +699,9 @@ class TgoGenerator:
|
||||
|
||||
for ground_object in cp.ground_objects:
|
||||
generator: GroundObjectGenerator
|
||||
if isinstance(ground_object, CarrierGroundObject):
|
||||
if isinstance(ground_object, CarrierGroundObject) and isinstance(
|
||||
cp, NavalControlPoint
|
||||
):
|
||||
generator = CarrierGenerator(
|
||||
ground_object,
|
||||
cp,
|
||||
@@ -675,7 +715,9 @@ class TgoGenerator:
|
||||
self.unit_map,
|
||||
self.mission_data,
|
||||
)
|
||||
elif isinstance(ground_object, LhaGroundObject):
|
||||
elif isinstance(ground_object, LhaGroundObject) and isinstance(
|
||||
cp, NavalControlPoint
|
||||
):
|
||||
generator = LhaGenerator(
|
||||
ground_object,
|
||||
cp,
|
||||
|
||||
@@ -2,8 +2,9 @@ from typing import Type
|
||||
|
||||
from dcs.helicopters import HelicopterType, helicopter_map
|
||||
from dcs.planes import PlaneType, plane_map
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.unittype import VehicleType, ShipType
|
||||
from dcs.vehicles import vehicle_map
|
||||
from dcs.ships import ship_map
|
||||
|
||||
|
||||
def helicoptermod(helicopter: Type[HelicopterType]) -> Type[HelicopterType]:
|
||||
@@ -19,3 +20,8 @@ def planemod(plane: Type[PlaneType]) -> Type[PlaneType]:
|
||||
def vehiclemod(vehicle: Type[VehicleType]) -> Type[VehicleType]:
|
||||
vehicle_map[vehicle.id] = vehicle
|
||||
return vehicle
|
||||
|
||||
|
||||
def shipmod(ship: Type[ShipType]) -> Type[ShipType]:
|
||||
ship_map[ship.id] = ship
|
||||
return ship
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
from typing import List, Optional, TYPE_CHECKING, Any
|
||||
|
||||
from game.settings import Settings
|
||||
|
||||
@@ -32,9 +32,9 @@ class LuaPluginWorkOrder:
|
||||
|
||||
|
||||
class PluginSettings:
|
||||
def __init__(self, identifier: str, enabled_by_default: bool) -> None:
|
||||
def __init__(self, identifier: str, value: Any) -> None:
|
||||
self.identifier = identifier
|
||||
self.enabled_by_default = enabled_by_default
|
||||
self.value = value
|
||||
self.settings = Settings()
|
||||
self.initialize_settings()
|
||||
|
||||
@@ -46,20 +46,26 @@ class PluginSettings:
|
||||
# Plugin options are saved in the game's Settings, but it's possible for
|
||||
# plugins to change across loads. If new plugins are added or new
|
||||
# options added to those plugins, initialize the new settings.
|
||||
self.settings.initialize_plugin_option(self.identifier, self.enabled_by_default)
|
||||
self.settings.initialize_plugin_option(self.identifier, self.value)
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
def get_value(self) -> Any:
|
||||
return self.settings.plugin_option(self.identifier)
|
||||
|
||||
def set_enabled(self, enabled: bool) -> None:
|
||||
self.settings.set_plugin_option(self.identifier, enabled)
|
||||
def set_value(self, value: Any) -> None:
|
||||
self.settings.set_plugin_option(self.identifier, value)
|
||||
|
||||
|
||||
class LuaPluginOption(PluginSettings):
|
||||
def __init__(self, identifier: str, name: str, enabled_by_default: bool) -> None:
|
||||
super().__init__(identifier, enabled_by_default)
|
||||
def __init__(
|
||||
self, identifier: str, name: str, min: Any, max: Any, value: Any
|
||||
) -> None:
|
||||
super().__init__(identifier, value)
|
||||
self.name = name
|
||||
if type(value) == int or type(value) == float:
|
||||
self.min, self.max = min, max
|
||||
else:
|
||||
self.min, self.max = None, None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -83,7 +89,9 @@ class LuaPluginDefinition:
|
||||
LuaPluginOption(
|
||||
identifier=f"{name}.{option_id}",
|
||||
name=option.get("nameInUI", name),
|
||||
enabled_by_default=option.get("defaultValue"),
|
||||
min=option.get("minimumValue", 0),
|
||||
max=option.get("maximumValue", 10000),
|
||||
value=option.get("defaultValue"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -136,6 +144,10 @@ class LuaPlugin(PluginSettings):
|
||||
def options(self) -> List[LuaPluginOption]:
|
||||
return self.definition.options
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return type(self.value) == bool and self.value
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]:
|
||||
try:
|
||||
@@ -160,9 +172,9 @@ class LuaPlugin(PluginSettings):
|
||||
if self.options:
|
||||
option_decls = []
|
||||
for option in self.options:
|
||||
enabled = str(option.enabled).lower()
|
||||
value = str(option.value).lower()
|
||||
name = option.identifier
|
||||
option_decls.append(f" dcsRetribution.plugins.{name} = {enabled}")
|
||||
option_decls.append(f" dcsRetribution.plugins.{name} = {value}")
|
||||
|
||||
joined_options = "\n".join(option_decls)
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ if TYPE_CHECKING:
|
||||
from game.factions.faction import Faction
|
||||
from game.squadrons import Squadron
|
||||
|
||||
FRONTLINE_RESERVES_FACTOR = 1.3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftProcurementRequest:
|
||||
@@ -41,7 +39,6 @@ class ProcurementAi:
|
||||
manage_front_line: bool,
|
||||
manage_aircraft: bool,
|
||||
) -> None:
|
||||
|
||||
self.game = game
|
||||
self.is_player = for_player
|
||||
self.air_wing = game.air_wing_for(for_player)
|
||||
@@ -74,13 +71,15 @@ class ProcurementAi:
|
||||
cp_aircraft = cp.allocated_aircraft()
|
||||
aircraft_investment += cp_aircraft.total_value
|
||||
|
||||
total_investment = aircraft_investment + armor_investment
|
||||
if total_investment == 0:
|
||||
air = self.game.settings.auto_procurement_balance / 100.0
|
||||
ground = 1 - air
|
||||
weighted_investment = aircraft_investment * air + armor_investment * ground
|
||||
if weighted_investment == 0:
|
||||
# Turn 0 or all units were destroyed. Either way, split 30/70.
|
||||
return 0.3
|
||||
return min(0.3, ground)
|
||||
|
||||
# the more planes we have, the more ground units we want and vice versa
|
||||
ground_unit_share = aircraft_investment / total_investment
|
||||
ground_unit_share = aircraft_investment * air / weighted_investment
|
||||
if ground_unit_share > 1.0:
|
||||
raise ValueError
|
||||
|
||||
@@ -257,7 +256,8 @@ class ProcurementAi:
|
||||
# No source of ground units, so can't buy anything.
|
||||
continue
|
||||
|
||||
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
|
||||
fr_factor = self.game.settings.frontline_reserves_factor / 100.0
|
||||
purchase_target = cp.frontline_unit_count_limit * fr_factor
|
||||
allocated = cp.allocated_ground_units(
|
||||
self.game.coalition_for(self.is_player).transfers
|
||||
)
|
||||
|
||||
6
game/radio/ICLSContainer.py
Normal file
6
game/radio/ICLSContainer.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ICLSContainer:
|
||||
icls_channel: Optional[int] = None
|
||||
icls_name: Optional[str] = None
|
||||
8
game/radio/Link4Container.py
Normal file
8
game/radio/Link4Container.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import Optional
|
||||
|
||||
from game.radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from game.radio.radios import RadioFrequency
|
||||
|
||||
|
||||
class Link4Container(RadioFrequencyContainer):
|
||||
link4: Optional[RadioFrequency] = None
|
||||
7
game/radio/RadioFrequencyContainer.py
Normal file
7
game/radio/RadioFrequencyContainer.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from typing import Optional
|
||||
|
||||
from game.radio.radios import RadioFrequency
|
||||
|
||||
|
||||
class RadioFrequencyContainer:
|
||||
frequency: Optional[RadioFrequency] = None
|
||||
8
game/radio/TacanContainer.py
Normal file
8
game/radio/TacanContainer.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import Optional
|
||||
|
||||
from game.radio.tacan import TacanChannel
|
||||
|
||||
|
||||
class TacanContainer:
|
||||
tacan: Optional[TacanChannel] = None
|
||||
tcn_name: Optional[str] = None
|
||||
@@ -76,6 +76,9 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
|
||||
for jtac in mission_data.jtacs:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), jtac.freq)
|
||||
|
||||
if (freq := flight.package.frequency) is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), freq)
|
||||
|
||||
if flight.arrival != flight.departure and flight.arrival.atc is not None:
|
||||
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, FrozenSet, Iterator, List, Set, Tuple
|
||||
@@ -156,57 +157,61 @@ class ChannelInUseError(RuntimeError):
|
||||
# TODO: Figure out appropriate steps for each radio. These are just guesses.
|
||||
#: List of all known radios used by aircraft in the game.
|
||||
RADIOS: List[Radio] = [
|
||||
Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
|
||||
Radio("AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), MHz(1), Modulation.AM),)),
|
||||
Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), MHz(1), Modulation.FM),)),
|
||||
Radio("AN/ARC-164", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
|
||||
Radio(
|
||||
"AN/ARC-186(V) AM", (RadioRange(MHz(116), MHz(152), kHz(25), Modulation.AM),)
|
||||
),
|
||||
Radio("AN/ARC-186(V) FM", (RadioRange(MHz(30), MHz(76), kHz(25), Modulation.FM),)),
|
||||
Radio(
|
||||
"AN/ARC-210",
|
||||
(
|
||||
RadioRange(
|
||||
MHz(225),
|
||||
MHz(400),
|
||||
MHz(1),
|
||||
kHz(25),
|
||||
Modulation.AM,
|
||||
frozenset((MHz(243),)),
|
||||
),
|
||||
RadioRange(MHz(136), MHz(155), MHz(1), Modulation.AM),
|
||||
RadioRange(MHz(156), MHz(174), MHz(1), Modulation.FM),
|
||||
RadioRange(MHz(118), MHz(136), MHz(1), Modulation.AM),
|
||||
RadioRange(MHz(30), MHz(88), MHz(1), Modulation.FM),
|
||||
RadioRange(MHz(136), MHz(155), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(156), MHz(174), kHz(25), Modulation.FM),
|
||||
RadioRange(MHz(118), MHz(136), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(30), MHz(88), kHz(25), Modulation.FM),
|
||||
# The AN/ARC-210 can also use 225-400 and 136-155 with FM Modulation
|
||||
RadioRange(
|
||||
MHz(225),
|
||||
MHz(400),
|
||||
MHz(1),
|
||||
kHz(25),
|
||||
Modulation.FM,
|
||||
frozenset((MHz(243),)),
|
||||
),
|
||||
RadioRange(MHz(136), MHz(155), MHz(1), Modulation.FM),
|
||||
RadioRange(MHz(136), MHz(155), kHz(25), Modulation.FM),
|
||||
),
|
||||
),
|
||||
Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(152), MHz(1), Modulation.AM),)),
|
||||
Radio("SCR-522", (RadioRange(MHz(100), MHz(156), MHz(1), Modulation.AM),)),
|
||||
Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), MHz(1), Modulation.AM),)),
|
||||
Radio("AN/ARC-222", (RadioRange(MHz(116), MHz(152), kHz(25), Modulation.AM),)),
|
||||
Radio("SCR-522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
|
||||
Radio("A.R.I. 1063", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
|
||||
Radio("BC-1206", (RadioRange(kHz(200), kHz(400), kHz(10), Modulation.AM),)),
|
||||
Radio(
|
||||
"TRT ERA 7000 V/UHF",
|
||||
(
|
||||
RadioRange(MHz(118), MHz(150), MHz(1), Modulation.AM),
|
||||
RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),
|
||||
RadioRange(MHz(118), MHz(150), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),
|
||||
),
|
||||
),
|
||||
Radio("TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
|
||||
Radio(
|
||||
"TRT ERA 7200 UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)
|
||||
),
|
||||
# Tomcat radios
|
||||
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
|
||||
Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
|
||||
Radio("AN/ARC-159", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
|
||||
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
|
||||
Radio(
|
||||
"AN/ARC-182",
|
||||
(
|
||||
RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),
|
||||
RadioRange(MHz(108), MHz(174), MHz(1), Modulation.AM),
|
||||
RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(108), MHz(174), kHz(25), Modulation.AM),
|
||||
# The Range from 30-88MHz should be FM but its modeled as AM in dcs
|
||||
RadioRange(MHz(30), MHz(88), MHz(1), Modulation.AM),
|
||||
RadioRange(MHz(30), MHz(88), kHz(25), Modulation.AM),
|
||||
),
|
||||
),
|
||||
Radio(
|
||||
@@ -220,18 +225,28 @@ RADIOS: List[Radio] = [
|
||||
# 4 preset channels (A/B/C/D)
|
||||
Radio("SCR522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
|
||||
# JF-17 Radios should use AM
|
||||
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), MHz(1), Modulation.AM),)),
|
||||
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), MHz(1), Modulation.AM),)),
|
||||
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), kHz(25), Modulation.AM),)),
|
||||
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
|
||||
# MiG-15bis
|
||||
Radio("RSI-6K HF", (RadioRange(MHz(3, 750), MHz(5), kHz(25), Modulation.AM),)),
|
||||
# MiG-19P
|
||||
Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), MHz(1), Modulation.AM),)),
|
||||
Radio("RSIU-4V", (RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),)),
|
||||
# MiG-21bis
|
||||
Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), MHz(1), Modulation.AM),)),
|
||||
Radio("RSIU-5V", (RadioRange(MHz(118), MHz(140), kHz(25), Modulation.AM),)),
|
||||
# Ka-50
|
||||
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
|
||||
Radio("R-800L1", (RadioRange(MHz(220), MHz(400), kHz(25), Modulation.AM),)),
|
||||
Radio("R-828", (RadioRange(MHz(20), MHz(60), kHz(25), Modulation.FM),)),
|
||||
# Mi-8 / Mi-24P
|
||||
Radio(
|
||||
"R-863",
|
||||
(
|
||||
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(220), MHz(400), kHz(25), Modulation.AM),
|
||||
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.FM),
|
||||
RadioRange(MHz(220), MHz(400), kHz(25), Modulation.FM),
|
||||
),
|
||||
),
|
||||
# UH-1H
|
||||
Radio("AN/ARC-51BX", (RadioRange(MHz(225), MHz(400), kHz(50), Modulation.AM),)),
|
||||
Radio("AN/ARC-131", (RadioRange(MHz(30), MHz(76), kHz(50), Modulation.FM),)),
|
||||
@@ -263,7 +278,7 @@ RADIOS: List[Radio] = [
|
||||
RadioRange(
|
||||
MHz(225),
|
||||
MHz(400),
|
||||
MHz(1),
|
||||
kHz(25),
|
||||
Modulation.AM,
|
||||
frozenset((MHz(243),)),
|
||||
),
|
||||
@@ -275,28 +290,28 @@ RADIOS: List[Radio] = [
|
||||
RadioRange(
|
||||
MHz(30),
|
||||
MHz(88),
|
||||
MHz(1),
|
||||
kHz(25),
|
||||
Modulation.FM,
|
||||
frozenset((MHz(40, 500),)),
|
||||
),
|
||||
RadioRange(
|
||||
MHz(108),
|
||||
MHz(156),
|
||||
MHz(1),
|
||||
kHz(25),
|
||||
Modulation.AM,
|
||||
frozenset((MHz(121, 500),)),
|
||||
),
|
||||
RadioRange(
|
||||
MHz(156),
|
||||
MHz(174),
|
||||
MHz(1),
|
||||
kHz(25),
|
||||
Modulation.FM,
|
||||
frozenset((MHz(156, 800),)),
|
||||
),
|
||||
RadioRange(
|
||||
MHz(225),
|
||||
MHz(400),
|
||||
MHz(1),
|
||||
kHz(25),
|
||||
Modulation.AM, # Actually AM/FM, but we can't represent that.
|
||||
frozenset((MHz(243),)),
|
||||
),
|
||||
@@ -323,6 +338,13 @@ def get_radio(name: str) -> Radio:
|
||||
raise KeyError(f"Unknown radio: {name}")
|
||||
|
||||
|
||||
def random_frequency(radio: Radio) -> RadioFrequency:
|
||||
range: RadioRange = radio.ranges[0]
|
||||
delta = round(random.random() * (range.maximum.hertz - range.minimum.hertz))
|
||||
delta -= delta % range.step.hertz
|
||||
return RadioFrequency(range.minimum.hertz + delta)
|
||||
|
||||
|
||||
class RadioRegistry:
|
||||
"""Manages allocation of radio channels.
|
||||
|
||||
@@ -368,9 +390,8 @@ class RadioRegistry:
|
||||
OutOfChannelsError: All channels compatible with the given radio are
|
||||
already allocated.
|
||||
"""
|
||||
allocator = self.radio_allocators[radio]
|
||||
try:
|
||||
while (channel := next(allocator)) in self.allocated_channels:
|
||||
while (channel := random_frequency(radio)) in self.allocated_channels:
|
||||
pass
|
||||
self.reserve(channel)
|
||||
return channel
|
||||
|
||||
@@ -54,8 +54,8 @@ class TacanChannel:
|
||||
if match is None:
|
||||
raise ValueError(f"Could not parse TACAN from {text}")
|
||||
number = int(match.group(1))
|
||||
if not number:
|
||||
raise ValueError("TACAN channel cannot be 0")
|
||||
if not (0 < number <= 126):
|
||||
raise ValueError("TACAN channel cannot be 0 or larger than 126")
|
||||
return TacanChannel(number, TacanBand(match.group(2)))
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ class SceneryGroup:
|
||||
def __init__(
|
||||
self, zone_def: TriggerZone, zones: Iterable[TriggerZone], category: str
|
||||
) -> None:
|
||||
|
||||
self.zone_def = zone_def
|
||||
self.zones = zones
|
||||
self.position = zone_def.position
|
||||
|
||||
@@ -50,7 +50,6 @@ class GameUpdateEventsJs(BaseModel):
|
||||
def from_events(
|
||||
cls, events: GameUpdateEvents, game: Game | None
|
||||
) -> GameUpdateEventsJs:
|
||||
|
||||
# We still need to be able to send update events when there is no game loaded
|
||||
# because we need to send the unload event.
|
||||
new_combats = []
|
||||
|
||||
@@ -36,6 +36,7 @@ CAMPAIGN_MANAGEMENT_PAGE = "Campaign Management"
|
||||
GENERAL_SECTION = "General"
|
||||
PILOTS_AND_SQUADRONS_SECTION = "Pilots and Squadrons"
|
||||
HQ_AUTOMATION_SECTION = "HQ Automation"
|
||||
FLIGHT_PLANNER_AUTOMATION = "Flight Planner Automation"
|
||||
|
||||
MISSION_GENERATOR_PAGE = "Mission Generator"
|
||||
|
||||
@@ -173,30 +174,6 @@ class Settings:
|
||||
"extremely incomplete so does not affect all weapons."
|
||||
),
|
||||
)
|
||||
disable_legacy_aewc: bool = boolean_option(
|
||||
"Spawn invulnerable, always-available AEW&C aircraft (deprecated)",
|
||||
page=CAMPAIGN_MANAGEMENT_PAGE,
|
||||
section=GENERAL_SECTION,
|
||||
default=True,
|
||||
invert=True,
|
||||
detail=(
|
||||
"If checked, an invulnerable friendly AEW&C aircraft that begins the "
|
||||
"mission on station will be be spawned. This behavior will be removed in a "
|
||||
"future release."
|
||||
),
|
||||
)
|
||||
disable_legacy_tanker: bool = boolean_option(
|
||||
"Spawn invulnerable, always-available tanker aircraft (deprecated)",
|
||||
page=CAMPAIGN_MANAGEMENT_PAGE,
|
||||
section=GENERAL_SECTION,
|
||||
default=True,
|
||||
invert=True,
|
||||
detail=(
|
||||
"If checked, an invulnerable friendly tanker aircraft that begins the "
|
||||
"mission on station will be be spawned. This behavior will be removed in a "
|
||||
"future release."
|
||||
),
|
||||
)
|
||||
# Pilots and Squadrons
|
||||
ai_pilot_levelling: bool = boolean_option(
|
||||
"Allow AI pilot leveling",
|
||||
@@ -297,7 +274,77 @@ class Settings:
|
||||
HQ_AUTOMATION_SECTION,
|
||||
default=True,
|
||||
)
|
||||
reserves_procurement_target: int = 10
|
||||
auto_procurement_balance: int = bounded_int_option(
|
||||
"AI ground unit procurement budget ratio (%)",
|
||||
CAMPAIGN_MANAGEMENT_PAGE,
|
||||
HQ_AUTOMATION_SECTION,
|
||||
min=0,
|
||||
max=100,
|
||||
default=50,
|
||||
detail=(
|
||||
"Ratio (larger number -> more budget for ground units) "
|
||||
"that indicates how the AI procurement planner should "
|
||||
"spend its budget."
|
||||
),
|
||||
)
|
||||
frontline_reserves_factor: int = bounded_int_option(
|
||||
"AI ground unit front-line reserves factor (%)",
|
||||
CAMPAIGN_MANAGEMENT_PAGE,
|
||||
HQ_AUTOMATION_SECTION,
|
||||
min=0,
|
||||
max=1000,
|
||||
default=130,
|
||||
detail=(
|
||||
"Factor to be multiplied with the control point's unit count limit "
|
||||
"to calculate the procurement target for reserve troops at front-lines."
|
||||
),
|
||||
)
|
||||
reserves_procurement_target: int = bounded_int_option(
|
||||
"AI ground unit reserves procurement target",
|
||||
CAMPAIGN_MANAGEMENT_PAGE,
|
||||
HQ_AUTOMATION_SECTION,
|
||||
min=0,
|
||||
max=1000,
|
||||
default=10,
|
||||
detail=(
|
||||
"The number of units that will be bought as reserves for applicable control points"
|
||||
),
|
||||
)
|
||||
|
||||
# Flight Planner Automation
|
||||
#: The weight used for 2-ships.
|
||||
fpa_2ship_weight: int = bounded_int_option(
|
||||
"2-ship weight factor (WF2)",
|
||||
CAMPAIGN_MANAGEMENT_PAGE,
|
||||
FLIGHT_PLANNER_AUTOMATION,
|
||||
default=50,
|
||||
min=0,
|
||||
max=100,
|
||||
detail=(
|
||||
"Used as a distribution to randomize 2/3/4-ships for BARCAP, CAS, OCA & ANTI-SHIP flights. "
|
||||
"The weight W_i is calculated according to the following formula: "
|
||||
"W_i = WF_i / (WF2 + WF3 + WF4)"
|
||||
),
|
||||
)
|
||||
#: The weight used for 3-ships.
|
||||
fpa_3ship_weight: int = bounded_int_option(
|
||||
"3-ship weight factor (WF3)",
|
||||
CAMPAIGN_MANAGEMENT_PAGE,
|
||||
FLIGHT_PLANNER_AUTOMATION,
|
||||
default=35,
|
||||
min=0,
|
||||
max=100,
|
||||
detail="See 2-ship weight factor (WF2)",
|
||||
)
|
||||
fpa_4ship_weight: int = bounded_int_option(
|
||||
"4-ship weight factor (WF4)",
|
||||
CAMPAIGN_MANAGEMENT_PAGE,
|
||||
FLIGHT_PLANNER_AUTOMATION,
|
||||
default=15,
|
||||
min=0,
|
||||
max=100,
|
||||
detail="See 2-ship weight factor (WF2)",
|
||||
)
|
||||
|
||||
# Mission Generator
|
||||
# Gameplay
|
||||
@@ -367,7 +414,7 @@ class Settings:
|
||||
"Player flights ignore TOT and spawn immediately",
|
||||
MISSION_GENERATOR_PAGE,
|
||||
GAMEPLAY_SECTION,
|
||||
default=False,
|
||||
default=True,
|
||||
detail=(
|
||||
"Does not adjust package waypoint times. Should not be used if players "
|
||||
"have runway or in-air starts."
|
||||
@@ -379,6 +426,16 @@ class Settings:
|
||||
"option only allows the player to wait on the ground.</strong>"
|
||||
),
|
||||
)
|
||||
atflir_autoswap: bool = boolean_option(
|
||||
"Auto-swap ATFLIR to LITENING",
|
||||
MISSION_GENERATOR_PAGE,
|
||||
GAMEPLAY_SECTION,
|
||||
default=True,
|
||||
detail=(
|
||||
"Automatically swaps ATFLIR to LITENING pod for newly generated land-based F-18 flights "
|
||||
"without having to change the payload. <u>Takes effect after current turn!</u>"
|
||||
),
|
||||
)
|
||||
default_start_type: StartType = choices_option(
|
||||
"Default start type for AI aircraft",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
@@ -400,6 +457,14 @@ class Settings:
|
||||
min=30,
|
||||
max=150,
|
||||
)
|
||||
desired_tanker_on_station_time: timedelta = minutes_option(
|
||||
"Desired tanker on-station time",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=timedelta(minutes=60),
|
||||
min=30,
|
||||
max=150,
|
||||
)
|
||||
# Mission specific
|
||||
max_frontline_length: int = bounded_int_option(
|
||||
"Maximum frontline length (km)",
|
||||
@@ -409,6 +474,67 @@ class Settings:
|
||||
min=1,
|
||||
max=100,
|
||||
)
|
||||
opfor_autoplanner_aggressiveness: int = bounded_int_option(
|
||||
"OPFOR autoplanner aggressiveness (%)",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=20,
|
||||
min=0,
|
||||
max=100,
|
||||
detail=(
|
||||
"Chance (larger number -> higher chance) that the OPFOR AI "
|
||||
"autoplanner will take risks and plan flights against targets "
|
||||
"within threatened airspace."
|
||||
),
|
||||
)
|
||||
game_masters_count: int = bounded_int_option(
|
||||
"Number of game masters",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=1,
|
||||
min=0,
|
||||
max=10,
|
||||
detail=(
|
||||
"The number of game master slots to generate for each side. "
|
||||
"Game masters can see, control & direct all units in the mission."
|
||||
),
|
||||
)
|
||||
tactical_commander_count: int = bounded_int_option(
|
||||
"Number of tactical commands",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=3,
|
||||
min=0,
|
||||
max=10,
|
||||
detail=(
|
||||
"The number of tactical commander slots to generate for each side. "
|
||||
"Tactical commanders can control & direct friendly units."
|
||||
),
|
||||
)
|
||||
jtac_count: int = bounded_int_option(
|
||||
"Number of JTAC controllers",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=3,
|
||||
min=0,
|
||||
max=10,
|
||||
detail=(
|
||||
"The number of JTAC controller slots to generate for each side. "
|
||||
"JTAC operators can only control friendly units."
|
||||
),
|
||||
)
|
||||
observer_count: int = bounded_int_option(
|
||||
"Number of observers",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=0,
|
||||
min=0,
|
||||
max=10,
|
||||
detail=(
|
||||
"The number of observers slots to generate for each side. "
|
||||
'Use this to allow spectators when disabling "Allow external views".'
|
||||
),
|
||||
)
|
||||
|
||||
# Performance
|
||||
perf_smoke_gen: bool = boolean_option(
|
||||
@@ -523,6 +649,7 @@ class Settings:
|
||||
show_red_ato: bool = False
|
||||
enable_frontline_cheats: bool = False
|
||||
enable_base_capture_cheat: bool = False
|
||||
enable_transfer_cheat: bool = False
|
||||
|
||||
# LUA Plugins system
|
||||
plugins: Dict[str, bool] = field(default_factory=dict)
|
||||
@@ -533,17 +660,17 @@ class Settings:
|
||||
def plugin_settings_key(identifier: str) -> str:
|
||||
return f"plugins.{identifier}"
|
||||
|
||||
def initialize_plugin_option(self, identifier: str, default_value: bool) -> None:
|
||||
def initialize_plugin_option(self, identifier: str, default_value: Any) -> None:
|
||||
try:
|
||||
self.plugin_option(identifier)
|
||||
except KeyError:
|
||||
self.set_plugin_option(identifier, default_value)
|
||||
|
||||
def plugin_option(self, identifier: str) -> bool:
|
||||
def plugin_option(self, identifier: str) -> Any:
|
||||
return self.plugins[self.plugin_settings_key(identifier)]
|
||||
|
||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
||||
def set_plugin_option(self, identifier: str, value: Any) -> None:
|
||||
self.plugins[self.plugin_settings_key(identifier)] = value
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
|
||||
@@ -235,7 +235,6 @@ class MissionResultsProcessor:
|
||||
delta = DEFEAT_INFLUENCE
|
||||
status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory."
|
||||
elif ally_casualties > enemy_casualties:
|
||||
|
||||
if (
|
||||
ally_units_alive > 2 * enemy_units_alive
|
||||
and player_aggresive
|
||||
|
||||
@@ -22,7 +22,7 @@ class SquadronDefLoader:
|
||||
def squadron_directories() -> Iterator[Path]:
|
||||
from game import persistency
|
||||
|
||||
yield Path(persistency.base_path()) / "Liberation/Squadrons"
|
||||
yield Path(persistency.base_path()) / "Retribution/Squadrons"
|
||||
yield Path("resources/squadrons")
|
||||
|
||||
def load(self) -> dict[AircraftType, list[SquadronDef]]:
|
||||
|
||||
@@ -22,6 +22,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
from uuid import UUID
|
||||
|
||||
@@ -40,6 +41,7 @@ from dcs.ships import (
|
||||
Hms_invincible,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||
from dcs.unittype import ShipType
|
||||
|
||||
@@ -61,6 +63,7 @@ from game.theater.presetlocation import PresetLocation
|
||||
from game.utils import Distance, Heading, meters
|
||||
from .base import Base
|
||||
from .frontline import FrontLine
|
||||
from .interfaces.CTLD import CTLD
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import (
|
||||
GenericCarrierGroundObject,
|
||||
@@ -73,6 +76,10 @@ from ..data.units import UnitClass
|
||||
from ..db import Database
|
||||
from ..dcs.aircrafttype import AircraftType
|
||||
from ..dcs.groundunittype import GroundUnitType
|
||||
from ..radio.ICLSContainer import ICLSContainer
|
||||
from ..radio.Link4Container import Link4Container
|
||||
from ..radio.RadioFrequencyContainer import RadioFrequencyContainer
|
||||
from ..radio.TacanContainer import TacanContainer
|
||||
from ..utils import nautical_miles
|
||||
from ..weather import Conditions
|
||||
|
||||
@@ -305,7 +312,7 @@ class ControlPointStatus(IntEnum):
|
||||
Destroyed = auto()
|
||||
|
||||
|
||||
StartingPosition = ShipGroup | StaticGroup | Airport | Point
|
||||
StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
|
||||
|
||||
|
||||
class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
@@ -870,6 +877,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
) -> RunwayData:
|
||||
...
|
||||
|
||||
def stub_runway_data(self) -> RunwayData:
|
||||
return RunwayData(
|
||||
self.full_name, runway_heading=Heading.from_degrees(0), runway_name=""
|
||||
)
|
||||
|
||||
@property
|
||||
def airdrome_id_for_landing(self) -> Optional[int]:
|
||||
return None
|
||||
@@ -1042,9 +1054,13 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
||||
...
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
class Airfield(ControlPoint, CTLD):
|
||||
def __init__(
|
||||
self, airport: Airport, theater: ConflictTheater, starts_blue: bool
|
||||
self,
|
||||
airport: Airport,
|
||||
theater: ConflictTheater,
|
||||
starts_blue: bool,
|
||||
ctld_zones: Optional[List[Tuple[Point, float]]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
airport.name,
|
||||
@@ -1056,6 +1072,7 @@ class Airfield(ControlPoint):
|
||||
)
|
||||
self.airport = airport
|
||||
self._runway_status = RunwayStatus()
|
||||
self.ctld_zones = ctld_zones
|
||||
|
||||
@property
|
||||
def dcs_airport(self) -> Airport:
|
||||
@@ -1128,6 +1145,14 @@ class Airfield(ControlPoint):
|
||||
conditions: Conditions,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> RunwayData:
|
||||
if not self.airport.runways:
|
||||
# Some airfields are heliports and don't have any runways. This isn't really
|
||||
# the best fix, since we should still try to generate partial data for TACAN
|
||||
# beacons, but it'll do for a bug fix, and the proper fix probably involves
|
||||
# making heliports their own CP type.
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/2710
|
||||
return self.stub_runway_data()
|
||||
|
||||
assigner = RunwayAssigner(conditions)
|
||||
return assigner.get_preferred_runway(theater, self.airport)
|
||||
|
||||
@@ -1161,7 +1186,9 @@ class Airfield(ControlPoint):
|
||||
return ControlPointStatus.Functional
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
class NavalControlPoint(
|
||||
ControlPoint, ABC, Link4Container, TacanContainer, ICLSContainer
|
||||
):
|
||||
@property
|
||||
def is_fleet(self) -> bool:
|
||||
return True
|
||||
@@ -1219,7 +1246,7 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
fallback = RunwayData(
|
||||
self.full_name, runway_heading=Heading.from_degrees(0), runway_name=""
|
||||
)
|
||||
return dynamic_runways.get(self.name, fallback)
|
||||
return dynamic_runways.get(self.full_name, fallback)
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
@@ -1264,7 +1291,7 @@ class Carrier(NavalControlPoint):
|
||||
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
from game.ato.flighttype import FlightType
|
||||
|
||||
yield from super().mission_types(for_player)
|
||||
if self.is_friendly(for_player):
|
||||
@@ -1371,9 +1398,7 @@ class OffMapSpawn(ControlPoint):
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> RunwayData:
|
||||
logging.warning("TODO: Off map spawns have no runways.")
|
||||
return RunwayData(
|
||||
self.full_name, runway_heading=Heading.from_degrees(0), runway_name=""
|
||||
)
|
||||
return self.stub_runway_data()
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
@@ -1392,14 +1417,20 @@ class OffMapSpawn(ControlPoint):
|
||||
return ControlPointStatus.Functional
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
|
||||
def __init__(
|
||||
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
|
||||
):
|
||||
self,
|
||||
name: str,
|
||||
at: Point,
|
||||
theater: ConflictTheater,
|
||||
starts_blue: bool,
|
||||
ctld_zones: Optional[List[Tuple[Point, float]]] = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name, at, at, theater, starts_blue, cptype=ControlPointType.FOB
|
||||
)
|
||||
self.name = name
|
||||
self.ctld_zones = ctld_zones
|
||||
|
||||
@property
|
||||
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
|
||||
@@ -1415,9 +1446,7 @@ class Fob(ControlPoint):
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> RunwayData:
|
||||
logging.warning("TODO: FOBs have no runways.")
|
||||
return RunwayData(
|
||||
self.full_name, runway_heading=Heading.from_degrees(0), runway_name=""
|
||||
)
|
||||
return self.stub_runway_data()
|
||||
|
||||
@property
|
||||
def runway_status(self) -> RunwayStatus:
|
||||
|
||||
@@ -12,7 +12,7 @@ from ..utils import Heading, pairwise
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import FlightType
|
||||
from .controlpoint import ControlPoint
|
||||
from .controlpoint import ControlPoint, Coalition
|
||||
|
||||
|
||||
FRONTLINE_MIN_CP_DISTANCE = 5000
|
||||
@@ -121,6 +121,10 @@ class FrontLine(MissionTarget):
|
||||
"""Returns a tuple of the two control points."""
|
||||
return self.blue_cp, self.red_cp
|
||||
|
||||
@property
|
||||
def coalition(self) -> Coalition:
|
||||
return self.blue_cp.coalition
|
||||
|
||||
@property
|
||||
def route_length(self) -> float:
|
||||
"""The total distance of all segments"""
|
||||
|
||||
7
game/theater/interfaces/CTLD.py
Normal file
7
game/theater/interfaces/CTLD.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from typing import Optional, List, Tuple
|
||||
|
||||
from dcs import Point
|
||||
|
||||
|
||||
class CTLD:
|
||||
ctld_zones: Optional[List[Tuple[Point, float]]] = None
|
||||
@@ -6,7 +6,7 @@ from dcs.mapping import Point
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.theater import TheaterUnit
|
||||
from game.theater import TheaterUnit, Coalition
|
||||
|
||||
|
||||
class MissionTarget:
|
||||
@@ -47,3 +47,7 @@ class MissionTarget:
|
||||
@property
|
||||
def strike_targets(self) -> list[TheaterUnit]:
|
||||
return []
|
||||
|
||||
@property
|
||||
def coalition(self) -> Coalition:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, time
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
import dcs.statics
|
||||
|
||||
@@ -30,6 +30,7 @@ from .theatergroup import IadsGroundGroup, IadsRole, SceneryUnit, TheaterGroup
|
||||
from ..armedforces.armedforces import ArmedForces
|
||||
from ..armedforces.forcegroup import ForceGroup
|
||||
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||
from ..campaignloader.campaigngroundconfig import TgoConfig
|
||||
from ..data.groups import GroupTask
|
||||
from ..profiling import logged_duration
|
||||
from ..settings import Settings
|
||||
@@ -47,15 +48,17 @@ class GeneratorSettings:
|
||||
no_lha: bool
|
||||
no_player_navy: bool
|
||||
no_enemy_navy: bool
|
||||
tgo_config: TgoConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModSettings:
|
||||
a4_skyhawk: bool = False
|
||||
a6a_intruder: bool = False
|
||||
f4b_phantom: bool = False
|
||||
f4bc_phantom: bool = False
|
||||
f15d_baz: bool = False
|
||||
f_16_idf: bool = False
|
||||
fa_18efg: bool = False
|
||||
f22_raptor: bool = False
|
||||
f84g_thunderjet: bool = False
|
||||
f100_supersabre: bool = False
|
||||
@@ -69,6 +72,7 @@ class ModSettings:
|
||||
frenchpack: bool = False
|
||||
high_digit_sams: bool = False
|
||||
ov10a_bronco: bool = False
|
||||
swedishmilitaryassetspack: bool = False
|
||||
|
||||
|
||||
class GameGenerator:
|
||||
@@ -295,9 +299,31 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.generate_missile_sites()
|
||||
self.generate_coastal_sites()
|
||||
|
||||
def get_unit_group_for_task(
|
||||
self, position: PresetLocation, task: GroupTask
|
||||
) -> Optional[ForceGroup]:
|
||||
tgo_config = self.generator_settings.tgo_config
|
||||
fg = tgo_config[position.original_name]
|
||||
valid_fg = (
|
||||
fg
|
||||
and task in fg.tasks
|
||||
and all([u in self.faction.accessible_units for u in fg.units])
|
||||
)
|
||||
if valid_fg:
|
||||
unit_group = fg
|
||||
assert fg
|
||||
self.armed_forces.add_or_update_force_group(fg)
|
||||
else:
|
||||
if fg and not valid_fg:
|
||||
logging.warning(
|
||||
f"Override in ground_forces failed for {fg} at {position.original_name}"
|
||||
)
|
||||
unit_group = self.armed_forces.random_group_for_task(task)
|
||||
return unit_group
|
||||
|
||||
def generate_armor_groups(self) -> None:
|
||||
for position in self.control_point.preset_locations.armor_groups:
|
||||
unit_group = self.armed_forces.random_group_for_task(GroupTask.BASE_DEFENSE)
|
||||
unit_group = self.get_unit_group_for_task(position, GroupTask.BASE_DEFENSE)
|
||||
if not unit_group:
|
||||
logging.error(f"{self.faction_name} has no ForceGroup for Armor")
|
||||
return
|
||||
@@ -351,7 +377,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
def generate_aa_at(self, location: PresetLocation, tasks: list[GroupTask]) -> None:
|
||||
for task in tasks:
|
||||
unit_group = self.armed_forces.random_group_for_task(task)
|
||||
unit_group = self.get_unit_group_for_task(location, task)
|
||||
if unit_group:
|
||||
# Only take next (smaller) aa_range when no template available for the
|
||||
# most requested range. Otherwise break the loop and continue
|
||||
|
||||
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
|
||||
from game.ato.flighttype import FlightType
|
||||
from game.threatzones import ThreatPoly
|
||||
from .theatergroup import TheaterUnit, TheaterGroup
|
||||
from .controlpoint import ControlPoint
|
||||
from .controlpoint import ControlPoint, Coalition
|
||||
|
||||
|
||||
NAME_BY_CATEGORY = {
|
||||
@@ -275,6 +275,10 @@ class TheaterGroundObject(MissionTarget, SidcDescribable, ABC):
|
||||
def is_iads(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def coalition(self) -> Coalition:
|
||||
return self.control_point.coalition
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
|
||||
@@ -195,7 +195,8 @@ class TheaterGroup:
|
||||
|
||||
def max_threat_range(self, radar_only: bool = False) -> Distance:
|
||||
"""Calculate the maximum threat range of the TheaterGroup.
|
||||
This also checks for Launcher and Tracker Pairs and if they are functioning or not. Allows to also use only radar emitting units for the calculation with the parameter."""
|
||||
This also checks for Launcher and Tracker Pairs and if they are functioning or not. Allows to also use only radar emitting units for the calculation with the parameter.
|
||||
"""
|
||||
max_non_radar = meters(0)
|
||||
max_telar_range = meters(0)
|
||||
max_tel_range = meters(0)
|
||||
|
||||
@@ -90,6 +90,10 @@ class TheaterLoader:
|
||||
def landmap_path(self) -> Path:
|
||||
return self.descriptor_path.with_name("landmap.p")
|
||||
|
||||
@property
|
||||
def icon_path(self) -> Path:
|
||||
return self.descriptor_path.with_name("icon.gif")
|
||||
|
||||
@property
|
||||
def menu_thumbnail_dcs_relative_path(self) -> Path:
|
||||
with self.descriptor_path.open() as descriptor_file:
|
||||
|
||||
@@ -59,6 +59,7 @@ from game.utils import meters, nautical_miles
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import Coalition
|
||||
|
||||
|
||||
class Transport:
|
||||
@@ -414,7 +415,8 @@ class MultiGroupTransport(MissionTarget, Transport):
|
||||
units: dict[GroundUnitType, int] = defaultdict(int)
|
||||
for transfer in self.transfers:
|
||||
for unit_type, count in transfer.units.items():
|
||||
units[unit_type] += count
|
||||
if count > 0:
|
||||
units[unit_type] += count
|
||||
return units
|
||||
|
||||
def iter_units(self) -> Iterator[GroundUnitType]:
|
||||
@@ -432,6 +434,10 @@ class MultiGroupTransport(MissionTarget, Transport):
|
||||
def description(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def coalition(self) -> Coalition:
|
||||
return self.origin.coalition
|
||||
|
||||
|
||||
class Convoy(MultiGroupTransport):
|
||||
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
|
||||
@@ -615,13 +621,20 @@ class PendingTransfers:
|
||||
raise ValueError
|
||||
|
||||
units = {}
|
||||
to_delete = []
|
||||
for unit_type, remaining in transfer.units.items():
|
||||
take = min(remaining, size)
|
||||
if not take:
|
||||
if not remaining:
|
||||
to_delete.append(unit_type)
|
||||
continue
|
||||
size -= take
|
||||
transfer.units[unit_type] -= take
|
||||
if not transfer.units[unit_type]:
|
||||
to_delete.append(unit_type)
|
||||
units[unit_type] = take
|
||||
if not size:
|
||||
break
|
||||
for td in to_delete:
|
||||
del transfer.units[td]
|
||||
new_transfer = TransferOrder(transfer.origin, transfer.destination, units)
|
||||
self.pending_transfers.append(new_transfer)
|
||||
return new_transfer
|
||||
@@ -717,7 +730,6 @@ class PendingTransfers:
|
||||
self.order_airlift_assets_at(control_point)
|
||||
|
||||
def desired_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||
|
||||
if control_point.has_factory:
|
||||
is_major_hub = control_point.total_aircraft_parking > 0
|
||||
# Check if there is a CP which is only reachable via Airlift
|
||||
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
MAJOR_VERSION = 1
|
||||
MINOR_VERSION = 0
|
||||
MINOR_VERSION = 1
|
||||
MICRO_VERSION = 0
|
||||
|
||||
|
||||
@@ -170,6 +170,13 @@ VERSION = _build_version_string()
|
||||
#:
|
||||
#: Version 10.5
|
||||
#: * Support for scenery objectives defined by quad zones.
|
||||
#: * Campaign designers can now define `max_frontline_length` (in km) setting:
|
||||
#: `max_frontline_length: 25`
|
||||
CAMPAIGN_FORMAT_VERSION = (10, 5)
|
||||
#: * Campaign designers can now define more settings:
|
||||
#: `max_frontline_length: 25` (in km)
|
||||
#: `culling_exclusion_radius: 35` (in km)
|
||||
#:
|
||||
#: Version 10.6
|
||||
#: * Designated CTLD zones for ControlPoints (Airbases & FOBs/FARPs)
|
||||
#: * 'ground_forces' in yaml file to specify preset groups for TGOs,
|
||||
#: given the group is available for the faction and the task matches
|
||||
|
||||
CAMPAIGN_FORMAT_VERSION = (10, 6)
|
||||
|
||||
@@ -299,7 +299,6 @@ class Weather:
|
||||
def interpolate_solar_activity(
|
||||
time_of_day: TimeOfDay, high: float, low: float
|
||||
) -> float:
|
||||
|
||||
scale: float = 0
|
||||
|
||||
match time_of_day:
|
||||
|
||||
Binary file not shown.
@@ -1,51 +0,0 @@
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
|
||||
|
||||
#define MyAppName "DCS Liberation"
|
||||
#define MyAppVersion "{{version}}"
|
||||
#define MyAppPublisher "Khopa"
|
||||
#define MyAppURL "https://github.com/dcs-liberation/dcs_liberation/wiki"
|
||||
#define MyAppExeName "liberation_main.exe"
|
||||
|
||||
[Setup]
|
||||
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||
AppId={{6753B352-D281-42CB-9AFA-5E93EB90AA5A}
|
||||
AppName={#MyAppName}
|
||||
AppVersion={#MyAppVersion}
|
||||
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||
AppPublisher={#MyAppPublisher}
|
||||
AppPublisherURL={#MyAppURL}
|
||||
AppSupportURL={#MyAppURL}
|
||||
AppUpdatesURL={#MyAppURL}
|
||||
DefaultDirName={autopf}\{#MyAppName}
|
||||
DefaultGroupName={#MyAppName}
|
||||
AllowNoIcons=yes
|
||||
; Remove the following line to run in administrative install mode (install for all users.)
|
||||
PrivilegesRequired=lowest
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
OutputDir=..\dist
|
||||
OutputBaseFilename=dcs_liberation
|
||||
SetupIconFile=..\resources\icon.ico
|
||||
UninstallDisplayIcon={app}\liberation_main.exe
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||
|
||||
[Files]
|
||||
Source: "..\dist\dcs_liberation\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
@@ -14,6 +14,7 @@ from .su30 import *
|
||||
from .su57 import *
|
||||
from .uh60l import *
|
||||
from .ov10a import *
|
||||
from .swedishmilitaryassetspack import *
|
||||
|
||||
|
||||
def load_mods() -> None:
|
||||
|
||||
@@ -12,7 +12,6 @@ from game.modsupport import planemod
|
||||
class VSN_A6A(PlaneType):
|
||||
id = "VSN_A6A"
|
||||
flyable = True
|
||||
group_size_max = 1
|
||||
height = 4.57
|
||||
width = 10.15
|
||||
length = 17.98
|
||||
|
||||
@@ -613,7 +613,10 @@ class VSN_F104S(PlaneType):
|
||||
AIM_9M_Sidewinder_IR_AAM = (3, Weapons.AIM_9M_Sidewinder_IR_AAM)
|
||||
AIM_9P_Sidewinder_IR_AAM = (3, Weapons.AIM_9P_Sidewinder_IR_AAM)
|
||||
AIM_9P5_Sidewinder_IR_AAM = (3, Weapons.AIM_9P5_Sidewinder_IR_AAM)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (3, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
3,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (3, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
|
||||
class Pylon4:
|
||||
@@ -659,7 +662,10 @@ class VSN_F104S(PlaneType):
|
||||
AIM_9M_Sidewinder_IR_AAM = (9, Weapons.AIM_9M_Sidewinder_IR_AAM)
|
||||
AIM_9P_Sidewinder_IR_AAM = (9, Weapons.AIM_9P_Sidewinder_IR_AAM)
|
||||
AIM_9P5_Sidewinder_IR_AAM = (9, Weapons.AIM_9P5_Sidewinder_IR_AAM)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (9, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
9,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (9, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
|
||||
class Pylon10:
|
||||
|
||||
@@ -106,7 +106,10 @@ class F_15D(PlaneType):
|
||||
4,
|
||||
Weapons.AIM_7MH_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (4, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
4,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
I_Derby_ER = (4, WeaponsF15D.I_Derby_ER)
|
||||
|
||||
class Pylon5:
|
||||
@@ -121,7 +124,10 @@ class F_15D(PlaneType):
|
||||
5,
|
||||
Weapons.AIM_7MH_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (5, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
5,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
I_Derby_ER = (5, WeaponsF15D.I_Derby_ER)
|
||||
ELL___8222SB = (5, WeaponsF15D.ELL___8222SB)
|
||||
|
||||
@@ -141,7 +147,10 @@ class F_15D(PlaneType):
|
||||
7,
|
||||
Weapons.AIM_7MH_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (7, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
7,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
I_Derby_ER = (7, WeaponsF15D.I_Derby_ER)
|
||||
|
||||
class Pylon8:
|
||||
@@ -156,7 +165,10 @@ class F_15D(PlaneType):
|
||||
8,
|
||||
Weapons.AIM_7MH_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (8, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
8,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
I_Derby_ER = (8, WeaponsF15D.I_Derby_ER)
|
||||
|
||||
class Pylon9:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,17 @@ from game.modsupport import planemod
|
||||
from pydcs_extensions.weapon_injector import inject_weapons
|
||||
|
||||
|
||||
class WeaponsF4B:
|
||||
class WeaponsF4BC:
|
||||
F4B_SUU_23_Gun_Pod = {
|
||||
"clsid": "{VSN_F4B_Gunpod}",
|
||||
"name": "F4B SUU-23 Gun Pod",
|
||||
"weight": 112.35,
|
||||
}
|
||||
F4B_Gunpod_w_SAPHEI_T = {
|
||||
"clsid": "{VSN_F4B_Equalizer}",
|
||||
"name": "F4B Gunpod w/SAPHEI-T",
|
||||
"weight": 244.46,
|
||||
}
|
||||
VSN_F4EC_PTB = {
|
||||
"clsid": "VSN_F4EC_PTB",
|
||||
"name": "Fuel tank 600 Gal",
|
||||
@@ -28,6 +33,11 @@ class WeaponsF4B:
|
||||
"name": "Fuel tank 370 Gal",
|
||||
"weight": 1240,
|
||||
}
|
||||
VSN_F4B_C2_PTB = {
|
||||
"clsid": "VSN_F4B_C2_PTB",
|
||||
"name": "Fuel tank Center 370 Gal",
|
||||
"weight": 1240,
|
||||
}
|
||||
LAU_105_2_AIM_9J = {
|
||||
"clsid": "{VSN_F4B_LAU105_AIM9J}",
|
||||
"name": "LAU-105 2*AIM-9J",
|
||||
@@ -40,7 +50,7 @@ class WeaponsF4B:
|
||||
}
|
||||
|
||||
|
||||
inject_weapons(WeaponsF4B)
|
||||
inject_weapons(WeaponsF4BC)
|
||||
|
||||
|
||||
@planemod
|
||||
@@ -98,10 +108,6 @@ class VSN_F4B(PlaneType):
|
||||
2,
|
||||
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
|
||||
)
|
||||
LAU_118a_with_AGM_45B_Shrike_ARM__Imp_ = (
|
||||
2,
|
||||
Weapons.LAU_118a_with_AGM_45B_Shrike_ARM__Imp_,
|
||||
)
|
||||
Smokewinder___red = (2, Weapons.Smokewinder___red)
|
||||
Smokewinder___green = (2, Weapons.Smokewinder___green)
|
||||
Smokewinder___blue = (2, Weapons.Smokewinder___blue)
|
||||
@@ -123,7 +129,8 @@ class VSN_F4B(PlaneType):
|
||||
2,
|
||||
Weapons.BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb,
|
||||
)
|
||||
VSN_F4EL_PTB = (2, WeaponsF4B.VSN_F4EL_PTB)
|
||||
BIN_200 = (2, Weapons.BIN_200)
|
||||
VSN_F4EL_PTB = (2, WeaponsF4BC.VSN_F4EL_PTB)
|
||||
|
||||
class Pylon3:
|
||||
GBU_10___2000lb_Laser_Guided_Bomb = (
|
||||
@@ -140,18 +147,6 @@ class VSN_F4B(PlaneType):
|
||||
Weapons.BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD,
|
||||
)
|
||||
Mk_84___2000lb_GP_Bomb_LD = (3, Weapons.Mk_84___2000lb_GP_Bomb_LD)
|
||||
LAU_88_with_2_x_AGM_65K___Maverick_K__CCD_Imp_ASM_ = (
|
||||
3,
|
||||
Weapons.LAU_88_with_2_x_AGM_65K___Maverick_K__CCD_Imp_ASM_,
|
||||
)
|
||||
LAU_88_with_2_x_AGM_65D___Maverick_D__IIR_ASM_ = (
|
||||
3,
|
||||
Weapons.LAU_88_with_2_x_AGM_65D___Maverick_D__IIR_ASM_,
|
||||
)
|
||||
LAU_118a_with_AGM_45B_Shrike_ARM__Imp_ = (
|
||||
3,
|
||||
Weapons.LAU_118a_with_AGM_45B_Shrike_ARM__Imp_,
|
||||
)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
3,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
@@ -168,18 +163,6 @@ class VSN_F4B(PlaneType):
|
||||
3,
|
||||
Weapons.BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
LAU_88_with_2_x_AGM_65D___Maverick_D__IIR_ASM_ = (
|
||||
3,
|
||||
Weapons.LAU_88_with_2_x_AGM_65D___Maverick_D__IIR_ASM_,
|
||||
)
|
||||
LAU_88_with_3_x_AGM_65D___Maverick_D__IIR_ASM_ = (
|
||||
3,
|
||||
Weapons.LAU_88_with_3_x_AGM_65D___Maverick_D__IIR_ASM_,
|
||||
)
|
||||
LAU_105_with_2_x_AIM_9M_Sidewinder_IR_AAM = (
|
||||
3,
|
||||
Weapons.LAU_105_with_2_x_AIM_9M_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_105_2_AIM_9L = (3, Weapons.LAU_105_2_AIM_9L)
|
||||
LAU_105_with_2_x_AIM_9P_Sidewinder_IR_AAM = (
|
||||
3,
|
||||
@@ -190,11 +173,13 @@ class VSN_F4B(PlaneType):
|
||||
3,
|
||||
Weapons.LAU_7_with_2_x_AIM_9B_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_105_2_AIM_9J = (3, WeaponsF4B.LAU_105_2_AIM_9J)
|
||||
LAU_105_2_AIM_9JULI = (3, WeaponsF4B.LAU_105_2_AIM_9JULI)
|
||||
AIM_120B_AMRAAM___Active_Rdr_AAM = (3, Weapons.AIM_120B_AMRAAM___Active_Rdr_AAM)
|
||||
LAU_105_2_AIM_9J = (3, WeaponsF4BC.LAU_105_2_AIM_9J)
|
||||
LAU_105_2_AIM_9JULI = (3, WeaponsF4BC.LAU_105_2_AIM_9JULI)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (3, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (3, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
3,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
Mk_20_Rockeye___490lbs_CBU__247_x_HEAT_Bomblets = (
|
||||
3,
|
||||
Weapons.Mk_20_Rockeye___490lbs_CBU__247_x_HEAT_Bomblets,
|
||||
@@ -208,18 +193,21 @@ class VSN_F4B(PlaneType):
|
||||
3,
|
||||
Weapons.BRU_33_with_2_x_Mk_83___1000lb_GP_Bomb_LD,
|
||||
)
|
||||
BIN_200 = (3, Weapons.BIN_200)
|
||||
|
||||
class Pylon4:
|
||||
AIM_7M_Sparrow_Semi_Active_Radar = (4, Weapons.AIM_7M_Sparrow_Semi_Active_Radar)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (4, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (4, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_120B_AMRAAM___Active_Rdr_AAM = (4, Weapons.AIM_120B_AMRAAM___Active_Rdr_AAM)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
4,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
|
||||
class Pylon5:
|
||||
AIM_7M_Sparrow_Semi_Active_Radar = (5, Weapons.AIM_7M_Sparrow_Semi_Active_Radar)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (5, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (5, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_120B_AMRAAM___Active_Rdr_AAM = (5, Weapons.AIM_120B_AMRAAM___Active_Rdr_AAM)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
5,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
|
||||
class Pylon6:
|
||||
Smokewinder___red = (6, Weapons.Smokewinder___red)
|
||||
@@ -240,20 +228,23 @@ class VSN_F4B(PlaneType):
|
||||
Weapons.BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD,
|
||||
)
|
||||
ALQ_131___ECM_Pod = (6, Weapons.ALQ_131___ECM_Pod)
|
||||
F4B_SUU_23_Gun_Pod = (6, WeaponsF4B.F4B_SUU_23_Gun_Pod)
|
||||
VSN_F4EC_PTB = (6, WeaponsF4B.VSN_F4EC_PTB)
|
||||
F4B_Gunpod_w_SAPHEI_T = (6, WeaponsF4BC.F4B_Gunpod_w_SAPHEI_T)
|
||||
VSN_F4EC_PTB = (6, WeaponsF4BC.VSN_F4EC_PTB)
|
||||
VSN_F4B_C2_PTB = (6, WeaponsF4BC.VSN_F4B_C2_PTB)
|
||||
|
||||
class Pylon7:
|
||||
AIM_7M_Sparrow_Semi_Active_Radar = (7, Weapons.AIM_7M_Sparrow_Semi_Active_Radar)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (7, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (7, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_120B_AMRAAM___Active_Rdr_AAM = (7, Weapons.AIM_120B_AMRAAM___Active_Rdr_AAM)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
7,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
|
||||
class Pylon8:
|
||||
AIM_7M_Sparrow_Semi_Active_Radar = (8, Weapons.AIM_7M_Sparrow_Semi_Active_Radar)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (8, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (8, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_120B_AMRAAM___Active_Rdr_AAM = (8, Weapons.AIM_120B_AMRAAM___Active_Rdr_AAM)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
8,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
|
||||
class Pylon9:
|
||||
GBU_10___2000lb_Laser_Guided_Bomb = (
|
||||
@@ -270,18 +261,6 @@ class VSN_F4B(PlaneType):
|
||||
Weapons.BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD,
|
||||
)
|
||||
Mk_84___2000lb_GP_Bomb_LD = (9, Weapons.Mk_84___2000lb_GP_Bomb_LD)
|
||||
LAU_88_with_2_x_AGM_65K___Maverick_K__CCD_Imp_ASM_ = (
|
||||
9,
|
||||
Weapons.LAU_88_with_2_x_AGM_65K___Maverick_K__CCD_Imp_ASM_,
|
||||
)
|
||||
LAU_88_with_2_x_AGM_65D___Maverick_D__IIR_ASM_ = (
|
||||
9,
|
||||
Weapons.LAU_88_with_2_x_AGM_65D___Maverick_D__IIR_ASM_,
|
||||
)
|
||||
LAU_118a_with_AGM_45B_Shrike_ARM__Imp_ = (
|
||||
9,
|
||||
Weapons.LAU_118a_with_AGM_45B_Shrike_ARM__Imp_,
|
||||
)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
9,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
@@ -298,18 +277,6 @@ class VSN_F4B(PlaneType):
|
||||
9,
|
||||
Weapons.BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
LAU_88_with_2_x_AGM_65D___Maverick_D__IIR_ASM_ = (
|
||||
9,
|
||||
Weapons.LAU_88_with_2_x_AGM_65D___Maverick_D__IIR_ASM_,
|
||||
)
|
||||
LAU_88_with_3_x_AGM_65D___Maverick_D__IIR_ASM_ = (
|
||||
9,
|
||||
Weapons.LAU_88_with_3_x_AGM_65D___Maverick_D__IIR_ASM_,
|
||||
)
|
||||
LAU_105_with_2_x_AIM_9M_Sidewinder_IR_AAM = (
|
||||
9,
|
||||
Weapons.LAU_105_with_2_x_AIM_9M_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_105_2_AIM_9L = (9, Weapons.LAU_105_2_AIM_9L)
|
||||
LAU_105_with_2_x_AIM_9P_Sidewinder_IR_AAM = (
|
||||
9,
|
||||
@@ -320,11 +287,13 @@ class VSN_F4B(PlaneType):
|
||||
9,
|
||||
Weapons.LAU_7_with_2_x_AIM_9B_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_105_2_AIM_9J = (9, WeaponsF4B.LAU_105_2_AIM_9J)
|
||||
LAU_105_2_AIM_9JULI = (9, WeaponsF4B.LAU_105_2_AIM_9JULI)
|
||||
AIM_120B_AMRAAM___Active_Rdr_AAM = (9, Weapons.AIM_120B_AMRAAM___Active_Rdr_AAM)
|
||||
LAU_105_2_AIM_9J = (9, WeaponsF4BC.LAU_105_2_AIM_9J)
|
||||
LAU_105_2_AIM_9JULI = (9, WeaponsF4BC.LAU_105_2_AIM_9JULI)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (9, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_Sparrow_Semi_Active_Radar = (9, Weapons.AIM_7E_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
9,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
Mk_20_Rockeye___490lbs_CBU__247_x_HEAT_Bomblets = (
|
||||
9,
|
||||
Weapons.Mk_20_Rockeye___490lbs_CBU__247_x_HEAT_Bomblets,
|
||||
@@ -338,6 +307,7 @@ class VSN_F4B(PlaneType):
|
||||
9,
|
||||
Weapons.BRU_33_with_2_x_Mk_83___1000lb_GP_Bomb_LD,
|
||||
)
|
||||
BIN_200 = (9, Weapons.BIN_200)
|
||||
|
||||
class Pylon10:
|
||||
GBU_10___2000lb_Laser_Guided_Bomb = (
|
||||
@@ -369,9 +339,331 @@ class VSN_F4B(PlaneType):
|
||||
10,
|
||||
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
|
||||
)
|
||||
LAU_118a_with_AGM_45B_Shrike_ARM__Imp_ = (
|
||||
Smokewinder___red = (10, Weapons.Smokewinder___red)
|
||||
Smokewinder___green = (10, Weapons.Smokewinder___green)
|
||||
Smokewinder___blue = (10, Weapons.Smokewinder___blue)
|
||||
Smokewinder___white = (10, Weapons.Smokewinder___white)
|
||||
Smokewinder___yellow = (10, Weapons.Smokewinder___yellow)
|
||||
BRU_33_with_2_x_LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
10,
|
||||
Weapons.LAU_118a_with_AGM_45B_Shrike_ARM__Imp_,
|
||||
Weapons.BRU_33_with_2_x_LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
10,
|
||||
Weapons.BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
BRU_41A_with_6_x_Mk_82___500lb_GP_Bomb_LD = (
|
||||
10,
|
||||
Weapons.BRU_41A_with_6_x_Mk_82___500lb_GP_Bomb_LD,
|
||||
)
|
||||
BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb = (
|
||||
10,
|
||||
Weapons.BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb,
|
||||
)
|
||||
BIN_200 = (10, Weapons.BIN_200)
|
||||
VSN_F4ER_PTB = (10, WeaponsF4BC.VSN_F4ER_PTB)
|
||||
|
||||
pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
|
||||
tasks = [
|
||||
task.CAP,
|
||||
task.Escort,
|
||||
task.FighterSweep,
|
||||
task.Intercept,
|
||||
task.Reconnaissance,
|
||||
task.GroundAttack,
|
||||
task.CAS,
|
||||
task.AFAC,
|
||||
task.RunwayAttack,
|
||||
]
|
||||
task_default = task.CAP
|
||||
|
||||
|
||||
@planemod
|
||||
class VSN_F4C(PlaneType):
|
||||
id = "VSN_F4C"
|
||||
flyable = True
|
||||
height = 5.02
|
||||
width = 11.71
|
||||
length = 19.2
|
||||
fuel_max = 6416
|
||||
max_speed = 2545.2
|
||||
chaff = 48
|
||||
flare = 48
|
||||
charge_total = 96
|
||||
chaff_charge_size = 1
|
||||
flare_charge_size = 1
|
||||
category = "Interceptor" # {78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
|
||||
radio_frequency = 127.5
|
||||
|
||||
livery_name = "VSN_F4C" # from type
|
||||
Liveries = Liveries()[livery_name]
|
||||
|
||||
class Pylon1:
|
||||
Smoke_Generator___red_ = (1, Weapons.Smoke_Generator___red_)
|
||||
Smoke_Generator___green_ = (1, Weapons.Smoke_Generator___green_)
|
||||
Smoke_Generator___blue_ = (1, Weapons.Smoke_Generator___blue_)
|
||||
Smoke_Generator___white_ = (1, Weapons.Smoke_Generator___white_)
|
||||
Smoke_Generator___yellow_ = (1, Weapons.Smoke_Generator___yellow_)
|
||||
Smoke_Generator___orange_ = (1, Weapons.Smoke_Generator___orange_)
|
||||
|
||||
class Pylon2:
|
||||
GBU_10___2000lb_Laser_Guided_Bomb = (
|
||||
2,
|
||||
Weapons.GBU_10___2000lb_Laser_Guided_Bomb,
|
||||
)
|
||||
GBU_12___500lb_Laser_Guided_Bomb = (2, Weapons.GBU_12___500lb_Laser_Guided_Bomb)
|
||||
BRU_42_with_3_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets = (
|
||||
2,
|
||||
Weapons.BRU_42_with_3_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets,
|
||||
)
|
||||
Mk_84___2000lb_GP_Bomb_LD = (2, Weapons.Mk_84___2000lb_GP_Bomb_LD)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
2,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
2,
|
||||
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
_3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
2,
|
||||
Weapons._3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
|
||||
2,
|
||||
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
|
||||
)
|
||||
Smokewinder___red = (2, Weapons.Smokewinder___red)
|
||||
Smokewinder___green = (2, Weapons.Smokewinder___green)
|
||||
Smokewinder___blue = (2, Weapons.Smokewinder___blue)
|
||||
Smokewinder___white = (2, Weapons.Smokewinder___white)
|
||||
Smokewinder___yellow = (2, Weapons.Smokewinder___yellow)
|
||||
BRU_33_with_2_x_LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
2,
|
||||
Weapons.BRU_33_with_2_x_LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
2,
|
||||
Weapons.BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
BRU_41A_with_6_x_Mk_82___500lb_GP_Bomb_LD = (
|
||||
2,
|
||||
Weapons.BRU_41A_with_6_x_Mk_82___500lb_GP_Bomb_LD,
|
||||
)
|
||||
BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb = (
|
||||
2,
|
||||
Weapons.BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb,
|
||||
)
|
||||
BIN_200 = (2, Weapons.BIN_200)
|
||||
VSN_F4EL_PTB = (2, WeaponsF4BC.VSN_F4EL_PTB)
|
||||
|
||||
class Pylon3:
|
||||
GBU_10___2000lb_Laser_Guided_Bomb = (
|
||||
3,
|
||||
Weapons.GBU_10___2000lb_Laser_Guided_Bomb,
|
||||
)
|
||||
GBU_12___500lb_Laser_Guided_Bomb = (3, Weapons.GBU_12___500lb_Laser_Guided_Bomb)
|
||||
BRU_42_with_3_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets = (
|
||||
3,
|
||||
Weapons.BRU_42_with_3_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets,
|
||||
)
|
||||
BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD = (
|
||||
3,
|
||||
Weapons.BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD,
|
||||
)
|
||||
Mk_84___2000lb_GP_Bomb_LD = (3, Weapons.Mk_84___2000lb_GP_Bomb_LD)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
3,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
_3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
3,
|
||||
Weapons._3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
BRU_33_with_2_x_LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
3,
|
||||
Weapons.BRU_33_with_2_x_LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
3,
|
||||
Weapons.BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
LAU_105_2_AIM_9L = (3, Weapons.LAU_105_2_AIM_9L)
|
||||
LAU_105_with_2_x_AIM_9P_Sidewinder_IR_AAM = (
|
||||
3,
|
||||
Weapons.LAU_105_with_2_x_AIM_9P_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_105_2_AIM_9P5 = (3, Weapons.LAU_105_2_AIM_9P5)
|
||||
LAU_7_with_2_x_AIM_9B_Sidewinder_IR_AAM = (
|
||||
3,
|
||||
Weapons.LAU_7_with_2_x_AIM_9B_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_105_2_AIM_9J = (3, WeaponsF4BC.LAU_105_2_AIM_9J)
|
||||
LAU_105_2_AIM_9JULI = (3, WeaponsF4BC.LAU_105_2_AIM_9JULI)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (3, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
3,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
Mk_20_Rockeye___490lbs_CBU__247_x_HEAT_Bomblets = (
|
||||
3,
|
||||
Weapons.Mk_20_Rockeye___490lbs_CBU__247_x_HEAT_Bomblets,
|
||||
)
|
||||
Mk_82___500lb_GP_Bomb_LD = (3, Weapons.Mk_82___500lb_GP_Bomb_LD)
|
||||
TER_9A_with_3_x_Mk_82_Snakeye___500lb_GP_Bomb_HD = (
|
||||
3,
|
||||
Weapons.TER_9A_with_3_x_Mk_82_Snakeye___500lb_GP_Bomb_HD,
|
||||
)
|
||||
BRU_33_with_2_x_Mk_83___1000lb_GP_Bomb_LD = (
|
||||
3,
|
||||
Weapons.BRU_33_with_2_x_Mk_83___1000lb_GP_Bomb_LD,
|
||||
)
|
||||
BIN_200 = (3, Weapons.BIN_200)
|
||||
|
||||
class Pylon4:
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (4, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
4,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
|
||||
class Pylon5:
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (5, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
5,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
|
||||
class Pylon6:
|
||||
Smokewinder___red = (6, Weapons.Smokewinder___red)
|
||||
Smokewinder___green = (6, Weapons.Smokewinder___green)
|
||||
Smokewinder___blue = (6, Weapons.Smokewinder___blue)
|
||||
Smokewinder___white = (6, Weapons.Smokewinder___white)
|
||||
Smokewinder___yellow = (6, Weapons.Smokewinder___yellow)
|
||||
BRU_41A_with_6_x_Mk_82___500lb_GP_Bomb_LD = (
|
||||
6,
|
||||
Weapons.BRU_41A_with_6_x_Mk_82___500lb_GP_Bomb_LD,
|
||||
)
|
||||
_3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
6,
|
||||
Weapons._3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD = (
|
||||
6,
|
||||
Weapons.BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD,
|
||||
)
|
||||
ALQ_131___ECM_Pod = (6, Weapons.ALQ_131___ECM_Pod)
|
||||
F4B_Gunpod_w_SAPHEI_T = (6, WeaponsF4BC.F4B_Gunpod_w_SAPHEI_T)
|
||||
VSN_F4EC_PTB = (6, WeaponsF4BC.VSN_F4EC_PTB)
|
||||
VSN_F4ER_PTB = (6, WeaponsF4BC.VSN_F4ER_PTB)
|
||||
|
||||
class Pylon7:
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (7, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
7,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
|
||||
class Pylon8:
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (8, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
8,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
|
||||
class Pylon9:
|
||||
GBU_10___2000lb_Laser_Guided_Bomb = (
|
||||
9,
|
||||
Weapons.GBU_10___2000lb_Laser_Guided_Bomb,
|
||||
)
|
||||
GBU_12___500lb_Laser_Guided_Bomb = (9, Weapons.GBU_12___500lb_Laser_Guided_Bomb)
|
||||
BRU_42_with_3_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets = (
|
||||
9,
|
||||
Weapons.BRU_42_with_3_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets,
|
||||
)
|
||||
BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD = (
|
||||
9,
|
||||
Weapons.BRU_42_with_3_x_Mk_82___500lb_GP_Bombs_LD,
|
||||
)
|
||||
Mk_84___2000lb_GP_Bomb_LD = (9, Weapons.Mk_84___2000lb_GP_Bomb_LD)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
9,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
_3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
9,
|
||||
Weapons._3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
BRU_33_with_2_x_LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
9,
|
||||
Weapons.BRU_33_with_2_x_LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
9,
|
||||
Weapons.BRU_33_with_2_x_LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
LAU_105_2_AIM_9L = (9, Weapons.LAU_105_2_AIM_9L)
|
||||
LAU_105_with_2_x_AIM_9P_Sidewinder_IR_AAM = (
|
||||
9,
|
||||
Weapons.LAU_105_with_2_x_AIM_9P_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_105_2_AIM_9P5 = (9, Weapons.LAU_105_2_AIM_9P5)
|
||||
LAU_7_with_2_x_AIM_9B_Sidewinder_IR_AAM = (
|
||||
9,
|
||||
Weapons.LAU_7_with_2_x_AIM_9B_Sidewinder_IR_AAM,
|
||||
)
|
||||
LAU_105_2_AIM_9J = (9, WeaponsF4BC.LAU_105_2_AIM_9J)
|
||||
LAU_105_2_AIM_9JULI = (9, WeaponsF4BC.LAU_105_2_AIM_9JULI)
|
||||
AIM_7F_Sparrow_Semi_Active_Radar = (9, Weapons.AIM_7F_Sparrow_Semi_Active_Radar)
|
||||
AIM_7E_2_Sparrow_Semi_Active_Radar = (
|
||||
9,
|
||||
Weapons.AIM_7E_2_Sparrow_Semi_Active_Radar,
|
||||
)
|
||||
Mk_20_Rockeye___490lbs_CBU__247_x_HEAT_Bomblets = (
|
||||
9,
|
||||
Weapons.Mk_20_Rockeye___490lbs_CBU__247_x_HEAT_Bomblets,
|
||||
)
|
||||
Mk_82___500lb_GP_Bomb_LD = (9, Weapons.Mk_82___500lb_GP_Bomb_LD)
|
||||
TER_9A_with_3_x_Mk_82_Snakeye___500lb_GP_Bomb_HD = (
|
||||
9,
|
||||
Weapons.TER_9A_with_3_x_Mk_82_Snakeye___500lb_GP_Bomb_HD,
|
||||
)
|
||||
BRU_33_with_2_x_Mk_83___1000lb_GP_Bomb_LD = (
|
||||
9,
|
||||
Weapons.BRU_33_with_2_x_Mk_83___1000lb_GP_Bomb_LD,
|
||||
)
|
||||
BIN_200 = (9, Weapons.BIN_200)
|
||||
|
||||
class Pylon10:
|
||||
GBU_10___2000lb_Laser_Guided_Bomb = (
|
||||
10,
|
||||
Weapons.GBU_10___2000lb_Laser_Guided_Bomb,
|
||||
)
|
||||
GBU_12___500lb_Laser_Guided_Bomb = (
|
||||
10,
|
||||
Weapons.GBU_12___500lb_Laser_Guided_Bomb,
|
||||
)
|
||||
BRU_42_with_3_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets = (
|
||||
10,
|
||||
Weapons.BRU_42_with_3_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets,
|
||||
)
|
||||
Mk_84___2000lb_GP_Bomb_LD = (10, Weapons.Mk_84___2000lb_GP_Bomb_LD)
|
||||
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
|
||||
10,
|
||||
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
|
||||
)
|
||||
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
10,
|
||||
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
_3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
|
||||
10,
|
||||
Weapons._3_x_LAU_61_pods___57_x_2_75_Hydra__UnGd_Rkts_M151__HE,
|
||||
)
|
||||
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
|
||||
10,
|
||||
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
|
||||
)
|
||||
Smokewinder___red = (10, Weapons.Smokewinder___red)
|
||||
Smokewinder___green = (10, Weapons.Smokewinder___green)
|
||||
@@ -394,13 +686,10 @@ class VSN_F4B(PlaneType):
|
||||
10,
|
||||
Weapons.BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb,
|
||||
)
|
||||
VSN_F4ER_PTB = (10, WeaponsF4B.VSN_F4ER_PTB)
|
||||
BIN_200 = (10, Weapons.BIN_200)
|
||||
VSN_F4ER_PTB = (10, WeaponsF4BC.VSN_F4ER_PTB)
|
||||
|
||||
class Pylon11:
|
||||
L005_Sorbtsiya_ECM_pod__left_ = (11, Weapons.L005_Sorbtsiya_ECM_pod__left_)
|
||||
L_081_Fantasmagoria_ELINT_pod = (11, Weapons.L_081_Fantasmagoria_ELINT_pod)
|
||||
|
||||
pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
|
||||
pylons = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
|
||||
tasks = [
|
||||
task.CAP,
|
||||
|
||||
2235
pydcs_extensions/fa18efg/fa18efg.py
Normal file
2235
pydcs_extensions/fa18efg/fa18efg.py
Normal file
File diff suppressed because it is too large
Load Diff
1
pydcs_extensions/swedishmilitaryassetspack/__init__.py
Normal file
1
pydcs_extensions/swedishmilitaryassetspack/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .swedishmilitaryassetspack import *
|
||||
@@ -0,0 +1,418 @@
|
||||
# Requires Swedish Military Assets for DCS by Currenthill:
|
||||
# https://forum.dcs.world/topic/295202-swedish-military-assets-for-dcs-by-currenthill/
|
||||
#
|
||||
|
||||
from typing import Set
|
||||
|
||||
from dcs import unittype, task
|
||||
from dcs.helicopters import HelicopterType
|
||||
from dcs.liveries_scanner import Liveries
|
||||
|
||||
from game.modsupport import vehiclemod, shipmod, helicoptermod
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class BV410_RBS70(unittype.VehicleType):
|
||||
id = "BV410_RBS70"
|
||||
name = "[SMA] RBS 70 EldE 70 Mobile SAM LN"
|
||||
detection_range = 0
|
||||
threat_range = 8000
|
||||
air_weapon_dist = 8000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class BV410_RBS90(unittype.VehicleType):
|
||||
id = "BV410_RBS90"
|
||||
name = "[SMA] RBS 90 EldE 90 Mobile SAM LN"
|
||||
detection_range = 20000
|
||||
threat_range = 8000
|
||||
air_weapon_dist = 8000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvS_103_Lavett103_Rb103A(unittype.VehicleType):
|
||||
id = "LvS-103_Lavett103_Rb103A"
|
||||
name = "[SMA] LvS-103 Lavett 103 Rb103A Stationary SAM LN"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvS_103_Lavett103_Rb103B(unittype.VehicleType):
|
||||
id = "LvS-103_Lavett103_Rb103B"
|
||||
name = "[SMA] LvS-103 Lavett 103 Rb103B Stationary SAM LN"
|
||||
detection_range = 0
|
||||
threat_range = 35000
|
||||
air_weapon_dist = 35000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvS_103_Lavett103_HX_Rb103A(unittype.VehicleType):
|
||||
id = "LvS-103_Lavett103_HX_Rb103A"
|
||||
name = "[SMA] LvS-103 Lavett 103 Rb103A Mobile SAM LN"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvS_103_Lavett103_HX_Rb103B(unittype.VehicleType):
|
||||
id = "LvS-103_Lavett103_HX_Rb103B"
|
||||
name = "[SMA] LvS-103 Lavett 103 Rb103B Mobile SAM LN"
|
||||
detection_range = 0
|
||||
threat_range = 150000
|
||||
air_weapon_dist = 150000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvS_103_StriE103(unittype.VehicleType):
|
||||
id = "LvS-103_StriE103"
|
||||
name = "[SMA] LvS-103 StriE 103 Mobile SAM ECS"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvS_103_PM103(unittype.VehicleType):
|
||||
id = "LvS-103_PM103"
|
||||
name = "[SMA] LvS-103 PM 103 Stationary SAM STR"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvS_103_PM103_HX(unittype.VehicleType):
|
||||
id = "LvS-103_PM103_HX"
|
||||
name = "[SMA] LvS-103 PM 103 Mobile SAM STR"
|
||||
detection_range = 160000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvS_103_Elverk103(unittype.VehicleType):
|
||||
id = "LvS-103_Elverk103"
|
||||
name = "[SMA] LvS-103 Elverk 103 Mobile SAM EPP"
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class LvKv9040(unittype.VehicleType):
|
||||
id = "LvKv9040"
|
||||
name = "[SMA] Lvkv 9040 SPAAG"
|
||||
detection_range = 15000
|
||||
threat_range = 4000
|
||||
air_weapon_dist = 4000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class RBS_70(unittype.VehicleType):
|
||||
id = "RBS-70"
|
||||
name = "[SMA] RBS 70 EldE 70 Stationary SAM LN"
|
||||
detection_range = 0
|
||||
threat_range = 8000
|
||||
air_weapon_dist = 8000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class RBS_90(unittype.VehicleType):
|
||||
id = "RBS-90"
|
||||
name = "[SMA] RBS 90 EldE 90 Stationary SAM LN"
|
||||
detection_range = 20000
|
||||
threat_range = 8000
|
||||
air_weapon_dist = 8000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class RBS_98(unittype.VehicleType):
|
||||
id = "RBS-98"
|
||||
name = "[SMA] RBS 98 EldE 98 Mobile SAM LN"
|
||||
detection_range = 0
|
||||
threat_range = 20000
|
||||
air_weapon_dist = 20000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class UndE23(unittype.VehicleType):
|
||||
id = "UndE23"
|
||||
name = "[SMA] UndE 23 (RBS 70/90/98) SAM STR"
|
||||
detection_range = 100000
|
||||
threat_range = 0
|
||||
air_weapon_dist = 0
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class BV410(unittype.VehicleType):
|
||||
id = "BV410"
|
||||
name = "[SMA] Bv 410 ATV"
|
||||
detection_range = 0
|
||||
threat_range = 1800
|
||||
air_weapon_dist = 1800
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class CV9040(unittype.VehicleType):
|
||||
id = "CV9040"
|
||||
name = "[SMA] Strf 9040 IFV"
|
||||
detection_range = 0
|
||||
threat_range = 4000
|
||||
air_weapon_dist = 4000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class Strv103(unittype.VehicleType):
|
||||
id = "Strv103"
|
||||
name = "[SMA] Strv 103 MBT"
|
||||
detection_range = 6000
|
||||
threat_range = 6000
|
||||
air_weapon_dist = 6000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class Strv121(unittype.VehicleType):
|
||||
id = "Strv121"
|
||||
name = "[SMA] Strv 121 MBT"
|
||||
detection_range = 0
|
||||
threat_range = 8000
|
||||
air_weapon_dist = 1200
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class Strv122(unittype.VehicleType):
|
||||
id = "Strv122"
|
||||
name = "[SMA] Strv 122 MBT"
|
||||
detection_range = 0
|
||||
threat_range = 8000
|
||||
air_weapon_dist = 1200
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class Strv2000(unittype.VehicleType):
|
||||
id = "Strv2000"
|
||||
name = "[SMA] Strv 2000 T140/40 MBT"
|
||||
detection_range = 8000
|
||||
threat_range = 8000
|
||||
air_weapon_dist = 4000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class Volvo740(unittype.VehicleType):
|
||||
id = "Volvo740"
|
||||
name = "[SMA] Volvo 740 Improvised fighting vehicle"
|
||||
detection_range = 0
|
||||
threat_range = 1200
|
||||
air_weapon_dist = 1200
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class RBS_15KA(unittype.VehicleType):
|
||||
id = "RBS-15KA"
|
||||
name = "[SMA] RBS 15KA LBASM"
|
||||
detection_range = 300000
|
||||
threat_range = 300000
|
||||
air_weapon_dist = 300000
|
||||
eplrs = True
|
||||
|
||||
|
||||
## INFANTRY
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class AG_90(unittype.VehicleType):
|
||||
id = "AG-90"
|
||||
name = "[SMA] Ag 90 Sniper team"
|
||||
detection_range = 5000
|
||||
threat_range = 1800
|
||||
air_weapon_dist = 1800
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class SwedishinfantryAK4(unittype.VehicleType):
|
||||
id = "SwedishinfantryAK4"
|
||||
name = "[SMA] Ak 4 Soldier"
|
||||
detection_range = 1500
|
||||
threat_range = 600
|
||||
air_weapon_dist = 600
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class SwedishinfantryAK5(unittype.VehicleType):
|
||||
id = "SwedishinfantryAK5"
|
||||
name = "[SMA] Ak 5 Soldier"
|
||||
detection_range = 1500
|
||||
threat_range = 500
|
||||
air_weapon_dist = 500
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class SwedishinfantryAK5GT(unittype.VehicleType):
|
||||
id = "SwedishinfantryAK5GT"
|
||||
name = "[SMA] Ak 5 Granattillsats Soldier"
|
||||
detection_range = 1000
|
||||
threat_range = 500
|
||||
air_weapon_dist = 500
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class SwedishinfantryKSP90(unittype.VehicleType):
|
||||
id = "SwedishinfantryKSP90"
|
||||
name = "[SMA] Ksp 90 Soldier"
|
||||
detection_range = 700
|
||||
threat_range = 700
|
||||
air_weapon_dist = 700
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class SwedishinfantryKSP58(unittype.VehicleType):
|
||||
id = "SwedishinfantryKSP58"
|
||||
name = "[SMA] Ksp 58 Soldier"
|
||||
detection_range = 1200
|
||||
threat_range = 1200
|
||||
air_weapon_dist = 1200
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class SwedishinfantryPskott86(unittype.VehicleType):
|
||||
id = "SwedishinfantryPskott86"
|
||||
name = "[SMA] Pskott 86 Soldier"
|
||||
detection_range = 1500
|
||||
threat_range = 400
|
||||
air_weapon_dist = 400
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class RBS_57(unittype.VehicleType):
|
||||
id = "RBS-57"
|
||||
name = "[SMA] RBS 57 ATGM"
|
||||
detection_range = 5000
|
||||
threat_range = 1000
|
||||
air_weapon_dist = 1000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class RBS_58(unittype.VehicleType):
|
||||
id = "RBS-58"
|
||||
name = "[SMA] RBS 58 ATGM"
|
||||
detection_range = 5000
|
||||
threat_range = 5000
|
||||
air_weapon_dist = 5000
|
||||
eplrs = True
|
||||
|
||||
|
||||
## ARTILLERY
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class Artillerisystem08(unittype.VehicleType):
|
||||
id = "Artillerisystem08"
|
||||
name = "[SMA] Artillerisystem 08 SPG"
|
||||
detection_range = 10000
|
||||
threat_range = 25000
|
||||
air_weapon_dist = 25000
|
||||
eplrs = True
|
||||
|
||||
|
||||
@vehiclemod
|
||||
class Grkpbv90(unittype.VehicleType):
|
||||
id = "Grkpbv90"
|
||||
name = "[SMA] Grkpbv 90 SPM"
|
||||
detection_range = 0
|
||||
threat_range = 7500
|
||||
air_weapon_dist = 7500
|
||||
eplrs = True
|
||||
|
||||
|
||||
## SHIPS
|
||||
|
||||
|
||||
@shipmod
|
||||
class HSwMS_Visby(unittype.ShipType):
|
||||
id = "HSwMS_Visby"
|
||||
name = "[SMA] HSwMS Visby Corvette"
|
||||
plane_num = 0
|
||||
helicopter_num = 1
|
||||
parking = 1
|
||||
detection_range = 180000
|
||||
threat_range = 30000
|
||||
air_weapon_dist = 30000
|
||||
|
||||
|
||||
@shipmod
|
||||
class Strb90(unittype.ShipType):
|
||||
id = "Strb90"
|
||||
name = "[SMA] Strb 90 FAC"
|
||||
plane_num = 0
|
||||
helicopter_num = 0
|
||||
parking = 0
|
||||
detection_range = 46300
|
||||
threat_range = 10000
|
||||
air_weapon_dist = 10000
|
||||
|
||||
|
||||
## HELICOPTERS
|
||||
|
||||
|
||||
@helicoptermod
|
||||
class HKP15B(HelicopterType):
|
||||
id = "HKP15B"
|
||||
height = 3.4
|
||||
width = 10.83
|
||||
length = 11.45
|
||||
fuel_max = 686
|
||||
max_speed = 311
|
||||
chaff = 0
|
||||
flare = 0
|
||||
charge_total = 0
|
||||
chaff_charge_size = 0
|
||||
flare_charge_size = 0
|
||||
eplrs = True
|
||||
radio_frequency = 124
|
||||
|
||||
panel_radio = {
|
||||
1: {
|
||||
"channels": {6: 41, 2: 31, 8: 50, 3: 32, 1: 30, 4: 33, 5: 40, 7: 42},
|
||||
},
|
||||
}
|
||||
|
||||
livery_name = "HKP15B" # from type
|
||||
Liveries = Liveries()[livery_name]
|
||||
|
||||
pylons: Set[int] = set()
|
||||
|
||||
tasks = [task.Reconnaissance, task.Transport]
|
||||
task_default = task.Reconnaissance
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user