143 Commits
v1.4.0 ... dev

Author SHA1 Message Date
Taco
9520b76246 Updated soundhandler with mirror performance for Red and Blue Clients… (#575) 2025-11-09 22:21:04 -05:00
Starfire13
67183c31da Improve B-52 mission types and update B-52 and B-1 loadouts (#615) 2025-11-09 21:50:01 -05:00
Druss99
54759f1f94 Fixed bugs wrt planning escort flights (#612) 2025-11-09 21:49:39 -05:00
Starfire13
0fcf308079 Add Mi-17 variant to Mi-8MT configuration (#614) 2025-11-08 18:09:44 -05:00
Drexyl
1ad27e27ea Update Splash Damage Plugin to 3.4.2 (#601)
Co-authored-by: Druss99 <42724070+Druss99@users.noreply.github.com>
2025-11-08 14:35:17 +00:00
Starfire13
eae5e1af30 Starfire campaign updates (#613) 2025-11-08 08:40:40 -05:00
Starfire13
b268d09432 Swap intra-flight and inter-flight radio indices (#608) 2025-11-07 08:33:25 -05:00
Drexyl
220b83a22d Add additional supported Carrier/LHA for Airboss (#603) 2025-11-07 08:32:07 -05:00
Starfire13
83530af35d 10.8 Update for Noisy Cricket, Aegean Aegis, and Vegas Nerve (#607) 2025-11-06 15:18:57 -05:00
Starfire13
00475d767b Update Final Countdown II and Operation Velvet Thunder to 10.8 (#606) 2025-11-06 09:43:14 -05:00
Starfire13
1a8faff717 Update Tornado loadouts (#610) 2025-11-06 09:39:36 -05:00
Druss99
68e0d43a1d fix frontline generation (#609) 2025-11-05 17:52:38 -05:00
Starfire13
0510af8b6b Added missing High Digit SAMs mod version (#605) 2025-11-02 14:59:32 -05:00
Raffson
c54ac4e21a Save compat fix w.r.t. renamed airport in AFG 2025-10-28 21:05:56 +01:00
Raffson
bbc41f4c1a Pydcs update for airfields (Normandy/Lappi/AFG) 2025-10-27 21:10:59 +01:00
Raffson
8bc8eb0e28 Update beacons 2025-10-27 20:50:24 +01:00
Starfire13
86f647cc6d Starfire campaign updates #1 (#604) 2025-10-26 23:16:40 +00:00
Raffson
2f16916ac4 Rename & Extend WW2-Essex layout to support ARA-VDM + CVA-31 2025-10-26 18:21:38 +01:00
Druss99
06044dd4c3 fix frontline debriefing (#602) 2025-10-23 07:15:37 -04:00
Eclipse/Druss99
c46cce01b6 fix ownfor being set to UN Peacekeepers regardless of campaign 2025-10-21 14:20:20 -04:00
Eclipse/Druss99
d78e570d9d fix a bug with untasked aircraft still spawning 2025-10-20 15:14:43 -04:00
Eclipse/Druss99
f4a1908088 fix a bug with pretense generation wrt neutral bases 2025-10-20 15:14:43 -04:00
Druss99
23341e69fa update changelog (#596) 2025-10-19 22:09:48 -04:00
Druss99
11fc28ad3a fix bugs wrt divert airfields for opfor (#595) 2025-10-19 21:40:40 -04:00
Druss99
9b96b7be26 fix runtime error where dictionary size changes (#594) 2025-10-19 20:10:19 -04:00
Raffson
f7ee923cbf Bump campaign version to 10.8 for neutral base support 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
3d788e3ae8 fix numerous UI elements using is_ownfor 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
624f2a00fd show PRESENT status for neutral TGOs 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
6c3cbbf0de avoid spawning neutral tgos with units 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
96d0b2b7d9 fix save compat 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
855d269420 refactor trigger zone generation 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
c0748e2a3e add support for neutral FOBs 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
9f10ecc884 fix save compat 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
31c80dfd02 refactor of previous commits
refactor to enum

typing and many other fixes

fix tests

attempt to fix some typescript

more typescript fixes

more typescript test fixes

revert all API changes

update to pydcs

mypy fixes

Use properties to check if player is blue/red/neutral

update requirements.txt

black -_-

bump pydcs and fix mypy

add opponent property

bump pydcs
2025-10-19 19:34:38 +02:00
Drexyl
362ce66f80 Airboss Moose Module 1.0 (#592)
* Initial Commit

* Rescue Helo Options

* Main modifications to Airboss/Moose Dev Branch

* Restore plugin.json

* Airboss Moose Module 1.0

* Fix plugins.json

---------

Co-authored-by: Druss99 <42724070+Druss99@users.noreply.github.com>
2025-10-18 14:08:48 +00:00
Drexyl
96ee73dd50 Fix for Offensive Jamming when object has been destroyed (#591)
* Fix Offensive Jamming when object has been destroyed
2025-10-18 09:42:02 -04:00
Eclipse/Druss99
f82fc3a930 use aircraft display names instead of variant ids in faction editor 2025-10-12 19:53:01 +02:00
Eclipse/Druss99
cf575fba2a fix a bug with parking allocation 2025-10-12 19:42:52 +02:00
Eclipse/Druss99
96f940bc59 fix bugs with mixed parking 2025-10-12 19:01:07 +02:00
Drexyl
98c71c255b Fix to exclude Moose plugins from Pretense injection (#585) 2025-10-07 18:58:19 -04:00
Drexyl
0e5567ceee Update EW Script with fixes/enhancements (#584) 2025-10-07 08:13:44 -04:00
Druss99
2832ece316 add channels for MiG-29 Fulcrum (#582) 2025-10-05 14:27:06 -04:00
Druss99
5cfd6c86f3 add radios for MiG-29 Fulcrum (#581) 2025-10-04 13:48:11 -04:00
Druss99
27ef269435 add support for more units (#580) 2025-09-22 18:27:34 -04:00
Druss99
8b46ef5e35 add support for MiG-29A Fulcrum (#579) 2025-09-20 18:41:23 -04:00
Eclipse/Druss99
e243d074ff su30 mod update 2025-09-20 19:36:41 +02:00
Eclipse/Druss99
cc39e6579b bump pydcs 2025-09-20 19:36:41 +02:00
Druss99
d7eaf1330f remove antiship from tu-160 (#577) 2025-09-20 13:05:02 -04:00
Druss99
c44ea0025d Fix plugins.json & moose plugins (#574) 2025-09-08 00:35:54 +02:00
Taco
1216c90891 Add Moose's Soundhandler Plugin (#558)
* Soundhandler: Adds sounds for weapons fired at or by blue units as well as destruction sounds for enemy jets and ground units.  By default plays only to the groups involved.

* Added Specific Options and removed 0.1 from mnemonic

* Update was incomplete before, use this one

* Linked in UI choices and consolidated logging functions

* Remove soundhandler duplicate

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-09-07 17:25:14 +00:00
Drexyl
ea80ddc321 Update CJS Super Hornet Mod to 2.4.4 (#572)
* Update CJS Super Hornet Mod to 2.4.4

* Update UI for CJS Super Hornet Mod to 2.4.4

* Update default loadouts for CJS Super Hornet Mod to 2.4.4

* Update changelog.md for CJS Super Hornet Mod to 2.4.4

* Update Growler loadout for missing external fuel tank
2025-09-07 19:24:07 +02:00
Druss99
121ca20edb Fix naval squadron retreat bug (#567) 2025-09-07 17:37:07 +02:00
Starfire13
e4586add72 Add factories to Northern Guardian (#571) 2025-09-07 17:29:45 +02:00
Xisco Lladó
32b2e40b37 Update convoys spawns & CTLD landing zones for Syrian Shield (#570)
* updated convoys spawns and CTLD landing zones

* removed red helipads on palmyra zone

---------

Co-authored-by: Xisco <x.llado@hotelinking.com>
2025-09-07 17:29:00 +02:00
Taco
e8190748eb New Moose-based Autolase alternative with UI options (#565) 2025-08-27 19:24:00 +02:00
Drexyl
d2aa615133 Update EW Script and refine logic (#555) 2025-08-27 19:20:27 +02:00
Starfire13
756602e682 Update Su-24 loadouts (#566) 2025-08-27 19:18:39 +02:00
Taco
204950e487 MarkerOps Plugin (#560)
* MarkerOps

* Change MarkerOps to MooseMarkerOps in all places

* Prettify name in UI

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-08-23 16:37:34 +02:00
Druss99
6290607eb6 Expend all ARMs for SEAD (#562) 2025-08-13 12:32:36 +02:00
Druss99
afad73ac27 Fix bugs while loading air wing configs (#552)
* fix bugs with air wing config loading

* add aircraft type to airwing config for better loading
2025-08-10 21:14:38 +02:00
Eclipse/Druss99
52fbc75e1c update changelog 2025-08-10 19:40:06 +02:00
Xisco Lladó
5c5af40bd5 Update Operation syrian shield (#554)
* New campaign Operation_syrian_shield

* removed author on player factions. Credited to 'Fuzzle'

* changed recommended factions to the easy way

* Update operation_syrian_shield.yaml

removed mission time, and squadrons start full

* Update operation_syrian_shield.yaml

better campaign description

* added better squadron descriptions

* updated campaign description

---------

Co-authored-by: Xisco <x.llado@hotelinking.com>
2025-08-10 16:19:50 +02:00
ingax01
d1f66ed040 Remove unused SU-30 payload files (#551) 2025-08-04 00:08:28 +02:00
Druss99
6467193c88 Save-compat fix for su-30 update (#553) 2025-08-04 00:07:38 +02:00
Drexyl
b2670ba30b Add Military Aircraft Mod 1.7.2 (#547) 2025-08-03 02:02:14 +02:00
Raffson
dcf4da57b3 Update starlette + fastapi 2025-08-02 19:27:30 +02:00
Raffson
6f825db086 Update h11 2025-08-02 19:27:15 +02:00
Raffson
afa9122885 Bump electron to 28.3.2 2025-08-02 19:09:31 +02:00
Raffson
df8e09a1ad Update npm packages 2025-08-02 19:00:36 +02:00
Druss99
5d038e8247 Add support for Control Point Influence Zones (#540) 2025-08-02 18:47:55 +02:00
Xisco Lladó
ce70952ee0 New campaign Operation_syrian_shield (#544)
* New campaign Operation_syrian_shield

* removed author on player factions. Credited to 'Fuzzle'

* changed recommended factions to the easy way

* Update operation_syrian_shield.yaml

removed mission time, and squadrons start full

* Update operation_syrian_shield.yaml

better campaign description

* added better squadron descriptions

---------

Co-authored-by: Xisco <x.llado@hotelinking.com>
2025-08-02 18:30:15 +02:00
Drexyl
1d16aba678 Add Moose as base plugin (#522) 2025-08-02 18:28:36 +02:00
Revinl
f3156ab51a Northern Guardian campaign for the Germany map by Revinl (#545) 2025-08-02 16:27:52 +00:00
ingax01
ed9fa37341 Updated SU-30 weapon definitions to version v2.8.03 Beta + CWS 3.70 (#546) 2025-08-02 18:20:01 +02:00
Starfire13
12aa1ee287 Add sniper pod to Viper loadout (#549) 2025-08-02 18:15:07 +02:00
Druss99
7e755f1eb1 bump pydcs (#548) 2025-07-28 22:18:29 +02:00
Starfire13
3da1689767 Fixed typos in Tetrarch description (#542) 2025-07-21 16:53:02 +02:00
Raffson
0671e45dde Restore squadrons for Essex-transformed carriers 2025-07-20 12:36:48 +02:00
Drexyl
c15b4ac83f Essex as AircraftCarrier hack (#541) 2025-07-19 18:21:33 +00:00
Starfire13
f1eb514295 Crossing the Rubicon improved performance (#539) 2025-07-14 12:38:57 +02:00
Raffson
df8f1c4291 Fix some auto-rotate issues in exercise quasar 2025-07-13 19:19:09 +02:00
Raffson
c32525b542 Formatting -_- 2025-07-13 19:05:10 +02:00
Raffson
db14b691cc Make F4U-1D Carrier/LHA capable 2025-07-13 18:55:09 +02:00
Raffson
052a04ca05 Properly support Essex carrier 2025-07-13 18:41:20 +02:00
Raffson
ad100a1a17 Overrride WFMR-window's reject to properly reset game-state 2025-07-13 13:46:57 +02:00
Drexyl
4412a0fa91 Add Tornado F3 ADV (1.0) mod support 2025-07-13 13:35:53 +02:00
Starfire13
23a7add2f1 Add missing Apache and FC Tiger II to Blufor Late Cold War faction (#538) 2025-07-13 13:34:21 +02:00
Starfire13
040362b333 Add Viet Cong 1965 faction (#537) 2025-07-13 13:32:59 +02:00
Starfire13
4155afc69a Set performance to 3/3 for Crossing The Rubicon (#536)
Setting this to 3/3 for now as folks are having performance issues
2025-07-13 13:32:05 +02:00
Starfire13
cf84c03d8f Implements campaign inversion for 13 of Starfire's campaigns (#535) 2025-07-13 13:31:32 +02:00
Starfire13
336384cd74 Implement campaign inversion for Crossing the Rubicon (#534) 2025-07-13 13:30:27 +02:00
Druss99
da7d3a3f1a Fix S-3B unable to do OCA/Aircraft (#533) 2025-07-08 22:25:40 +00:00
Starfire13
1231cb145d Add Germany Cold War campaign (#530) 2025-07-09 00:16:50 +02:00
Drexyl
47488a0bdd F4U-1D Banner Image (#529) 2025-07-08 07:55:58 +02:00
Starfire13
015859e91e F4U-1D loadouts (#528) 2025-07-08 00:39:34 +02:00
Raffson
9c0d114085 Save compat fix for Sinai's renamed Palmahim to Palmachim 2025-07-06 22:40:23 +02:00
Raffson
f907356f93 Pydcs update for new Sinai airports + save-compat fix 2025-07-06 22:37:07 +02:00
Raffson
fcb610423b Update beacon-data 2025-07-06 22:12:32 +02:00
Raffson
3086f64e98 F4U-1D support except banner & payloads 2025-07-06 22:09:50 +02:00
Raffson
5592a834c9 Downgrade h11 to 0.14 2025-07-06 21:59:09 +02:00
Raffson
1e48affc1c Add Japanese WW2 ground units 2025-07-06 21:59:09 +02:00
Raffson
f27b6e28e0 Add support Essex 2025-07-06 21:58:59 +02:00
Raffson
d0ff271a79 Update pydcs for 2.9.17.12034 2025-07-06 19:31:05 +02:00
Cedric Menard
9a00a60cd4 Initial support for Germany Cold War (#518)
* Initial support of Germany Cold War terrain

* Updated with latest beacon import

* Updated changelog and ran black

* Fixed the GermanyCW landmap and naming scheme

* Temporarily changed the requirements for pydcs to my branch (will switch back when merged)

* Updated landmap for germanycw with even fewer exclusions

* Removed unnecessary changes

* Updated requirements to lastest pydcs

* Fixed naming issues for Germany

* Update pydcs to latest commit for GCW support

* Add landmap to game.theater.__init__

* Update requirements.txt

* Resolve PyCharm type-waring

* Updated pydcs and exclusion of germany

* Updated land and sea shapefiles for Germany Cold War with new map limits

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-07-06 19:30:00 +02:00
MrPozor
5254ef3442 Fix #304: Waypoint numbers should start at 0 on map (#526) 2025-06-11 19:50:30 +02:00
MrPozor
fe8efbe905 Revert "Waypoint numbers should start at 0 on map. (#524)" (#525) 2025-06-10 21:28:14 +02:00
MrPozor
48040d7306 Waypoint numbers should start at 0 on map. (#524) 2025-06-09 22:58:15 +02:00
Raffson
f3ed25b5b2 Make EPLRS configurable
Resolves #517
2025-06-07 22:04:56 +02:00
Raffson
2d15db1932 Update h11 to 0.16.0 2025-06-07 19:40:19 +02:00
Raffson
1fa1f830d4 Update bug-report templates for v1.4.1 2025-06-07 19:33:05 +02:00
Raffson
0c0291fc5e Changelog for v1.4.1 2025-06-07 19:32:29 +02:00
Raffson
24c6b62ad4 Fix missing check in handle_ew_jamming w.r.t. plugin option 2025-06-07 19:23:36 +02:00
Starfire13
8c9343e0db Campaign inversion fix for Operation Vectron's Claw (#520) 2025-06-07 16:04:56 +02:00
Starfire13
a33da01af6 Blufor Late Cold War & Russia 1980 faction update (#521) 2025-06-07 16:03:40 +02:00
Starfire13
ce8ccb0227 Update README.md with correct link to preview builds page on the wiki (#519) 2025-05-30 13:10:07 +02:00
Drexyl
68a9dbd141 Update Grinnelli Designs F-22A Mod to 2.0.0 (May 2025) (#515) 2025-05-28 00:54:14 +02:00
Drexyl
4c3f0a82d9 Add EA-6B Prowler to ALQ-99 Jammer Pod (#516) 2025-05-28 00:48:55 +02:00
Starfire13
dc85287cd8 Add locales to Ukraine factions (#513)
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-24 15:48:38 +00:00
szcz13
65f4a2cf86 Added new and updated weapons CLSIDs and payloads for MiG-31BM mod (#514) 2025-05-24 17:38:16 +02:00
Druss99
02de6d325a Add invisible FOBs (#504)
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-18 17:50:47 +02:00
Druss99
7b16967641 Prevent saving packages with Escorts only (#509)
Fixes a bug that would later pop up when trying to calculate start-times...
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-17 15:58:35 +00:00
Starfire13
0e7ea0050f Add OCA-Aircraft to B-21's tasks (#510)
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-17 15:57:12 +00:00
Starfire13
ec0e64eaf4 Add Growler + Raptor squadrons to some campaigns (#505) 2025-05-17 17:55:41 +02:00
szcz13
b12169f180 Update for MiG-31BM v2.6 (#512)
* Add files via upload

* Add files via upload

* Update start_generator.py

* Update faction.py

* Update QNewGameWizard.py

* Update QGeneratorSettings.py

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update MiG-31BM.lua

* Added SEAD Escort to MiG-31BM.yaml

* mig31bm.py fixed weapon injection

* Update mig31bm.py

formatted black

* Update faction.py

reformatted black

* Update start_generator.py

reformatted black

* Update __init__.py

* Update QGeneratorSettings.py

reformatted black

* added support for MiG-31BM mod

* Commit changes which can't seem to be discarded...

* Remove (presumably) accidentally copied file

* Remove unused imports

* Updated MiG-31BM mod version in QGeneratorSettings.py

* Added new Kh-31P missile to MiG-31BM mod

* Added new Kh-31P missile to MiG-31BM mod

* Updated MiG-31BM mod payloads for a new Kh-31P.lua

* Remove double entry

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-17 15:55:05 +00:00
Raffson
3a5d0e831c Formatting -_- 2025-05-10 16:40:57 +02:00
Raffson
903a6c4d5f Update changelog 2025-05-07 20:12:03 +02:00
Raffson
e36f07a3ff Preference option to set up preferences on every start (#503) 2025-05-07 20:02:21 +02:00
Raffson
2d65a038ee Remove print statements 2025-05-06 17:24:29 +02:00
Raffson
09617adc56 Passive defense for non-air-to-air flights in package with jammer 2025-05-01 19:57:01 +02:00
Raffson
01832b6594 Add MPS-410 to weapons data 2025-05-01 19:41:02 +02:00
Raffson
30ad71f93b Refactor & improve EW-jamming setup in mission-generator 2025-05-01 17:22:46 +02:00
Raffson
4f8ce77e50 Add "has_built_in_ecm" field to AircraftType
To be used with EW-jamming script...
2025-05-01 17:21:52 +02:00
Raffson
464ece60da Save-compat fix w.r.t. renamed airfield 2025-04-21 21:24:32 +02:00
Raffson
cafaea897d Pydcs update for new Lappi airfields 2025-04-21 21:21:36 +02:00
Raffson
58820dd6c6 Update Lappi beacons 2025-04-21 21:18:29 +02:00
Starfire13
8e845e6a27 Add Tornado IDS to Blufor Late Cold War faction (#502)
Add missing Tornado IDS
2025-04-21 18:04:13 +02:00
Raffson
e1b55816e3 Update actions/checkout to v4 2025-04-20 23:21:00 +02:00
Raffson
e95661c6a2 Update actions/setup-node to v4 2025-04-20 23:17:23 +02:00
Raffson
c96e0d8f28 Update actions/setup-python to v5? 2025-04-20 23:14:52 +02:00
Raffson
dfb65facb5 Another attempt at fixing the pipeline -_- 2025-04-20 23:01:22 +02:00
Raffson
8c6ca4de5f Pin python version for black in lint.yml 2025-04-20 22:58:19 +02:00
Raffson
1c3dee2098 Pydcs data update 2025-04-19 17:49:21 +02:00
Raffson
170e432008 Preparations for v1.5.0 2025-04-14 21:34:17 +02:00
626 changed files with 163561 additions and 32979 deletions

View File

@@ -31,7 +31,7 @@ body:
If the bug was found in a test/development build, select "Test build" or "Development build" If the bug was found in a test/development build, select "Test build" or "Development build"
and provide a link to the applicable build in the field below. and provide a link to the applicable build in the field below.
options: options:
- v1.3.1 - v1.4.1
- Test build - Test build
- type: input - type: input
attributes: attributes:

View File

@@ -39,7 +39,7 @@ body:
If the bug was found in a test/development build, select "Test build" or "Development build" If the bug was found in a test/development build, select "Test build" or "Development build"
and provide a link to the applicable build in the field below. and provide a link to the applicable build in the field below.
options: options:
- 1.3.1 - 1.4.1
- Test build - Test build
- type: input - type: input
attributes: attributes:

View File

@@ -4,7 +4,7 @@ runs:
using: composite using: composite
steps: steps:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: "16" node-version: "16"
cache: npm cache: npm

View File

@@ -4,7 +4,7 @@ runs:
using: composite using: composite
steps: steps:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
cache: pip cache: pip

View File

@@ -12,7 +12,7 @@ jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true

View File

@@ -7,8 +7,8 @@ jobs:
name: Black name: Black
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-python@v2 - uses: actions/setup-python@v5
- uses: psf/black@stable - uses: psf/black@stable
with: with:
src: "." src: "."
@@ -18,7 +18,7 @@ jobs:
name: Type checking name: Type checking
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true

View File

@@ -14,7 +14,7 @@ jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true

View File

@@ -5,7 +5,7 @@ jobs:
name: Python tests name: Python tests
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
with: with:
submodules: true submodules: true
@@ -24,7 +24,7 @@ jobs:
name: Typescript tests name: Typescript tests
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Set up JS environment - name: Set up JS environment
uses: ./.github/actions/setup-liberation-js uses: ./.github/actions/setup-liberation-js

View File

@@ -68,7 +68,7 @@ Our plans for future releases can be found on our
[Projects page](https://github.com/dcs-retribution/dcs-retribution/projects). [Projects page](https://github.com/dcs-retribution/dcs-retribution/projects).
Each planned release has a Project, and the page for that project has columns for to do, Each planned release has a Project, and the page for that project has columns for to do,
in progress, and done. Items in the Done column are in the in progress, and done. Items in the Done column are in the
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Betas) [preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
for that release. Items in the To do column are planned to be added to that release. for that release. Items in the To do column are planned to be added to that release.
## Resources ## Resources

View File

@@ -1,3 +1,47 @@
# Retribution v1.5.0
## Features/Improvements
* **[Campaigns]** Ability to define invisible FOBs
* **[Campaigns]** Ability to define influence zones for Control Points
* **[Plugins]** Improvements to AI support for EW Script 2.0
* **[Plugins]** Add moose as a base plugin
* **[Config]** New preference setting to trigger the first-start window on every start (could help in scenarios multiple Retribution instances need to run concurrently)
* **[Modding]** Update Grinelli Designs F-22A Mod to 2.0.0 (May 2025)
* **[Campaign Design]** Added support for Germany Cold War terrain by Ugra Media
* **[Options]** New option to control EPLRS-task injection in mission generator
* **[Modding]** Add Tornado F3 ADV (v1.0, requires FC3 fix)
* **[Modding]** Add Military Aircraft Mod 1.7.2 (See VSN Discord)
* **[Payload Editor]** Add Sniper POD
* **[Modding]** Update SU-30 mod to v2.8.04 Beta + CWS 3.72
* **[Plugins]** EW Script - DEAD added to applicable flight types.
* **[Plugins]** EW Script - Offensive Jamming restricted to aircraft with ALQ99/249 pods, or "has_built_in_jamming: true" in aircarft yaml (AI and Player)
* **[Modding]** Update to CJS Super Hornet Mod v2.4.4
* **[Engine]** Support for DCS 2.9.20.15010, including MiG-29A Fulcrum support.
* **[Campaigns]** Ability to define neutral FOBs and Airfields
* **[Modding]** Add Airboss Moose Module
* **[AirWing]** Use aircraft display names for easier differentiation between modules
## Fixes
* **[Flight Plans]** Fixed bugs wrt planning escort flights
* **[Flight Plans]** Added AntiShipStrike as a fallback task for OCA/Aircraft to fix a bug where the S-3B could not do OCA/Aircraft
* **[Squadrons]** Fixed a bug where loading an air wing config would not properly load all squadrons
* **[Flight Plans]** Fixed a bug where SEAD flights would fire one ARM and RTB
* **[Plugins]** EW Script - Fix radar detection routine.
* **[Campaign]** Fixed a bug where sinking a destroyer in a carrier group would cause squadrons to be removed from the carrier
* **[Engine]** Fixed a bug with transfers to Helipads
* **[Engine]** Fixed a bug with parking allocation
* **[Flight Plans]** Fixed a bug where divert airfield was broken for opfor
* **[Engine]** Fixed a bug with state.json loading wrt transfers
* **[Engine]** Fixed a bug wrt pretense generation and moose script conflicts
* **[Engine]** Fixed a bug where frontline debriefing was not properly calculated
* **[Mission Generation]** Fixed an issue where blue and red units spawned next to each other on frontlines
# Retribution v1.4.1 (hotfix)
## Fixes
* **[Plugins]** Avoid injecting jamming-tasks for player-controller aircraft when EW-Jamming plugin is disabled
# Retribution v1.4.0 # Retribution v1.4.0
## Features/Improvements ## Features/Improvements

22917
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,7 @@
"@types/leaflet": "^1.9.8", "@types/leaflet": "^1.9.8",
"@types/redux-logger": "^3.0.13", "@types/redux-logger": "^3.0.13",
"@types/websocket": "^1.0.10", "@types/websocket": "^1.0.10",
"electron": "^22.3.27", "electron": "^28.3.2",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0", "generate-license-file": "^2.0.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",

View File

@@ -60,7 +60,7 @@ const WaypointMarker = (props: WaypointMarkerProps) => {
useEffect(() => { useEffect(() => {
const waypoint = props.waypoint; const waypoint = props.waypoint;
marker.current?.setTooltipContent( marker.current?.setTooltipContent(
`${props.number} ${waypoint.name}<br />` + `${props.number-1} ${waypoint.name}<br />` +
`${waypoint.altitude_ft.toFixed()} ft ${waypoint.altitude_reference}<br />` + `${waypoint.altitude_ft.toFixed()} ft ${waypoint.altitude_reference}<br />` +
waypoint.timing waypoint.timing
); );

View File

@@ -7,9 +7,9 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "DCS Retribution" project = "DCS Retribution"
copyright = "2024, DCS Retribution Team" copyright = "2025, DCS Retribution Team"
author = "DCS Retribution Team" author = "DCS Retribution Team"
release = "1.4.0" release = "1.5.0"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@@ -40,7 +40,7 @@ class ArmedForces:
# all layouts which are usable by the given faction. # all layouts which are usable by the given faction.
for layout in LAYOUTS.layouts: for layout in LAYOUTS.layouts:
if layout.generic and layout.usable_by_faction(faction): if layout.generic and layout.usable_by_faction(faction):
# Creates a faction compatible GorceGroup # Creates a faction compatible ForceGroup
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction)) self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
# Add all preset groups afterwards to prevent them being merged with generics # Add all preset groups afterwards to prevent them being merged with generics

View File

@@ -258,6 +258,9 @@ class ForceGroup:
if unit_count == 0: if unit_count == 0:
# No units to be created so dont create a theater group for them # No units to be created so dont create a theater group for them
return return
# If the control point is neutral, don't create any units at all
if ground_object.control_point.captured.is_neutral:
return
# Generate Units # Generate Units
fixed_pos = FIXED_POS_ARG in unit_group.name fixed_pos = FIXED_POS_ARG in unit_group.name
fixed_hdg = FIXED_HDG_ARG in unit_group.name fixed_hdg = FIXED_HDG_ARG in unit_group.name

View File

@@ -35,6 +35,7 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents from game.sim.gameupdateevents import GameUpdateEvents
from game.sim.simulationresults import SimulationResults from game.sim.simulationresults import SimulationResults
from game.squadrons import Squadron, Pilot from game.squadrons import Squadron, Pilot
from game.theater.player import Player
from game.transfers import TransferOrder from game.transfers import TransferOrder
from game.data.weapons import WeaponType from game.data.weapons import WeaponType
from .flightmember import FlightMember from .flightmember import FlightMember
@@ -174,7 +175,7 @@ class Flight(
self.roster = FlightMembers.from_roster(self, self.roster) self.roster = FlightMembers.from_roster(self, self.roster)
@property @property
def blue(self) -> bool: def blue(self) -> Player:
return self.squadron.player return self.squadron.player
@property @property

View File

@@ -23,7 +23,13 @@ class EscortFlightPlan(FormationAttackFlightPlan):
@property @property
def split_time(self) -> datetime: def split_time(self) -> datetime:
if self.package.primary_flight and self.package.primary_flight.flight_plan: # Avoid infinite recursion when this escort flight is itself the primary flight
# This can happen when only escort flights remain in a package
if (
self.package.primary_flight
and self.package.primary_flight != self.flight
and self.package.primary_flight.flight_plan
):
return self.package.primary_flight.flight_plan.mission_departure_time return self.package.primary_flight.flight_plan.mission_departure_time
else: else:
return super().split_time return super().split_time

View File

@@ -11,7 +11,7 @@ from ..packagewaypoints import PackageWaypoints
if TYPE_CHECKING: if TYPE_CHECKING:
from game.coalition import Coalition from game.coalition import Coalition
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.theater import ConflictTheater from game.theater import ConflictTheater, Player
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from ..flight import Flight from ..flight import Flight
from ..package import Package from ..package import Package
@@ -38,7 +38,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
self._generate_package_waypoints_if_needed(dump_debug_info) self._generate_package_waypoints_if_needed(dump_debug_info)
self._flight_plan = self.build(dump_debug_info) self._flight_plan = self.build(dump_debug_info)
except NavMeshError as ex: except NavMeshError as ex:
color = "blue" if self.flight.squadron.player else "red" color = "blue" if self.flight.squadron.player.is_blue else "red"
raise PlanningError( raise PlanningError(
f"Could not plan {color} {self.flight.flight_type.value} from " f"Could not plan {color} {self.flight.flight_type.value} from "
f"{self.flight.departure} to {self.package.target}" f"{self.flight.departure} to {self.package.target}"
@@ -71,7 +71,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
return self.flight.coalition return self.flight.coalition
@property @property
def is_player(self) -> bool: def is_player(self) -> Player:
return self.coalition.player return self.coalition.player
@property @property

View File

@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Optional, TYPE_CHECKING, Union from typing import Any, Optional, TYPE_CHECKING, Union
@@ -26,6 +25,7 @@ class SquadronConfig:
name: Optional[str] name: Optional[str]
nickname: Optional[str] nickname: Optional[str]
female_pilot_percentage: Optional[int] female_pilot_percentage: Optional[int]
aircraft_type: Optional[str]
@property @property
def auto_assignable(self) -> set[FlightType]: def auto_assignable(self) -> set[FlightType]:
@@ -51,6 +51,7 @@ class SquadronConfig:
data.get("name", None), data.get("name", None),
data.get("nickname", None), data.get("nickname", None),
data.get("female_pilot_percentage", None), data.get("female_pilot_percentage", None),
data.get("aircraft_type", None),
) )
@staticmethod @staticmethod
@@ -83,15 +84,14 @@ class CampaignAirWingConfig:
try: try:
base = theater.control_point_named(base_id) base = theater.control_point_named(base_id)
except: except:
if base_id == "Red CV": logging.warning(
base = next((c for c in carriers if not c.captured), None) f"Control point {base_id} not found, trying to match by full name"
elif base_id == "Blue CV": )
base = next((c for c in carriers if c.captured), None) if not base:
elif base_id == "Red LHA": try:
base = next((l for l in lhas if not l.captured), None) base = theater.control_point_by_full_name(base_id)
elif base_id == "Blue LHA": except KeyError:
base = next((l for l in lhas if l.captured), None) logging.error(f"Control point {base_id} not found, skipping")
for squadron_data in squadron_configs: for squadron_data in squadron_configs:
if base is None: if base is None:
logging.warning( logging.warning(

View File

@@ -60,7 +60,7 @@ class DefaultSquadronAssigner:
) -> Optional[SquadronDef]: ) -> Optional[SquadronDef]:
for preferred_aircraft in config.aircraft: for preferred_aircraft in config.aircraft:
squadron_def = self.find_preferred_squadron( squadron_def = self.find_preferred_squadron(
preferred_aircraft, config.primary, control_point preferred_aircraft, config.aircraft_type, config.primary, control_point
) )
if squadron_def is not None: if squadron_def is not None:
return squadron_def return squadron_def
@@ -78,7 +78,11 @@ class DefaultSquadronAssigner:
) )
def find_preferred_squadron( def find_preferred_squadron(
self, preferred_aircraft: str, task: FlightType, control_point: ControlPoint self,
preferred_aircraft: str,
aircraft_type: Optional[str],
task: FlightType,
control_point: ControlPoint,
) -> Optional[SquadronDef]: ) -> Optional[SquadronDef]:
# Attempt to find a squadron with the name in the request. # Attempt to find a squadron with the name in the request.
squadron_def = self.find_squadron_by_name( squadron_def = self.find_squadron_by_name(
@@ -90,7 +94,7 @@ class DefaultSquadronAssigner:
# If the name didn't match a squadron available to this coalition, try to find # If the name didn't match a squadron available to this coalition, try to find
# an aircraft with the matching name that meets the requirements. # an aircraft with the matching name that meets the requirements.
try: try:
aircraft = AircraftType.named(preferred_aircraft) aircraft = AircraftType.named(aircraft_type or preferred_aircraft)
except KeyError: except KeyError:
logging.warning( logging.warning(
"%s is neither a compatible squadron or a known aircraft type, " "%s is neither a compatible squadron or a known aircraft type, "

View File

@@ -16,6 +16,7 @@ from dcs.terrain import Airport
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from game.controlpoint_influenceradius import ControlPointInfluenceRadius, point_in_zone
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.positioned import Positioned from game.positioned import Positioned
from game.profiling import logged_duration from game.profiling import logged_duration
@@ -24,6 +25,8 @@ from game.theater.controlpoint import (
Airfield, Airfield,
Carrier, Carrier,
ControlPoint, ControlPoint,
ControlPointType,
Player,
Fob, Fob,
Lha, Lha,
OffMapSpawn, OffMapSpawn,
@@ -53,6 +56,8 @@ class MizCampaignLoader:
FOB_UNIT_TYPE = Unarmed.SKP_11.id FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"] FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"]
INVISIBLE_FOB_UNIT_TYPE = Unarmed.M_818.id
NEUTRAL_FOB_UNIT_TYPE = Unarmed.KrAZ6322.id
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
@@ -120,9 +125,13 @@ class MizCampaignLoader:
def control_point_from_airport( def control_point_from_airport(
self, airport: Airport, ctld_zones: List[Tuple[Point, float]] self, airport: Airport, ctld_zones: List[Tuple[Point, float]]
) -> ControlPoint: ) -> ControlPoint:
cp = Airfield( if airport.dynamic_spawn:
airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones starting_coalition = Player.NEUTRAL
) elif airport.is_blue():
starting_coalition = Player.BLUE
else:
starting_coalition = Player.RED
cp = Airfield(airport, self.theater, starting_coalition, ctld_zones=ctld_zones)
# Use the unlimited aircraft option to determine if an airfield should # Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted". # be owned by the player when the campaign is "inverted".
@@ -130,42 +139,54 @@ class MizCampaignLoader:
return cp return cp
def country(self, blue: bool) -> Country: def country(self, blue: Player) -> Country:
country = self.mission.country( if blue.is_blue:
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name country = self.mission.country(self.BLUE_COUNTRY.name)
) else:
country = self.mission.country(self.RED_COUNTRY.name)
# Should be guaranteed because we initialized them. # Should be guaranteed because we initialized them.
assert country assert country
return country return country
@property @property
def blue(self) -> Country: def blue(self) -> Country:
return self.country(blue=True) return self.country(blue=Player.BLUE)
@property @property
def red(self) -> Country: def red(self) -> Country:
return self.country(blue=False) return self.country(blue=Player.RED)
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]: def off_map_spawns(self, blue: Player) -> Iterator[PlaneGroup]:
for group in self.country(blue).plane_group: for group in self.country(blue).plane_group:
if group.units[0].type == self.OFF_MAP_UNIT_TYPE: if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
yield group yield group
def carriers(self, blue: bool) -> Iterator[ShipGroup]: def carriers(self, blue: Player) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group: for group in self.country(blue).ship_group:
if group.units[0].type == self.CV_UNIT_TYPE: if group.units[0].type == self.CV_UNIT_TYPE:
yield group yield group
def lhas(self, blue: bool) -> Iterator[ShipGroup]: def lhas(self, blue: Player) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group: for group in self.country(blue).ship_group:
if group.units[0].type == self.LHA_UNIT_TYPE: if group.units[0].type == self.LHA_UNIT_TYPE:
yield group yield group
def fobs(self, blue: bool) -> Iterator[VehicleGroup]: def fobs(self, blue: Player) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group: for group in self.country(blue).vehicle_group:
if group.units[0].type == self.FOB_UNIT_TYPE: if group.units[0].type == self.FOB_UNIT_TYPE:
yield group yield group
def invisible_fobs(self, blue: Player) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group:
if group.units[0].type == self.INVISIBLE_FOB_UNIT_TYPE:
yield group
@property
def neutral_fobs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type == self.NEUTRAL_FOB_UNIT_TYPE:
yield group
@property @property
def ships(self) -> Iterator[ShipGroup]: def ships(self) -> Iterator[ShipGroup]:
for group in self.red.ship_group: for group in self.red.ship_group:
@@ -272,16 +293,22 @@ class MizCampaignLoader:
def scenery(self) -> List[SceneryGroup]: def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones) return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
@cached_property
def cp_influence_zones(self) -> List[ControlPointInfluenceRadius]:
return ControlPointInfluenceRadius.from_trigger_zones(
self.mission.triggers._zones
)
@cached_property @cached_property
def control_points(self) -> dict[UUID, ControlPoint]: def control_points(self) -> dict[UUID, ControlPoint]:
control_points = {} control_points = {}
for airport in self.mission.terrain.airport_list(): for airport in self.mission.terrain.airport_list():
if airport.is_blue() or airport.is_red(): if airport.is_blue() or airport.is_red() or airport.is_neutral():
ctld_zones = self.get_ctld_zones(airport.name) ctld_zones = self.get_ctld_zones(airport.name)
control_point = self.control_point_from_airport(airport, ctld_zones) control_point = self.control_point_from_airport(airport, ctld_zones)
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for blue in (False, True): for blue in (Player.RED, Player.BLUE):
for group in self.off_map_spawns(blue): for group in self.off_map_spawns(blue):
control_point = OffMapSpawn( control_point = OffMapSpawn(
str(group.name), group.position, self.theater, starts_blue=blue str(group.name), group.position, self.theater, starts_blue=blue
@@ -312,17 +339,47 @@ class MizCampaignLoader:
control_point.captured_invert = fob.late_activation control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for fob in self.invisible_fobs(blue):
ctld_zones = self.get_ctld_zones(fob.name)
control_point = Fob(
str(fob.name),
fob.position,
self.theater,
starts_blue=blue,
ctld_zones=ctld_zones,
is_invisible=True,
)
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
for fob in self.neutral_fobs:
ctld_zones = self.get_ctld_zones(fob.name)
control_point = Fob(
str(fob.name),
fob.position,
self.theater,
starts_blue=Player.NEUTRAL,
ctld_zones=ctld_zones,
)
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
if self.cp_influence_zones:
for cp in control_points.values():
for influence_radius in self.cp_influence_zones:
if cp.full_name == influence_radius.cp_name:
cp.influence_radius = influence_radius
return control_points return control_points
@property @property
def front_line_path_groups(self) -> Iterator[VehicleGroup]: def front_line_path_groups(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=True).vehicle_group: for group in self.country(blue=Player.BLUE).vehicle_group:
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE: if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
yield group yield group
@property @property
def shipping_lane_groups(self) -> Iterator[ShipGroup]: def shipping_lane_groups(self) -> Iterator[ShipGroup]:
for group in self.country(blue=True).ship_group: for group in self.country(blue=Player.BLUE).ship_group:
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE: if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
yield group yield group
@@ -346,7 +403,7 @@ class MizCampaignLoader:
@property @property
def cp_convoy_spawns(self) -> Iterator[VehicleGroup]: def cp_convoy_spawns(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=True).vehicle_group: for group in self.country(blue=Player.BLUE).vehicle_group:
if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE: if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE:
yield group yield group
@@ -471,7 +528,59 @@ class MizCampaignLoader:
def objective_info( def objective_info(
self, near: Positioned, allow_naval: bool = False self, near: Positioned, allow_naval: bool = False
) -> Tuple[ControlPoint, Distance]: ) -> Tuple[ControlPoint, Distance]:
closest = self.theater.closest_control_point(near.position, allow_naval) zones_containing_point = [
z
for z in self.cp_influence_zones
if point_in_zone(z.zone_def, near.position)
]
# Ensure we only consider naval control points if allow_naval is True
candidates = [
self.theater.control_point_named(z.cp_name) for z in zones_containing_point
]
if not allow_naval:
candidates = [
cp
for cp in candidates
if cp.cptype
not in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]
]
if candidates:
closest = min(
candidates, key=lambda cp: cp.position.distance_to_point(near.position)
)
distance = meters(closest.position.distance_to_point(near.position))
return closest, distance
# If no zones contain the point, find the closest control point without an influence radius
if not allow_naval:
fallback_candidates = [
cp
for cp in self.theater.controlpoints
if cp.cptype
not in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]
]
else:
fallback_candidates = self.theater.controlpoints
fallback_candidates = [
cp for cp in fallback_candidates if not cp.influence_radius
]
if not fallback_candidates:
raise RuntimeError(
f"All control points have an influence zone but no zones contain {near} at {near.position}"
)
closest = min(
fallback_candidates,
key=lambda cp: cp.position.distance_to_point(near.position),
)
distance = meters(closest.position.distance_to_point(near.position)) distance = meters(closest.position.distance_to_point(near.position))
return closest, distance return closest, distance

View File

@@ -17,6 +17,7 @@ from game.procurement import AircraftProcurementRequest, ProcurementAi
from game.profiling import MultiEventTracer, logged_duration from game.profiling import MultiEventTracer, logged_duration
from game.squadrons import AirWing from game.squadrons import AirWing
from game.theater.bullseye import Bullseye from game.theater.bullseye import Bullseye
from game.theater.player import Player
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.transfers import PendingTransfers from game.transfers import PendingTransfers
@@ -32,7 +33,7 @@ if TYPE_CHECKING:
class Coalition: class Coalition:
def __init__( def __init__(
self, game: Game, faction: Faction, budget: float, player: bool self, game: Game, faction: Faction, budget: float, player: Player
) -> None: ) -> None:
self.game = game self.game = game
self.player = player self.player = player
@@ -68,9 +69,11 @@ class Coalition:
@property @property
def coalition_id(self) -> int: def coalition_id(self) -> int:
if self.player: if self.player.is_blue:
return 2 return 2
return 1 elif self.player.is_red:
return 1
return 0
@property @property
def opponent(self) -> Coalition: def opponent(self) -> Coalition:
@@ -95,12 +98,23 @@ class Coalition:
state = self.__dict__.copy() state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically # Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility. # recomputed on load for the sake of save compatibility.
del state["_threat_zone"]
del state["_navmesh"]
del state["faker"] del state["faker"]
# TODO: Figure out why this is needed after adding neutral point support
if state["player"] != Player.NEUTRAL:
del state["_threat_zone"]
del state["_navmesh"]
return state return state
def __setstate__(self, state: dict[str, Any]) -> None: def __setstate__(self, state: dict[str, Any]) -> None:
# Migration: Convert old boolean player values to Player enum
if "player" in state and isinstance(state["player"], bool):
from game.theater.player import Player
if state["player"]:
state["player"] = Player.BLUE
else:
state["player"] = Player.RED
self.__dict__.update(state) self.__dict__.update(state)
# Regenerate any state that was not persisted. # Regenerate any state that was not persisted.
self.on_load() self.on_load()
@@ -203,7 +217,7 @@ class Coalition:
squadron.refund_orders() squadron.refund_orders()
def plan_missions(self, now: datetime) -> None: def plan_missions(self, now: datetime) -> None:
color = "Blue" if self.player else "Red" color = "Blue" if self.player.is_blue else "Red"
with MultiEventTracer() as tracer: with MultiEventTracer() as tracer:
with tracer.trace(f"{color} mission planning"): with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"): with tracer.trace(f"{color} mission identification"):
@@ -220,7 +234,7 @@ class Coalition:
# to ground forces and 1400 to aircraft. After that the budget will be spent # to ground forces and 1400 to aircraft. After that the budget will be spent
# proportionally based on how much is already invested. # proportionally based on how much is already invested.
if self.player: if self.player.is_blue:
manage_runways = self.game.settings.automate_runway_repair manage_runways = self.game.settings.automate_runway_repair
manage_front_line = self.game.settings.automate_front_line_reinforcements manage_front_line = self.game.settings.automate_front_line_reinforcements
manage_aircraft = self.game.settings.automate_aircraft_reinforcements manage_aircraft = self.game.settings.automate_aircraft_reinforcements

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import itertools
import math import math
import operator import operator
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
@@ -16,6 +17,7 @@ from game.theater import (
OffMapSpawn, OffMapSpawn,
ParkingType, ParkingType,
NavalControlPoint, NavalControlPoint,
Player,
) )
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
@@ -35,7 +37,7 @@ MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
class ObjectiveFinder: class ObjectiveFinder:
"""Identifies potential objectives for the mission planner.""" """Identifies potential objectives for the mission planner."""
def __init__(self, game: Game, is_player: bool) -> None: def __init__(self, game: Game, is_player: Player) -> None:
self.game = game self.game = game
self.is_player = is_player self.is_player = is_player
@@ -154,7 +156,7 @@ class ObjectiveFinder:
airfields_in_proximity = self.closest_airfields_to(cp) airfields_in_proximity = self.closest_airfields_to(cp)
airbase_threat_range = self.game.settings.airbase_threat_range airbase_threat_range = self.game.settings.airbase_threat_range
if ( if (
not self.is_player self.is_player.is_red
and randint(1, 100) and randint(1, 100)
> self.game.settings.opfor_autoplanner_aggressiveness > self.game.settings.opfor_autoplanner_aggressiveness
): ):
@@ -216,7 +218,7 @@ class ObjectiveFinder:
def farthest_friendly_control_point(self) -> ControlPoint: def farthest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is farthest from any threats.""" """Finds the friendly control point that is farthest from any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player) threat_zones = self.game.threat_zone_for(self.is_player.opponent)
farthest = None farthest = None
max_distance = meters(0) max_distance = meters(0)
@@ -234,7 +236,7 @@ class ObjectiveFinder:
def closest_friendly_control_point(self) -> ControlPoint: def closest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is closest to any threats.""" """Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player) threat_zones = self.game.threat_zone_for(self.is_player.opponent)
closest = None closest = None
min_distance = meters(math.inf) min_distance = meters(math.inf)
@@ -258,14 +260,37 @@ class ObjectiveFinder:
return ( return (
c c
for c in self.game.theater.controlpoints for c in self.game.theater.controlpoints
if not c.is_friendly(self.is_player) if not c.is_friendly(self.is_player) and c.captured != Player.NEUTRAL
) )
def prioritized_points(self) -> list[ControlPoint]: def prioritized_points(self) -> list[ControlPoint]:
prioritized = [] prioritized = []
capturable_later = [] capturable_later = []
isolated = [] isolated = []
for cp in self.game.theater.control_points_for(not self.is_player): for cp in self.game.theater.control_points_for(self.is_player.opponent):
if cp.is_isolated:
isolated.append(cp)
continue
if cp.has_active_frontline:
prioritized.append(cp)
else:
capturable_later.append(cp)
prioritized.extend(self._targets_by_range(capturable_later))
prioritized.extend(self._targets_by_range(isolated))
return prioritized
def air_assault_targets(self) -> list[ControlPoint]:
"""Returns control points suitable for air assault missions, including neutral bases."""
prioritized = []
capturable_later = []
isolated = []
combined_control_points = itertools.chain(
self.game.theater.control_points_for(self.is_player.opponent),
self.game.theater.control_points_for(Player.NEUTRAL),
)
for cp in combined_control_points:
if cp.is_isolated: if cp.is_isolated:
isolated.append(cp) isolated.append(cp)
continue continue

View File

@@ -43,7 +43,9 @@ class PackageFulfiller:
@property @property
def is_player(self) -> bool: def is_player(self) -> bool:
return self.coalition.player if self.coalition.player.is_blue:
return True
return False
@property @property
def ato(self) -> AirTaskingOrder: def ato(self) -> AirTaskingOrder:

View File

@@ -10,7 +10,7 @@ from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method from game.htn import CompoundTask, Method
from game.theater import ControlPoint, FrontLine from game.theater import ControlPoint, FrontLine, Player
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -26,12 +26,12 @@ class CaptureBase(CompoundTask[TheaterState]):
def enemy_cp(self, state: TheaterState) -> ControlPoint: def enemy_cp(self, state: TheaterState) -> ControlPoint:
return self.front_line.control_point_hostile_to(state.context.coalition.player) return self.front_line.control_point_hostile_to(state.context.coalition.player)
def units_deployable(self, state: TheaterState, player: bool) -> int: def units_deployable(self, state: TheaterState, player: Player) -> int:
cp = self.front_line.control_point_friendly_to(player) cp = self.front_line.control_point_friendly_to(player)
ammo_depots = list(state.ammo_dumps_at(cp)) ammo_depots = list(state.ammo_dumps_at(cp))
return cp.deployable_front_line_units_with(len(ammo_depots)) return cp.deployable_front_line_units_with(len(ammo_depots))
def unit_cap(self, state: TheaterState, player: bool) -> int: def unit_cap(self, state: TheaterState, player: Player) -> int:
cp = self.front_line.control_point_friendly_to(player) cp = self.front_line.control_point_friendly_to(player)
ammo_depots = list(state.ammo_dumps_at(cp)) ammo_depots = list(state.ammo_dumps_at(cp))
return cp.front_line_capacity_with(len(ammo_depots)) return cp.front_line_capacity_with(len(ammo_depots))

View File

@@ -11,10 +11,11 @@ from game.theater import FrontLine
if TYPE_CHECKING: if TYPE_CHECKING:
from game.coalition import Coalition from game.coalition import Coalition
from game.theater.player import Player
class FrontLineStanceTask(TheaterCommanderTask, ABC): class FrontLineStanceTask(TheaterCommanderTask, ABC):
def __init__(self, front_line: FrontLine, player: bool) -> None: def __init__(self, front_line: FrontLine, player: Player) -> None:
self.front_line = front_line self.front_line = front_line
self.friendly_cp = self.front_line.control_point_friendly_to(player) self.friendly_cp = self.front_line.control_point_friendly_to(player)
self.enemy_cp = self.front_line.control_point_hostile_to(player) self.enemy_cp = self.front_line.control_point_hostile_to(player)
@@ -26,7 +27,7 @@ class FrontLineStanceTask(TheaterCommanderTask, ABC):
@staticmethod @staticmethod
def management_allowed(state: TheaterState) -> bool: def management_allowed(state: TheaterState) -> bool:
return ( return (
not state.context.coalition.player not state.context.coalition.player.is_blue
or state.context.settings.automate_front_line_stance or state.context.settings.automate_front_line_stance
) )

View File

@@ -45,7 +45,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if ( if (
state.context.coalition.player state.context.coalition.player.is_blue
and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled
): ):
return False return False
@@ -102,7 +102,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
return 1 return 1
def fulfill_mission(self, state: TheaterState) -> bool: def fulfill_mission(self, state: TheaterState) -> bool:
color = "blue" if state.context.coalition.player else "red" color = "blue" if state.context.coalition.player.is_blue else "red"
self.propose_flights() self.propose_flights()
fulfiller = PackageFulfiller( fulfiller = PackageFulfiller(
state.context.coalition, state.context.coalition,
@@ -214,7 +214,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
settings = state.context.coalition.game.settings settings = state.context.coalition.game.settings
margin = 100 - ( margin = 100 - (
settings.ownfor_autoplanner_aggressiveness settings.ownfor_autoplanner_aggressiveness
if state.context.coalition.player if state.context.coalition.player.is_blue
else settings.opfor_autoplanner_aggressiveness else settings.opfor_autoplanner_aggressiveness
) )
threat_range = iads_threat.max_threat_range() * (margin / 100) threat_range = iads_threat.max_threat_range() * (margin / 100)

View File

@@ -12,7 +12,7 @@ from game.theater import MissionTarget
class PlanAewc(PackagePlanningTask[MissionTarget]): class PlanAewc(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if ( if (
state.context.coalition.player state.context.coalition.player.is_blue
and not state.context.settings.auto_ato_behavior_awacs and not state.context.settings.auto_ato_behavior_awacs
): ):
return False return False

View File

@@ -6,7 +6,7 @@ from game.ato.flighttype import FlightType
from game.commander.missionproposals import EscortType from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState from game.commander.theaterstate import TheaterState
from game.theater import FrontLine from game.theater import FrontLine, Player
@dataclass @dataclass
@@ -19,9 +19,8 @@ class PlanCas(PackagePlanningTask[FrontLine]):
# An exception is made for turn zero since that's not being truly planned, but # An exception is made for turn zero since that's not being truly planned, but
# just to determine what missions should be planned on turn 1 (when there *will* # just to determine what missions should be planned on turn 1 (when there *will*
# be ground units) and what aircraft should be ordered. # be ground units) and what aircraft should be ordered.
enemy_cp = self.target.control_point_friendly_to( player = state.context.coalition.player.opponent
player=not state.context.coalition.player enemy_cp = self.target.control_point_friendly_to(player)
)
if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0: if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0:
return False return False
return super().preconditions_met(state) return super().preconditions_met(state)

View File

@@ -14,7 +14,7 @@ MARGIN = 4 # assume 4 aircraft can land without refueling
class PlanRecovery(PackagePlanningTask[ControlPoint]): class PlanRecovery(PackagePlanningTask[ControlPoint]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if ( if (
state.context.coalition.player state.context.coalition.player.is_blue
and not state.context.settings.auto_ato_behavior_tankers and not state.context.settings.auto_ato_behavior_tankers
): ):
return False return False

View File

@@ -12,7 +12,7 @@ from game.theater import MissionTarget
class PlanRefueling(PackagePlanningTask[MissionTarget]): class PlanRefueling(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool: def preconditions_met(self, state: TheaterState) -> bool:
if ( if (
state.context.coalition.player state.context.coalition.player.is_blue
and not state.context.settings.auto_ato_behavior_tankers and not state.context.settings.auto_ato_behavior_tankers
): ):
return False return False

View File

@@ -56,7 +56,7 @@ https://en.wikipedia.org/wiki/Hierarchical_task_network
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from game.ato.starttype import StartType from game.ato.starttype import StartType
from game.commander.tasks.compound.nextaction import PlanNextAction from game.commander.tasks.compound.nextaction import PlanNextAction
@@ -67,10 +67,11 @@ from game.profiling import MultiEventTracer
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.theater.player import Player
class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]): class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
def __init__(self, game: Game, player: bool) -> None: def __init__(self, game: Game, player: Player) -> None:
super().__init__( super().__init__(
PlanNextAction( PlanNextAction(
aircraft_cold_start=game.settings.default_start_type is StartType.COLD aircraft_cold_start=game.settings.default_start_type is StartType.COLD
@@ -79,6 +80,18 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
self.game = game self.game = game
self.player = player self.player = player
def __setstate__(self, state: dict[str, Any]) -> None:
# Migration: Convert old boolean player values to Player enum
if "player" in state and isinstance(state["player"], bool):
from game.theater.player import Player
if state["player"]:
state["player"] = Player.BLUE
else:
state["player"] = Player.RED
self.__dict__.update(state)
def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None: def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
state = TheaterState.from_game(self.game, self.player, now, tracer) state = TheaterState.from_game(self.game, self.player, now, tracer)
while True: while True:

View File

@@ -20,6 +20,7 @@ from game.theater import (
ControlPoint, ControlPoint,
FrontLine, FrontLine,
MissionTarget, MissionTarget,
Player,
) )
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
@@ -152,11 +153,12 @@ class TheaterState(WorldState["TheaterState"]):
@classmethod @classmethod
def from_game( def from_game(
cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer cls, game: Game, player: Player, now: datetime, tracer: MultiEventTracer
) -> TheaterState: ) -> TheaterState:
coalition = game.coalition_for(player) coalition = game.coalition_for(player)
finder = ObjectiveFinder(game, player) finder = ObjectiveFinder(game, player)
ordered_capturable_points = finder.prioritized_points() ordered_capturable_points = finder.prioritized_points()
air_assault_capturable_points = finder.air_assault_targets()
context = PersistentContext( context = PersistentContext(
game.db, game.db,
@@ -176,7 +178,7 @@ class TheaterState(WorldState["TheaterState"]):
battle_postitions: Dict[ControlPoint, BattlePositions] = { battle_postitions: Dict[ControlPoint, BattlePositions] = {
cp: BattlePositions.for_control_point(cp) cp: BattlePositions.for_control_point(cp)
for cp in ordered_capturable_points for cp in air_assault_capturable_points
} }
vulnerable_control_points = [ vulnerable_control_points = [
@@ -213,8 +215,8 @@ class TheaterState(WorldState["TheaterState"]):
) )
), ),
strike_targets=list(finder.strike_targets()), strike_targets=list(finder.strike_targets()),
enemy_barcaps=list(game.theater.control_points_for(not player)), enemy_barcaps=list(game.theater.control_points_for(player.opponent)),
threat_zones=game.threat_zone_for(not player), threat_zones=game.threat_zone_for(player.opponent),
vulnerable_control_points=vulnerable_control_points, vulnerable_control_points=vulnerable_control_points,
control_point_priority_queue=ordered_capturable_points, control_point_priority_queue=ordered_capturable_points,
priority_cp=( priority_cp=(

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
from typing import Iterable, List, TYPE_CHECKING
from functools import cached_property
from dcs.mapping import Polygon
from dcs.triggers import TriggerZone, TriggerZoneCircular, TriggerZoneQuadPoint
from game.theater.theatergroundobject import NAME_BY_CATEGORY
if TYPE_CHECKING:
from dcs.mapping import Point
class ControlPointInfluenceRadiusError(RuntimeError):
"""Error for when there are insufficient conditions to create a ControlPointInfluenceRadius."""
pass
class ControlPointInfluenceRadius:
"""Store information about a scenery objective."""
def __init__(self, zone_def: TriggerZone, cp_name: str) -> None:
self.zone_def = zone_def
self.position = zone_def.position
self.cp_name = cp_name
@staticmethod
def from_trigger_zones(
trigger_zones: Iterable[TriggerZone],
) -> List[ControlPointInfluenceRadius]:
"""Define scenery objectives based on their encompassing blue/red circle."""
zone_definitions = []
cp_influence_zones = []
for zone in trigger_zones:
if ControlPointInfluenceRadius.is_red(zone):
zone_definitions.append(zone)
# For each objective definition.
for zone_def in zone_definitions:
if len(zone_def.properties) == 0:
raise ControlPointInfluenceRadiusError(
"Undefined ControlPointInfluenceRadius category in TriggerZone: "
+ zone_def.name
)
zone_def_cp_name = zone_def.properties[1].get("value")
cp_influence_zones.append(
ControlPointInfluenceRadius(zone_def, zone_def_cp_name)
)
return cp_influence_zones
@staticmethod
def is_red(zone: TriggerZone) -> bool:
# Red in RGB is [1 Red], [0 Green], [0 Blue]. Ignore the fourth position: Transparency.
return zone.color[1] == 1 and zone.color[2] == 0 and zone.color[3] == 0
def point_in_zone(zone: TriggerZone, pos: Point) -> bool:
if isinstance(zone, TriggerZoneCircular):
return zone.position.distance_to_point(pos) < zone.radius
elif isinstance(zone, TriggerZoneQuadPoint):
return Polygon(pos._terrain, zone.verticies).point_in_poly(pos)
raise RuntimeError(f"Invalid trigger-zone: {zone.name}")

View File

@@ -6,6 +6,7 @@ REQUIRED_BUILDINGS = [
"ammo", "ammo",
"factory", "factory",
"fob", "fob",
"invisiblefob",
"oil", "oil",
] ]

View File

@@ -60,6 +60,7 @@ class GroupTask(Enum):
FARP = ("Farp", GroupRole.BUILDING) FARP = ("Farp", GroupRole.BUILDING)
FOB = ("FOB", GroupRole.BUILDING) FOB = ("FOB", GroupRole.BUILDING)
FUEL = ("Fuel", GroupRole.BUILDING) FUEL = ("Fuel", GroupRole.BUILDING)
INVISIBLE_FOB = ("InvisibleFOB", GroupRole.BUILDING)
OFFSHORE_STRIKE_TARGET = ("OffShoreStrikeTarget", GroupRole.BUILDING) OFFSHORE_STRIKE_TARGET = ("OffShoreStrikeTarget", GroupRole.BUILDING)
OIL = ("Oil", GroupRole.BUILDING) OIL = ("Oil", GroupRole.BUILDING)

View File

@@ -123,6 +123,7 @@ class WeaponType(Enum):
TGP = "TGP" TGP = "TGP"
DECOY = "DECOY" DECOY = "DECOY"
JAMMER = "JAMMER" JAMMER = "JAMMER"
OFFENSIVE_JAMMER = "OFFENSIVE_JAMMER"
UNKNOWN = "unknown" UNKNOWN = "unknown"

View File

@@ -41,6 +41,9 @@ from game.radio.channels import (
WarthogChannelNamer, WarthogChannelNamer,
PhantomChannelNamer, PhantomChannelNamer,
KiowaChannelNamer, KiowaChannelNamer,
ARC5RadioChannelAllocator,
ARC5ChannelNamer,
FulcrumChannelNamer,
) )
from game.utils import ( from game.utils import (
Distance, Distance,
@@ -94,6 +97,7 @@ class RadioConfig:
return None return None
allocator_type: Type[RadioChannelAllocator] = { allocator_type: Type[RadioChannelAllocator] = {
"SCR-522": SCR522RadioChannelAllocator, "SCR-522": SCR522RadioChannelAllocator,
"ARC-5": ARC5RadioChannelAllocator,
"common": CommonRadioChannelAllocator, "common": CommonRadioChannelAllocator,
"farmer": FarmerRadioChannelAllocator, "farmer": FarmerRadioChannelAllocator,
"noop": NoOpChannelAllocator, "noop": NoOpChannelAllocator,
@@ -105,6 +109,7 @@ class RadioConfig:
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]: def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
return { return {
"SCR-522": SCR522ChannelNamer, "SCR-522": SCR522ChannelNamer,
"ARC-5": ARC5ChannelNamer,
"default": ChannelNamer, "default": ChannelNamer,
"huey": HueyChannelNamer, "huey": HueyChannelNamer,
"mirage": MirageChannelNamer, "mirage": MirageChannelNamer,
@@ -118,6 +123,7 @@ class RadioConfig:
"a10c-ii": WarthogChannelNamer, "a10c-ii": WarthogChannelNamer,
"phantom": PhantomChannelNamer, "phantom": PhantomChannelNamer,
"kiowa": KiowaChannelNamer, "kiowa": KiowaChannelNamer,
"fulcrum": FulcrumChannelNamer,
}[config.get("namer", "default")] }[config.get("namer", "default")]
@@ -229,6 +235,14 @@ class AircraftType(UnitType[Type[FlyingType]]):
# when no TGP is mounted on any station. # when no TGP is mounted on any station.
has_built_in_target_pod: bool has_built_in_target_pod: bool
# indicates if the aircraft has a built-in jammer allowing Defensive EWJamming to be used
# without the need for a jamming pod
has_built_in_ecm: bool
# Indicates if the aircraft has a built in jammer allowing Offensive EWJamming to be used
# without the need for a jamming pod
has_built_in_jamming: bool
task_priorities: dict[FlightType, int] task_priorities: dict[FlightType, int]
laser_code_configs: list[LaserCodeConfig] laser_code_configs: list[LaserCodeConfig]
@@ -591,6 +605,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
can_carry_crates=data.get("can_carry_crates", aircraft.helicopter), can_carry_crates=data.get("can_carry_crates", aircraft.helicopter),
task_priorities=task_priorities, task_priorities=task_priorities,
has_built_in_target_pod=data.get("has_built_in_target_pod", False), has_built_in_target_pod=data.get("has_built_in_target_pod", False),
has_built_in_ecm=data.get("has_built_in_ecm", False),
has_built_in_jamming=data.get("has_built_in_jamming", False),
laser_code_configs=[ laser_code_configs=[
LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", []) LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", [])
], ],

View File

@@ -16,7 +16,7 @@ from uuid import UUID
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater import Airfield, ControlPoint from game.theater import Airfield, ControlPoint, Player
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -45,9 +45,9 @@ class AirLosses:
def losses(self) -> Iterator[FlyingUnit]: def losses(self) -> Iterator[FlyingUnit]:
return itertools.chain(self.player, self.enemy) return itertools.chain(self.player, self.enemy)
def by_type(self, player: bool) -> Dict[AircraftType, int]: def by_type(self, player: Player) -> Dict[AircraftType, int]:
losses_by_type: Dict[AircraftType, int] = defaultdict(int) losses_by_type: Dict[AircraftType, int] = defaultdict(int)
losses = self.player if player else self.enemy losses = self.player if player.is_blue else self.enemy
for loss in losses: for loss in losses:
losses_by_type[loss.flight.unit_type] += 1 losses_by_type[loss.flight.unit_type] += 1
return losses_by_type return losses_by_type
@@ -87,7 +87,7 @@ class GroundLosses:
@dataclass(frozen=True) @dataclass(frozen=True)
class BaseCaptureEvent: class BaseCaptureEvent:
control_point: ControlPoint control_point: ControlPoint
captured_by_player: bool captured_by_player: Player
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -166,7 +166,7 @@ class Debriefing:
def merge_simulation_results(self, results: SimulationResults) -> None: def merge_simulation_results(self, results: SimulationResults) -> None:
for air_loss in results.air_losses: for air_loss in results.air_losses:
if air_loss.flight.squadron.player: if air_loss.flight.squadron.player.is_blue:
self.air_losses.player.append(air_loss) self.air_losses.player.append(air_loss)
else: else:
self.air_losses.enemy.append(air_loss) self.air_losses.enemy.append(air_loss)
@@ -209,9 +209,9 @@ class Debriefing:
def casualty_count(self, control_point: ControlPoint) -> int: def casualty_count(self, control_point: ControlPoint) -> int:
return len([x for x in self.front_line_losses if x.origin == control_point]) return len([x for x in self.front_line_losses if x.origin == control_point])
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: def front_line_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int) losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player: if player.is_blue:
losses = self.ground_losses.player_front_line losses = self.ground_losses.player_front_line
else: else:
losses = self.ground_losses.enemy_front_line losses = self.ground_losses.enemy_front_line
@@ -219,9 +219,9 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1 losses_by_type[loss.unit_type] += 1
return losses_by_type return losses_by_type
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: def convoy_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int) losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player: if player.is_blue:
losses = self.ground_losses.player_convoy losses = self.ground_losses.player_convoy
else: else:
losses = self.ground_losses.enemy_convoy losses = self.ground_losses.enemy_convoy
@@ -229,9 +229,9 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1 losses_by_type[loss.unit_type] += 1
return losses_by_type return losses_by_type
def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: def cargo_ship_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int) losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player: if player.is_blue:
ships = self.ground_losses.player_cargo_ships ships = self.ground_losses.player_cargo_ships
else: else:
ships = self.ground_losses.enemy_cargo_ships ships = self.ground_losses.enemy_cargo_ships
@@ -240,9 +240,9 @@ class Debriefing:
losses_by_type[unit_type] += count losses_by_type[unit_type] += count
return losses_by_type return losses_by_type
def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]: def airlift_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int) losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player: if player.is_blue:
losses = self.ground_losses.player_airlifts losses = self.ground_losses.player_airlifts
else: else:
losses = self.ground_losses.enemy_airlifts losses = self.ground_losses.enemy_airlifts
@@ -251,9 +251,9 @@ class Debriefing:
losses_by_type[unit_type] += 1 losses_by_type[unit_type] += 1
return losses_by_type return losses_by_type
def ground_object_losses_by_type(self, player: bool) -> Dict[str, int]: def ground_object_losses_by_type(self, player: Player) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int) losses_by_type: Dict[str, int] = defaultdict(int)
if player: if player.is_blue:
losses = self.ground_losses.player_ground_objects losses = self.ground_losses.player_ground_objects
else: else:
losses = self.ground_losses.enemy_ground_objects losses = self.ground_losses.enemy_ground_objects
@@ -261,9 +261,9 @@ class Debriefing:
losses_by_type[loss.theater_unit.type.id] += 1 losses_by_type[loss.theater_unit.type.id] += 1
return losses_by_type return losses_by_type
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]: def scenery_losses_by_type(self, player: Player) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int) losses_by_type: Dict[str, int] = defaultdict(int)
if player: if player.is_blue:
losses = self.ground_losses.player_scenery losses = self.ground_losses.player_scenery
else: else:
losses = self.ground_losses.enemy_scenery losses = self.ground_losses.enemy_scenery
@@ -279,7 +279,7 @@ class Debriefing:
if aircraft is None: if aircraft is None:
logging.error(f"Could not find Flight matching {unit_name}") logging.error(f"Could not find Flight matching {unit_name}")
continue continue
if aircraft.flight.departure.captured: if aircraft.flight.departure.captured.is_blue:
player_losses.append(aircraft) player_losses.append(aircraft)
else: else:
enemy_losses.append(aircraft) enemy_losses.append(aircraft)
@@ -290,7 +290,7 @@ class Debriefing:
for unit_name in self.state_data.killed_ground_units: for unit_name in self.state_data.killed_ground_units:
front_line_unit = self.unit_map.front_line_unit(unit_name) front_line_unit = self.unit_map.front_line_unit(unit_name)
if front_line_unit is not None: if front_line_unit is not None:
if front_line_unit.origin.captured: if front_line_unit.origin.captured.is_blue:
losses.player_front_line.append(front_line_unit) losses.player_front_line.append(front_line_unit)
else: else:
losses.enemy_front_line.append(front_line_unit) losses.enemy_front_line.append(front_line_unit)
@@ -298,7 +298,7 @@ class Debriefing:
convoy_unit = self.unit_map.convoy_unit(unit_name) convoy_unit = self.unit_map.convoy_unit(unit_name)
if convoy_unit is not None: if convoy_unit is not None:
if convoy_unit.convoy.player_owned: if convoy_unit.convoy.player_owned.is_blue:
losses.player_convoy.append(convoy_unit) losses.player_convoy.append(convoy_unit)
else: else:
losses.enemy_convoy.append(convoy_unit) losses.enemy_convoy.append(convoy_unit)
@@ -306,7 +306,7 @@ class Debriefing:
cargo_ship = self.unit_map.cargo_ship(unit_name) cargo_ship = self.unit_map.cargo_ship(unit_name)
if cargo_ship is not None: if cargo_ship is not None:
if cargo_ship.player_owned: if cargo_ship.player_owned.is_blue:
losses.player_cargo_ships.append(cargo_ship) losses.player_cargo_ships.append(cargo_ship)
else: else:
losses.enemy_cargo_ships.append(cargo_ship) losses.enemy_cargo_ships.append(cargo_ship)
@@ -314,7 +314,9 @@ class Debriefing:
ground_object = self.unit_map.theater_units(unit_name) ground_object = self.unit_map.theater_units(unit_name)
if ground_object is not None: if ground_object is not None:
if ground_object.theater_unit.ground_object.is_friendly(to_player=True): if ground_object.theater_unit.ground_object.is_friendly(
to_player=Player.BLUE
):
losses.player_ground_objects.append(ground_object) losses.player_ground_objects.append(ground_object)
else: else:
losses.enemy_ground_objects.append(ground_object) losses.enemy_ground_objects.append(ground_object)
@@ -323,7 +325,9 @@ class Debriefing:
scenery_object = self.unit_map.scenery_object(unit_name) scenery_object = self.unit_map.scenery_object(unit_name)
# Try appending object to the name, because we do this for building statics. # Try appending object to the name, because we do this for building statics.
if scenery_object is not None: if scenery_object is not None:
if scenery_object.ground_unit.ground_object.is_friendly(to_player=True): if scenery_object.ground_unit.ground_object.is_friendly(
to_player=Player.BLUE
):
losses.player_scenery.append(scenery_object) losses.player_scenery.append(scenery_object)
else: else:
losses.enemy_scenery.append(scenery_object) losses.enemy_scenery.append(scenery_object)
@@ -331,9 +335,9 @@ class Debriefing:
airfield = self.unit_map.airfield(unit_name) airfield = self.unit_map.airfield(unit_name)
if airfield is not None: if airfield is not None:
if airfield.captured: if airfield.captured.is_blue:
losses.player_airfields.append(airfield) losses.player_airfields.append(airfield)
else: elif airfield.captured.is_red:
losses.enemy_airfields.append(airfield) losses.enemy_airfields.append(airfield)
continue continue
@@ -349,7 +353,7 @@ class Debriefing:
for unit_name in self.state_data.killed_aircraft: for unit_name in self.state_data.killed_aircraft:
airlift_unit = self.unit_map.airlift_unit(unit_name) airlift_unit = self.unit_map.airlift_unit(unit_name)
if airlift_unit is not None: if airlift_unit is not None:
if airlift_unit.transfer.player: if airlift_unit.transfer.player.is_blue:
losses.player_airlifts.append(airlift_unit) losses.player_airlifts.append(airlift_unit)
else: else:
losses.enemy_airlifts.append(airlift_unit) losses.enemy_airlifts.append(airlift_unit)
@@ -377,8 +381,10 @@ class Debriefing:
# Captured base is not a part of the campaign. This happens when neutral # Captured base is not a part of the campaign. This happens when neutral
# bases are near the conflict. Nothing to do. # bases are near the conflict. Nothing to do.
continue continue
if int(new_owner_id_str) == blue_coalition_id:
captured_by_player = int(new_owner_id_str) == blue_coalition_id captured_by_player = Player.BLUE
else:
captured_by_player = Player.RED
if control_point.is_friendly(to_player=captured_by_player): if control_point.is_friendly(to_player=captured_by_player):
# Base is currently friendly to the new owner. Was captured and # Base is currently friendly to the new owner. Was captured and
# recaptured in the same mission. Nothing to do. # recaptured in the same mission. Nothing to do.

View File

@@ -865,6 +865,19 @@ class Faction:
# MiG-31BM # MiG-31BM
if not mod_settings.mig31bm_foxhound: if not mod_settings.mig31bm_foxhound:
self.remove_aircraft("MiG-31BM") self.remove_aircraft("MiG-31BM")
# Mlitary Aircraft Mod 1.7.1
if not mod_settings.mam:
self.remove_aircraft("A400M_Atlas")
self.remove_aircraft("B2_Spirit")
self.remove_aircraft("C2A_Greyhound")
self.remove_aircraft("C5_Galaxy")
self.remove_aircraft("KC_10_Extender")
self.remove_aircraft("KC_10_Extender_D")
self.remove_aircraft("P3C_Orion")
self.remove_aircraft("V22_Osprey")
# Tornado f.3 ADV (1.0)
if not mod_settings.tornado_adv:
self.remove_aircraft("Tornado_ADV")
def remove_aircraft(self, name: str) -> None: def remove_aircraft(self, name: str) -> None:
for aircraft_set in [self.aircraft, self.awacs, self.tankers]: for aircraft_set in [self.aircraft, self.awacs, self.tankers]:

View File

@@ -4,9 +4,10 @@ import itertools
import logging import logging
import math import math
from collections.abc import Iterator from collections.abc import Iterator
from copy import deepcopy
from datetime import date, datetime, time, timedelta from datetime import date, datetime, time, timedelta
from enum import Enum from enum import Enum
from typing import Any, List, TYPE_CHECKING, Type, Union, cast from typing import Any, List, TYPE_CHECKING, Union, cast
from uuid import UUID from uuid import UUID
from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers
@@ -32,7 +33,7 @@ from .infos.information import Information
from .lasercodes.lasercoderegistry import LaserCodeRegistry from .lasercodes.lasercoderegistry import LaserCodeRegistry
from .profiling import logged_duration from .profiling import logged_duration
from .settings import Settings from .settings import Settings
from .theater import ConflictTheater from .theater import ConflictTheater, Player
from .theater.bullseye import Bullseye from .theater.bullseye import Bullseye
from .theater.theatergroundobject import ( from .theater.theatergroundobject import (
EwrGroundObject, EwrGroundObject,
@@ -138,8 +139,11 @@ class Game:
self.conditions = self.generate_conditions(forced_time=start_time) self.conditions = self.generate_conditions(forced_time=start_time)
self.sanitize_sides(player_faction, enemy_faction) self.sanitize_sides(player_faction, enemy_faction)
self.blue = Coalition(self, player_faction, player_budget, player=True) self.blue = Coalition(self, player_faction, player_budget, player=Player.BLUE)
self.red = Coalition(self, enemy_faction, enemy_budget, player=False) self.red = Coalition(self, enemy_faction, enemy_budget, player=Player.RED)
neutral_faction = deepcopy(player_faction)
neutral_faction.country = self.neutral_country
self.neutral = Coalition(self, neutral_faction, 0, player=Player.NEUTRAL)
self.blue.set_opponent(self.red) self.blue.set_opponent(self.red)
self.red.set_opponent(self.blue) self.red.set_opponent(self.blue)
@@ -178,10 +182,10 @@ class Game:
def point_in_world(self, x: float, y: float) -> Point: def point_in_world(self, x: float, y: float) -> Point:
return Point(x, y, self.theater.terrain) return Point(x, y, self.theater.terrain)
def ato_for(self, player: bool) -> AirTaskingOrder: def ato_for(self, player: Player) -> AirTaskingOrder:
return self.coalition_for(player).ato return self.coalition_for(player).ato
def transit_network_for(self, player: bool) -> TransitNetwork: def transit_network_for(self, player: Player) -> TransitNetwork:
return self.coalition_for(player).transit_network return self.coalition_for(player).transit_network
def generate_conditions(self, forced_time: time | None = None) -> Conditions: def generate_conditions(self, forced_time: time | None = None) -> Conditions:
@@ -209,32 +213,35 @@ class Game:
else: else:
enemy_faction.country = country_with_name("Russia") enemy_faction.country = country_with_name("Russia")
def faction_for(self, player: bool) -> Faction: def faction_for(self, player: Player) -> Faction:
return self.coalition_for(player).faction return self.coalition_for(player).faction
def faker_for(self, player: bool) -> Faker: def faker_for(self, player: Player) -> Faker:
return self.coalition_for(player).faker return self.coalition_for(player).faker
def air_wing_for(self, player: bool) -> AirWing: def air_wing_for(self, player: Player) -> AirWing:
return self.coalition_for(player).air_wing return self.coalition_for(player).air_wing
@property @property
def neutral_country(self) -> Type[Country]: def neutral_country(self) -> Country:
"""Return the best fitting country that can be used as neutral faction in the generated mission""" """Return the best fitting country that can be used as neutral faction in the generated mission"""
countries_in_use = {self.red.faction.country, self.blue.faction.country} countries_in_use = {self.red.faction.country, self.blue.faction.country}
if UnitedNationsPeacekeepers not in countries_in_use: if UnitedNationsPeacekeepers() not in countries_in_use:
return UnitedNationsPeacekeepers return UnitedNationsPeacekeepers()
elif Switzerland.name not in countries_in_use: elif Switzerland() not in countries_in_use:
return Switzerland return Switzerland()
else: else:
return USAFAggressors return USAFAggressors()
def coalition_for(self, player: bool) -> Coalition: def coalition_for(self, player: Player) -> Coalition:
if player: if player.is_neutral:
return self.neutral
elif player.is_blue:
return self.blue return self.blue
return self.red else:
return self.red
def adjust_budget(self, amount: float, player: bool) -> None: def adjust_budget(self, amount: float, player: Player) -> None:
self.coalition_for(player).adjust_budget(amount) self.coalition_for(player).adjust_budget(amount)
def on_load(self, game_still_initializing: bool = False) -> None: def on_load(self, game_still_initializing: bool = False) -> None:
@@ -489,7 +496,7 @@ class Game:
self.current_group_id += 1 self.current_group_id += 1
return self.current_group_id return self.current_group_id
def compute_transit_network_for(self, player: bool) -> TransitNetwork: def compute_transit_network_for(self, player: Player) -> TransitNetwork:
return TransitNetworkBuilder(self.theater, player).build() return TransitNetworkBuilder(self.theater, player).build()
def compute_threat_zones(self, events: GameUpdateEvents) -> None: def compute_threat_zones(self, events: GameUpdateEvents) -> None:
@@ -498,10 +505,10 @@ class Game:
self.blue.compute_nav_meshes(events) self.blue.compute_nav_meshes(events)
self.red.compute_nav_meshes(events) self.red.compute_nav_meshes(events)
def threat_zone_for(self, player: bool) -> ThreatZones: def threat_zone_for(self, player: Player) -> ThreatZones:
return self.coalition_for(player).threat_zone return self.coalition_for(player).threat_zone
def navmesh_for(self, player: bool) -> NavMesh: def navmesh_for(self, player: Player) -> NavMesh:
return self.coalition_for(player).nav_mesh return self.coalition_for(player).nav_mesh
def compute_unculled_zones(self, events: GameUpdateEvents) -> None: def compute_unculled_zones(self, events: GameUpdateEvents) -> None:

View File

@@ -66,7 +66,7 @@ class GroundUnitOrders:
bought_units: dict[GroundUnitType, int] = {} bought_units: dict[GroundUnitType, int] = {}
units_needing_transfer: dict[GroundUnitType, int] = {} units_needing_transfer: dict[GroundUnitType, int] = {}
for unit_type, count in self.units.items(): for unit_type, count in self.units.items():
allegiance = "Ally" if self.destination.captured else "Enemy" allegiance = "Ally" if self.destination.captured.is_blue else "Enemy"
d: dict[GroundUnitType, int] d: dict[GroundUnitType, int]
if self.destination != ground_unit_source: if self.destination != ground_unit_source:
source = ground_unit_source source = ground_unit_source

View File

@@ -4,6 +4,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from game.config import REWARDS from game.config import REWARDS
from game.theater.player import Player
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -22,8 +23,8 @@ class BuildingIncome:
class Income: class Income:
def __init__(self, game: Game, player: bool) -> None: def __init__(self, game: Game, player: Player) -> None:
if player: if player.is_blue:
self.multiplier = game.settings.player_income_multiplier self.multiplier = game.settings.player_income_multiplier
else: else:
self.multiplier = game.settings.enemy_income_multiplier self.multiplier = game.settings.enemy_income_multiplier

View File

@@ -288,6 +288,8 @@ class BuildingLayout(TgoLayout):
@property @property
def category(self) -> str: def category(self) -> str:
for task in self.tasks: for task in self.tasks:
if task is GroupTask.INVISIBLE_FOB:
return "fob"
if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]: if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]:
return task.description.lower() return task.description.lower()
raise RuntimeError(f"Building Template {self.name} has no building category") raise RuntimeError(f"Building Template {self.name} has no building category")

View File

@@ -192,7 +192,8 @@ class AircraftBehavior:
@staticmethod @staticmethod
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None: def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
if flight.unit_type.eplrs_capable: eplrs_enabled = flight.coalition.game.settings.eplrs_enabled
if eplrs_enabled and flight.unit_type.eplrs_capable:
group.points[0].tasks.append(EPLRS(group.id)) group.points[0].tasks.append(EPLRS(group.id))
def configure_cap(self, group: FlyingGroup[Any], flight: Flight) -> None: def configure_cap(self, group: FlyingGroup[Any], flight: Flight) -> None:
@@ -306,7 +307,7 @@ class AircraftBehavior:
) )
def configure_oca_strike(self, group: FlyingGroup[Any], flight: Flight) -> None: def configure_oca_strike(self, group: FlyingGroup[Any], flight: Flight) -> None:
self.configure_task(flight, group, CAS, [AFAC, SEAD]) self.configure_task(flight, group, CAS, [AFAC, SEAD, AntishipStrike])
self.configure_behavior( self.configure_behavior(
flight, flight,
group, group,

View File

@@ -13,6 +13,7 @@ from dcs.datalinks.datalink import DataLinkType
from dcs.datalinks.datalinkbase import DataLinkSettingsWithFlightLead from dcs.datalinks.datalinkbase import DataLinkSettingsWithFlightLead
from dcs.datalinks.link16 import Link16Network, ViperLink16NetworkMemberLink from dcs.datalinks.link16 import Link16Network, ViperLink16NetworkMemberLink
from dcs.mission import Mission from dcs.mission import Mission
from dcs.task import RunScript
from dcs.terrain.terrain import NoParkingSlotError from dcs.terrain.terrain import NoParkingSlotError
from dcs.triggers import TriggerOnce, Event from dcs.triggers import TriggerOnce, Event
from dcs.unit import Skill from dcs.unit import Skill
@@ -103,6 +104,23 @@ class AircraftGenerator:
for parking_slot in cp.parking_slots: for parking_slot in cp.parking_slots:
parking_slot.unit_id = None parking_slot.unit_id = None
def _prioritized_packages(self, ato: AirTaskingOrder) -> List[Package]:
"""Returns the packages in the order they should be generated."""
return sorted(
ato.packages,
key=lambda p: (
(
1
if any(
f.flight_type in [FlightType.AEWC, FlightType.REFUELING]
for f in p.flights
)
else 0
),
p.time_over_target,
),
)
def generate_flights( def generate_flights(
self, self,
country: Country, country: Country,
@@ -123,7 +141,7 @@ class AircraftGenerator:
self._reserve_frequencies_and_tacan(ato) self._reserve_frequencies_and_tacan(ato)
self.mission_data.packages.clear() self.mission_data.packages.clear()
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)): for package in reversed(self._prioritized_packages(ato)):
logging.info(f"Generating package for target: {package.target.name}") logging.info(f"Generating package for target: {package.target.name}")
if not package.flights: if not package.flights:
continue continue
@@ -202,7 +220,7 @@ class AircraftGenerator:
): ):
continue continue
if control_point.captured: if control_point.captured.is_blue:
country = player_country country = player_country
else: else:
country = enemy_country country = enemy_country
@@ -219,12 +237,12 @@ class AircraftGenerator:
squadron.location, Fob squadron.location, Fob
) )
if ( if (
squadron.coalition.player squadron.coalition.player.is_blue
and self.game.settings.perf_disable_untasked_blufor_aircraft and self.game.settings.perf_disable_untasked_blufor_aircraft
): ):
return return
elif ( elif (
not squadron.coalition.player squadron.coalition.player.is_red
and self.game.settings.perf_disable_untasked_opfor_aircraft and self.game.settings.perf_disable_untasked_opfor_aircraft
): ):
return return
@@ -256,7 +274,7 @@ class AircraftGenerator:
).create_idle_aircraft() ).create_idle_aircraft()
if group: if group:
if ( if (
not squadron.coalition.player squadron.coalition.player.is_red
and squadron.aircraft.flyable and squadron.aircraft.flyable
and ( and (
self.game.settings.enable_squadron_pilot_limits self.game.settings.enable_squadron_pilot_limits
@@ -341,16 +359,51 @@ class AircraftGenerator:
def _track_ewrj_flight(self, flight: Flight, group: FlyingGroup[Any]) -> None: def _track_ewrj_flight(self, flight: Flight, group: FlyingGroup[Any]) -> None:
if not self.ewrj_package_dict.get(id(flight.package)): if not self.ewrj_package_dict.get(id(flight.package)):
self.ewrj_package_dict[id(flight.package)] = [] self.ewrj_package_dict[id(flight.package)] = []
added = False
if ( if (
flight.package.primary_flight not flight.flight_type.is_air_to_air
and flight is flight.package.primary_flight and any(
or flight.client_count [
wpt
for wpt in group.points
if wpt.name in ["JOIN", "SPLIT", "RACETRACK START", "RACETRACK END"]
and any(
[
t
for t in wpt.tasks
if isinstance(t, RunScript)
and (
"Djamming" in t.params["action"]["params"]["command"]
or "EWjamm" in t.params["action"]["params"]["command"]
)
]
)
]
)
and ( and (
not self.need_ecm not self.need_ecm
or flight.any_member_has_weapon_of_type(WeaponType.JAMMER) or flight.any_member_has_weapon_of_type(WeaponType.JAMMER)
or flight.any_member_has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
or flight.squadron.aircraft.has_built_in_ecm
or flight.squadron.aircraft.has_built_in_jamming
) )
): ):
self.ewrj_package_dict[id(flight.package)].append(group) self.ewrj_package_dict[id(flight.package)].append(group)
added = True
if (
added
or not flight.flight_type.is_air_to_air
and self.ewrj_package_dict[id(flight.package)]
):
for f in flight.package.flights:
if f is flight or f.group_id == 0 or f.flight_type.is_air_to_air:
continue
g = self.mission.find_group_by_id(f.group_id)
if (
isinstance(g, FlyingGroup)
and g not in self.ewrj_package_dict[id(flight.package)]
):
self.ewrj_package_dict[id(flight.package)].append(g)
def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None: def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None:
for package in ato.packages: for package in ato.packages:

View File

@@ -14,6 +14,7 @@ if TYPE_CHECKING:
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.radio.radios import RadioFrequency from game.radio.radios import RadioFrequency
from game.runways import RunwayData from game.runways import RunwayData
from game.theater.player import Player
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -42,7 +43,7 @@ class FlightData:
size: int size: int
#: True if this flight belongs to the player's coalition. #: True if this flight belongs to the player's coalition.
friendly: bool friendly: Player
#: Number of seconds after mission start the flight is set to depart. #: Number of seconds after mission start the flight is set to depart.
departure_delay: timedelta departure_delay: timedelta

View File

@@ -185,10 +185,21 @@ class FlightGroupConfigurator:
if not member.is_player: if not member.is_player:
return return
settings = self.flight.coalition.game.settings settings = self.flight.coalition.game.settings
if not settings.plugin_option("ewrj"):
return
# Check if ecm_required option is enabled # Check if ecm_required option is enabled
jammer_required = settings.plugin_option("ewrj.ecm_required") jammer_required = settings.plugin_option("ewrj.ecm_required")
has_jammer = member.loadout.has_weapon_of_type(WeaponType.JAMMER) offensive_jammer = member.loadout.has_weapon_of_type(
if jammer_required and not has_jammer: WeaponType.OFFENSIVE_JAMMER
)
offensive_inbuilt = self.flight.squadron.aircraft.has_built_in_jamming
has_jammer = (
member.loadout.has_weapon_of_type(WeaponType.JAMMER) or offensive_jammer
)
built_in_jammer = (
self.flight.squadron.aircraft.has_built_in_ecm or offensive_inbuilt
)
if jammer_required and not (has_jammer or built_in_jammer):
return return
# Create the original ewrj_menu_trigger for player flight members # Create the original ewrj_menu_trigger for player flight members
ewrj_menu_trigger = TriggerStart(comment=f"EWRJ-{unit.name}") ewrj_menu_trigger = TriggerStart(comment=f"EWRJ-{unit.name}")
@@ -197,6 +208,20 @@ class FlightGroupConfigurator:
self.group.points[0].tasks[0] = OptReactOnThreat( self.group.points[0].tasks[0] = OptReactOnThreat(
OptReactOnThreat.Values.PassiveDefense OptReactOnThreat.Values.PassiveDefense
) )
# Create LUA Flags for Offensive Jamming in EW Script for Player Flights
if not (offensive_jammer or offensive_inbuilt):
return
ewrj_offensive_trigger = TriggerStart(
comment=f"Offensive Jammer Flag {unit.name}"
)
ewrj_offensive_trigger.add_action(
DoScript(
String(
f'trigger.action.setUserFlag("offensive_jamming_{unit.name}", 1)'
)
)
)
self.mission.triggerrules.triggers.append(ewrj_offensive_trigger)
def setup_radios(self) -> RadioFrequency: def setup_radios(self) -> RadioFrequency:
freq = self.flight.frequency freq = self.flight.frequency
@@ -274,7 +299,7 @@ class FlightGroupConfigurator:
unit.set_player() unit.set_player()
def skill_level_for(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> Skill: def skill_level_for(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> Skill:
if self.flight.squadron.player: if self.flight.squadron.player.is_blue:
base_skill = Skill(self.game.settings.player_skill) base_skill = Skill(self.game.settings.player_skill)
else: else:
base_skill = Skill(self.game.settings.enemy_skill) base_skill = Skill(self.game.settings.enemy_skill)

View File

@@ -9,13 +9,10 @@ from dcs.task import (
OptFormation, OptFormation,
Targets, Targets,
SetUnlimitedFuelCommand, SetUnlimitedFuelCommand,
RunScript,
OptReactOnThreat,
) )
from game.ato import FlightType from game.ato import FlightType
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.data.weapons import WeaponType
from game.theater import NavalControlPoint from game.theater import NavalControlPoint
from game.utils import nautical_miles, feet from game.utils import nautical_miles, feet
from .pydcswaypointbuilder import PydcsWaypointBuilder from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -63,8 +60,14 @@ class JoinPointBuilder(PydcsWaypointBuilder):
FlightType.SEAD_SWEEP, FlightType.SEAD_SWEEP,
FlightType.SEAD, FlightType.SEAD,
FlightType.SEAD_ESCORT, FlightType.SEAD_ESCORT,
FlightType.DEAD,
]: ]:
self.start_defensive_jamming(waypoint) settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
self.offensive_jamming(waypoint, "start")
self.defensive_jamming(waypoint, "start")
if self.flight.flight_type == FlightType.SEAD_ESCORT: if self.flight.flight_type == FlightType.SEAD_ESCORT:
self.handle_sead_escort(doctrine, waypoint) self.handle_sead_escort(doctrine, waypoint)
# Let the AI use ECM to preemptively defend themselves. # Let the AI use ECM to preemptively defend themselves.
@@ -104,28 +107,6 @@ class JoinPointBuilder(PydcsWaypointBuilder):
vertical_spacing=doctrine.sead_escort_spacing.feet, vertical_spacing=doctrine.sead_escort_spacing.feet,
) )
def start_defensive_jamming(self, waypoint: MovingPoint) -> None:
# Start Defensive Jamming
settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
ecm_required = settings.plugin_option("ewrj.ecm_required")
has_jammer = False
for unit, member in zip(self.group.units, self.flight.iter_members()):
has_jammer = member.loadout.has_weapon_of_type(WeaponType.JAMMER)
if ecm_required and not has_jammer:
continue
if not member.is_player:
script_content = f'startDjamming("{unit.name}")'
start_jamming_script = RunScript(script_content)
waypoint.tasks.append(start_jamming_script)
has_jammer = True
if has_jammer:
passive_defense = OptReactOnThreat(
OptReactOnThreat.Values.PassiveDefense
)
waypoint.tasks.append(passive_defense)
def configure_escort_tasks( def configure_escort_tasks(
self, self,
waypoint: MovingPoint, waypoint: MovingPoint,

View File

@@ -13,6 +13,7 @@ from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType from game.ato.starttype import StartType
from game.ato.traveltime import GroundSpeed from game.ato.traveltime import GroundSpeed
from game.data.weapons import WeaponType
from game.missiongenerator.missiondata import MissionData from game.missiongenerator.missiondata import MissionData
from game.theater import MissionTarget, TheaterUnit, OffMapSpawn from game.theater import MissionTarget, TheaterUnit, OffMapSpawn
@@ -157,3 +158,36 @@ class PydcsWaypointBuilder:
self.group.units[0].unit_type in (F_14A_135_GR, F_14B) self.group.units[0].unit_type in (F_14A_135_GR, F_14B)
): ):
self.group.add_nav_target_point(self.waypoint.position, "IP") self.group.add_nav_target_point(self.waypoint.position, "IP")
def defensive_jamming(self, waypoint: MovingPoint, action: str) -> None:
# Explodes incoming missiles within the jamming bubble through the EW-Jamming script
settings = self.flight.coalition.game.settings
ecm_required = settings.plugin_option("ewrj.ecm_required")
for unit, member in zip(self.group.units, self.flight.iter_members()):
has_jammer = member.loadout.has_weapon_of_type(
WeaponType.JAMMER
) or member.loadout.has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
built_in_jammer = (
self.flight.squadron.aircraft.has_built_in_ecm
or self.flight.squadron.aircraft.has_built_in_jamming
)
if ecm_required and not (has_jammer or built_in_jammer):
continue
if not member.is_player:
script_content = f'{action}IAdefjamming("{unit.name}")'
jamming_script = RunScript(script_content)
waypoint.tasks.append(jamming_script)
def offensive_jamming(self, waypoint: MovingPoint, action: str) -> None:
# Silences enemy radars through the EW-Jamming script
settings = self.flight.coalition.game.settings
ecm_required = settings.plugin_option("ewrj.ecm_required")
for unit, member in zip(self.group.units, self.flight.iter_members()):
has_jammer = member.loadout.has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
built_in_jammer = self.flight.squadron.aircraft.has_built_in_jamming
if ecm_required and not (has_jammer or built_in_jammer):
continue
if not member.is_player:
script_content = f'{action}EWjamm("{unit.name}")'
stop_jamming_script = RunScript(script_content)
waypoint.tasks.append(stop_jamming_script)

View File

@@ -9,8 +9,6 @@ from dcs.task import (
Tanker, Tanker,
Targets, Targets,
SetUnlimitedFuelCommand, SetUnlimitedFuelCommand,
RunScript,
OptReactOnThreat,
) )
from game.ato import FlightType from game.ato import FlightType
@@ -40,21 +38,12 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
) )
return return
if self.flight.flight_type == FlightType.AEWC: # Start Defensive Jamming for all flights
# Start Offensive Jamming for all AWACS flights settings = self.flight.coalition.game.settings
settings = self.flight.coalition.game.settings ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled") if settings.plugins.get("ewrj") and ai_jammer:
if settings.plugins.get("ewrj") and ai_jammer: self.defensive_jamming(waypoint, "start")
# all units in group are AWACS, no specific checks needed self.offensive_jamming(waypoint, "start")
for unit, member in zip(self.group.units, self.flight.iter_members()):
script_content = f'startEWjamm("{unit.name}")'
start_jamming_script = RunScript(script_content)
waypoint.tasks.append(start_jamming_script)
passive_defense = OptReactOnThreat(
OptReactOnThreat.Values.PassiveDefense
)
waypoint.tasks.append(passive_defense)
# NB: It's important that the engage task comes before the orbit task. # NB: It's important that the engage task comes before the orbit task.
# Though they're on the same waypoint, if the orbit task comes first it # Though they're on the same waypoint, if the orbit task comes first it

View File

@@ -3,8 +3,6 @@ import logging
from dcs.point import MovingPoint from dcs.point import MovingPoint
from dcs.task import ( from dcs.task import (
SetUnlimitedFuelCommand, SetUnlimitedFuelCommand,
RunScript,
OptReactOnThreat,
) )
from game.ato import FlightType from game.ato import FlightType
@@ -20,18 +18,12 @@ class RaceTrackEndBuilder(PydcsWaypointBuilder):
# Disable Offensive Jamming at Racetrack End # Disable Offensive Jamming at Racetrack End
if self.flight.flight_type == FlightType.AEWC: if self.flight.flight_type == FlightType.AEWC:
# Stop Offensive Jamming for all AWACS flights # Stop Defensive Jamming for all AWACS flights
settings = self.flight.coalition.game.settings settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled") ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer: if settings.plugins.get("ewrj") and ai_jammer:
# all units in group are AWACS, no specific checks needed self.defensive_jamming(waypoint, "stop")
for unit, member in zip(self.group.units, self.flight.iter_members()): self.offensive_jamming(waypoint, "stop")
script_content = f'stopEWjamming("{unit.name}")'
stop_jamming_script = RunScript(script_content)
waypoint.tasks.append(stop_jamming_script)
evade_fire = OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
waypoint.tasks.append(evade_fire)
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()

View File

@@ -62,6 +62,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
# into the SAM instead of waiting for it to come alive # into the SAM instead of waiting for it to come alive
engage_task = EngageGroup(miz_group.id) engage_task = EngageGroup(miz_group.id)
engage_task.params["weaponType"] = DcsWeaponType.ARM.value engage_task.params["weaponType"] = DcsWeaponType.ARM.value
engage_task.params["expend"] = Expend.All.value
waypoint.tasks.append(engage_task) waypoint.tasks.append(engage_task)
# Use other Air-to-Surface Missiles at last # Use other Air-to-Surface Missiles at last

View File

@@ -5,11 +5,9 @@ from dcs.task import (
SetUnlimitedFuelCommand, SetUnlimitedFuelCommand,
SwitchWaypoint, SwitchWaypoint,
RunScript, RunScript,
OptReactOnThreat,
) )
from game.ato import FlightType from game.ato import FlightType
from game.data.weapons import WeaponType
from game.utils import knots from game.utils import knots
from .pydcswaypointbuilder import PydcsWaypointBuilder from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -49,28 +47,15 @@ class SplitPointBuilder(PydcsWaypointBuilder):
FlightType.SEAD_SWEEP, FlightType.SEAD_SWEEP,
FlightType.SEAD, FlightType.SEAD,
FlightType.SEAD_ESCORT, FlightType.SEAD_ESCORT,
FlightType.DEAD,
]: ]:
if self.flight.flight_type == FlightType.SEAD_ESCORT: if self.flight.flight_type == FlightType.SEAD_ESCORT:
# Moved previous escort split tasks # Moved previous escort split tasks
if self.flight.flight_type.is_escort_type: if self.flight.flight_type.is_escort_type:
index = len(self.group.points) index = len(self.group.points)
self.group.add_trigger_action(SwitchWaypoint(None, index)) self.group.add_trigger_action(SwitchWaypoint(None, index))
self.stop_defensive_jamming(waypoint) settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
def stop_defensive_jamming(self, waypoint: MovingPoint) -> None: if settings.plugins.get("ewrj") and ai_jammer:
# Stop Defensive Jamming self.offensive_jamming(waypoint, "stop")
settings = self.flight.coalition.game.settings self.defensive_jamming(waypoint, "stop")
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
for unit, member in zip(self.group.units, self.flight.iter_members()):
if settings.plugin_option("ewrj.ecm_required"):
ecm = WeaponType.JAMMER
if not member.loadout.has_weapon_of_type(ecm):
continue
if not member.is_player:
script_content = f'stopDjamming("{unit.name}")'
stop_jamming_script = RunScript(script_content)
waypoint.tasks.append(stop_jamming_script)
evaide_fire = OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
waypoint.tasks.append(evaide_fire)

View File

@@ -17,8 +17,8 @@ from game.radio.radios import RadioFrequency
from game.runways import RunwayData from game.runways import RunwayData
from game.theater import ControlPoint, FrontLine from game.theater import ControlPoint, FrontLine
from .aircraft.flightdata import FlightData from .aircraft.flightdata import FlightData
from .missiondata import AwacsInfo, TankerInfo
from .flotgenerator import JtacInfo from .flotgenerator import JtacInfo
from .missiondata import AwacsInfo, TankerInfo
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -182,7 +182,7 @@ class BriefingGenerator(MissionInfoGenerator):
def generate_allied_flights_by_departure(self) -> None: def generate_allied_flights_by_departure(self) -> None:
"""Create iterable to display allied flights grouped by departure airfield.""" """Create iterable to display allied flights grouped by departure airfield."""
for flight in self.flights: for flight in self.flights:
if not flight.client_units and flight.friendly: if not flight.client_units and flight.friendly.is_blue:
name = flight.departure.airfield_name name = flight.departure.airfield_name
if ( if (
name in self.allied_flights_by_departure name in self.allied_flights_by_departure

View File

@@ -18,6 +18,7 @@ from game.utils import kph
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.theater.player import Player
class ConvoyGenerator: class ConvoyGenerator:
@@ -94,7 +95,7 @@ class ConvoyGenerator:
name: str, name: str,
position: Point, position: Point,
units: dict[GroundUnitType, int], units: dict[GroundUnitType, int],
for_player: bool, for_player: Player,
) -> VehicleGroup: ) -> VehicleGroup:
unit_types = list(units.items()) unit_types = list(units.items())
main_unit_type, main_unit_count = unit_types[0] main_unit_type, main_unit_count = unit_types[0]

View File

@@ -15,6 +15,7 @@ FRONTLINE_COLORS = Rgba(255, 0, 0, 255)
WHITE = Rgba(255, 255, 255, 255) WHITE = Rgba(255, 255, 255, 255)
CP_RED = Rgba(255, 0, 0, 80) CP_RED = Rgba(255, 0, 0, 80)
CP_BLUE = Rgba(0, 0, 255, 80) CP_BLUE = Rgba(0, 0, 255, 80)
CP_NEUTRAL = Rgba(128, 128, 128, 80)
BLUE_PATH_COLOR = Rgba(0, 0, 255, 100) BLUE_PATH_COLOR = Rgba(0, 0, 255, 100)
RED_PATH_COLOR = Rgba(255, 0, 0, 100) RED_PATH_COLOR = Rgba(255, 0, 0, 100)
ACTIVE_PATH_COLOR = Rgba(255, 80, 80, 100) ACTIVE_PATH_COLOR = Rgba(255, 80, 80, 100)
@@ -35,10 +36,12 @@ class DrawingsGenerator:
Generate cps as circles Generate cps as circles
""" """
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
if cp.captured: if cp.captured.is_blue:
color = CP_BLUE color = CP_BLUE
else: elif cp.captured.is_red:
color = CP_RED color = CP_RED
else:
color = CP_NEUTRAL
shape = self.player_layer.add_circle( shape = self.player_layer.add_circle(
cp.position, cp.position,
TRIGGER_RADIUS_CAPTURE, TRIGGER_RADIUS_CAPTURE,
@@ -61,9 +64,9 @@ class DrawingsGenerator:
continue continue
else: else:
# Determine path color # Determine path color
if cp.captured and destination.captured: if cp.captured.is_blue and destination.captured.is_blue:
color = BLUE_PATH_COLOR color = BLUE_PATH_COLOR
elif not cp.captured and not destination.captured: elif cp.captured.is_red and destination.captured.is_red:
color = RED_PATH_COLOR color = RED_PATH_COLOR
else: else:
color = ACTIVE_PATH_COLOR color = ACTIVE_PATH_COLOR

View File

@@ -40,7 +40,7 @@ from game.ground_forces.ai_ground_planner import (
from game.ground_forces.combat_stance import CombatStance from game.ground_forces.combat_stance import CombatStance
from game.naming import namegen from game.naming import namegen
from game.radio.radios import RadioRegistry from game.radio.radios import RadioRegistry
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint, Player
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import Heading from game.utils import Heading
from .frontlineconflictdescription import FrontLineConflictDescription from .frontlineconflictdescription import FrontLineConflictDescription
@@ -101,12 +101,12 @@ class FlotGenerator:
# Create player groups at random position # Create player groups at random position
player_groups = self._generate_groups( player_groups = self._generate_groups(
self.player_planned_combat_groups, is_player=True self.player_planned_combat_groups, is_player=Player.BLUE
) )
# Create enemy groups at random position # Create enemy groups at random position
enemy_groups = self._generate_groups( enemy_groups = self._generate_groups(
self.enemy_planned_combat_groups, is_player=False self.enemy_planned_combat_groups, is_player=Player.RED
) )
# TODO: Differentiate AirConflict and GroundConflict classes. # TODO: Differentiate AirConflict and GroundConflict classes.
@@ -193,7 +193,7 @@ class FlotGenerator:
callsign=callsign, callsign=callsign,
region=frontline, region=frontline,
code=str(code), code=str(code),
blue=True, blue=Player.BLUE,
freq=freq, freq=freq,
) )
) )
@@ -215,7 +215,7 @@ class FlotGenerator:
def gen_infantry_group_for_group( def gen_infantry_group_for_group(
self, self,
group: VehicleGroup, group: VehicleGroup,
is_player: bool, is_player: Player,
side: Country, side: Country,
forward_heading: Heading, forward_heading: Heading,
) -> None: ) -> None:
@@ -294,7 +294,7 @@ class FlotGenerator:
GroundForcePainter(faction, vehicle).apply_livery() GroundForcePainter(faction, vehicle).apply_livery()
vg.hidden_on_mfd = True vg.hidden_on_mfd = True
def _earliest_tot_on_flot(self, player: bool) -> timedelta: def _earliest_tot_on_flot(self, player: Player) -> timedelta:
tots = [ tots = [
x.time_over_target x.time_over_target
for x in self.game.ato_for(player).packages for x in self.game.ato_for(player).packages
@@ -413,7 +413,7 @@ class FlotGenerator:
""" """
duration = timedelta() duration = timedelta()
if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]: if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]:
duration = self._earliest_tot_on_flot(not to_cp.coalition.player) duration = self._earliest_tot_on_flot(to_cp.coalition.player.opponent)
self._set_reform_waypoint(dcs_group, forward_heading, duration) self._set_reform_waypoint(dcs_group, forward_heading, duration)
if stance == CombatStance.AGGRESSIVE: if stance == CombatStance.AGGRESSIVE:
# Attack nearest enemy if any # Attack nearest enemy if any
@@ -503,7 +503,7 @@ class FlotGenerator:
""" """
duration = timedelta() duration = timedelta()
if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]: if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]:
duration = self._earliest_tot_on_flot(not to_cp.coalition.player) duration = self._earliest_tot_on_flot(to_cp.coalition.player.opponent)
self._set_reform_waypoint(dcs_group, forward_heading, duration) self._set_reform_waypoint(dcs_group, forward_heading, duration)
if stance in [ if stance in [
CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE,
@@ -762,13 +762,15 @@ class FlotGenerator:
) )
def _generate_groups( def _generate_groups(
self, groups: list[CombatGroup], is_player: bool self, groups: list[CombatGroup], is_player: Player
) -> List[Tuple[VehicleGroup, CombatGroup]]: ) -> List[Tuple[VehicleGroup, CombatGroup]]:
"""Finds valid positions for planned groups and generates a pydcs group for them""" """Finds valid positions for planned groups and generates a pydcs group for them"""
positioned_groups = [] positioned_groups = []
assert self.conflict.heading is not None assert self.conflict.heading is not None
spawn_heading = ( spawn_heading = (
self.conflict.heading.left if is_player else self.conflict.heading.right self.conflict.heading.left
if is_player.is_blue
else self.conflict.heading.right
) )
country = self.game.coalition_for(is_player).faction.country country = self.game.coalition_for(is_player).faction.country
country = self.mission.country(country.name) country = self.mission.country(country.name)
@@ -795,7 +797,7 @@ class FlotGenerator:
final_position, final_position,
heading=spawn_heading.opposite, heading=spawn_heading.opposite,
) )
if is_player: if is_player == Player.BLUE:
g.set_skill(Skill(self.game.settings.player_skill)) g.set_skill(Skill(self.game.settings.player_skill))
else: else:
g.set_skill(Skill(self.game.settings.enemy_vehicle_skill)) g.set_skill(Skill(self.game.settings.enemy_vehicle_skill))
@@ -813,7 +815,7 @@ class FlotGenerator:
def _generate_group( def _generate_group(
self, self,
player: bool, player: Player,
side: Country, side: Country,
unit_type: GroundUnitType, unit_type: GroundUnitType,
count: int, count: int,

View File

@@ -10,15 +10,14 @@ from dcs import Mission
from dcs.action import DoScript, DoScriptFile from dcs.action import DoScript, DoScriptFile
from dcs.translation import String from dcs.translation import String
from dcs.triggers import TriggerStart from dcs.triggers import TriggerStart
from dcs.unit import Skill
from game.ato import FlightType from game.ato import FlightType
from game.data.units import UnitClass
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject from game.theater import TheaterGroundObject
from game.theater.iadsnetwork.iadsrole import IadsRole from game.theater.iadsnetwork.iadsrole import IadsRole
from game.utils import escape_string_for_lua from game.utils import escape_string_for_lua
from game.data.units import UnitClass
from .missiondata import MissionData from .missiondata import MissionData
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -102,13 +101,15 @@ class LuaGenerator:
for logistic_info in self.mission_data.logistics: for logistic_info in self.mission_data.logistics:
if logistic_info.transport not in transports: if logistic_info.transport not in transports:
transports.append(logistic_info.transport) transports.append(logistic_info.transport)
coalition_color = "blue" if logistic_info.blue else "red" coalition_color = "blue" if logistic_info.blue.is_blue else "red"
logistics_item = logistics_flights.add_item() logistics_item = logistics_flights.add_item()
logistics_item.add_data_array("pilot_names", logistic_info.pilot_names) logistics_item.add_data_array("pilot_names", logistic_info.pilot_names)
logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone) logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone)
logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone) logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone)
logistics_item.add_key_value("target_zone", logistic_info.target_zone) logistics_item.add_key_value("target_zone", logistic_info.target_zone)
logistics_item.add_key_value("side", str(2 if logistic_info.blue else 1)) logistics_item.add_key_value(
"side", str(2 if logistic_info.blue.is_blue else 1)
)
logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit) logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit)
logistics_item.add_key_value( logistics_item.add_key_value(
"aircraft_type", logistic_info.transport.dcs_id "aircraft_type", logistic_info.transport.dcs_id
@@ -144,7 +145,7 @@ class LuaGenerator:
target_points = lua_data.add_item("TargetPoints") target_points = lua_data.add_item("TargetPoints")
for flight in self.mission_data.flights: for flight in self.mission_data.flights:
if flight.friendly and flight.flight_type in [ if flight.friendly.is_blue and flight.flight_type in [
FlightType.ANTISHIP, FlightType.ANTISHIP,
FlightType.DEAD, FlightType.DEAD,
FlightType.SEAD, FlightType.SEAD,
@@ -178,7 +179,7 @@ class LuaGenerator:
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
coalition_object = ( coalition_object = (
lua_data.get_or_create_item("BlueAA") lua_data.get_or_create_item("BlueAA")
if cp.captured if cp.captured.is_blue
else lua_data.get_or_create_item("RedAA") else lua_data.get_or_create_item("RedAA")
) )
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
@@ -202,7 +203,9 @@ class LuaGenerator:
# Should probably do the same with all the roles... but the script is already # Should probably do the same with all the roles... but the script is already
# tolerant of those being empty. # tolerant of those being empty.
for node in self.game.theater.iads_network.skynet_nodes(self.game): for node in self.game.theater.iads_network.skynet_nodes(self.game):
coalition = iads_object.get_or_create_item("BLUE" if node.player else "RED") coalition = iads_object.get_or_create_item(
"BLUE" if node.player.is_blue else "RED"
)
iads_type = coalition.get_or_create_item(node.iads_role.value) iads_type = coalition.get_or_create_item(node.iads_role.value)
iads_element = iads_type.add_item() iads_element = iads_type.add_item()
iads_element.add_key_value("dcsGroupName", node.dcs_name) iads_element.add_key_value("dcsGroupName", node.dcs_name)

View File

@@ -15,6 +15,7 @@ from game.runways import RunwayData
if TYPE_CHECKING: if TYPE_CHECKING:
from game.radio.radios import RadioFrequency from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel from game.radio.tacan import TacanChannel
from game.theater.player import Player
from game.utils import Distance from game.utils import Distance
from uuid import UUID from uuid import UUID
@@ -24,7 +25,7 @@ class GroupInfo:
group_name: str group_name: str
callsign: str callsign: str
freq: RadioFrequency freq: RadioFrequency
blue: bool blue: Player
@dataclass @dataclass
@@ -85,7 +86,7 @@ class LogisticsInfo:
pilot_names: list[str] pilot_names: list[str]
transport: AircraftType transport: AircraftType
blue: bool blue: Player
logistic_unit: str = field(default_factory=str) logistic_unit: str = field(default_factory=str)
pickup_zone: str = field(default_factory=str) pickup_zone: str = field(default_factory=str)

View File

@@ -9,6 +9,7 @@ import dcs.lua
from dcs import Mission, Point from dcs import Mission, Point
from dcs.coalition import Coalition from dcs.coalition import Coalition
from dcs.countries import country_dict from dcs.countries import country_dict
from dcs.point import MovingPoint
from dcs.task import OptReactOnThreat from dcs.task import OptReactOnThreat
from dcs.terrain import Airport from dcs.terrain import Airport
from dcs.unit import Static from dcs.unit import Static
@@ -131,28 +132,42 @@ class MissionGenerator:
return self.unit_map return self.unit_map
@staticmethod @staticmethod
def _configure_ewrj(gen: AircraftGenerator) -> None: def _configure_react_to_threat_for_ew_jamming_packages(
gen: AircraftGenerator,
) -> None:
for groups in gen.ewrj_package_dict.values(): for groups in gen.ewrj_package_dict.values():
optrot = [
task
for task in groups[0].points[0].tasks
if isinstance(task, OptReactOnThreat)
][0]
assert isinstance(optrot, OptReactOnThreat)
if (
len(groups) == 1
and optrot.value != OptReactOnThreat.Values.PassiveDefense
):
# primary flight with no EWR-Jamming capability
continue
for group in groups: for group in groups:
tasks = group.points[0].tasks start_point = [
for i in range(len(tasks)): p for p in group.points if p.name in ["JOIN", "RACETRACK START"]
if isinstance(tasks[i], OptReactOnThreat): ]
tasks[i] = OptReactOnThreat( if not start_point:
OptReactOnThreat.Values.PassiveDefense sp = group.points[0]
) else:
break sp = start_point[0]
MissionGenerator.set_react_to_threat(
OptReactOnThreat.Values.PassiveDefense, sp
)
stop_point = [
p for p in group.points if p.name in ["SPLIT", "RACETRACK END"]
]
if not stop_point:
sp = group.points[0]
else:
sp = stop_point[0]
MissionGenerator.set_react_to_threat(
OptReactOnThreat.Values.EvadeFire, sp
)
@staticmethod
def set_react_to_threat(
optrot: OptReactOnThreat.Values, start_point: MovingPoint
) -> None:
tasks = start_point.tasks
for i in range(len(tasks)):
if isinstance(tasks[i], OptReactOnThreat):
tasks[i] = OptReactOnThreat(optrot)
return
tasks.append(OptReactOnThreat(optrot))
def setup_mission_coalitions(self) -> None: def setup_mission_coalitions(self) -> None:
self.mission.coalition["blue"] = Coalition( self.mission.coalition["blue"] = Coalition(
@@ -291,7 +306,7 @@ class MissionGenerator:
flight.aircraft_type.assign_channels_for_flight(flight, self.mission_data) flight.aircraft_type.assign_channels_for_flight(flight, self.mission_data)
if self.game.settings.plugins.get("ewrj"): if self.game.settings.plugins.get("ewrj"):
self._configure_ewrj(aircraft_generator) self._configure_react_to_threat_for_ew_jamming_packages(aircraft_generator)
def generate_destroyed_units(self) -> None: def generate_destroyed_units(self) -> None:
"""Add destroyed units to the Mission""" """Add destroyed units to the Mission"""
@@ -376,9 +391,12 @@ class MissionGenerator:
tmu.theater_unit.position, tmu.theater_unit.position,
self.mission.terrain, self.mission.terrain,
).dict() ).dict()
warehouse["coalition"] = ( if tmu.theater_unit.ground_object.coalition.player.is_neutral:
"blue" if tmu.theater_unit.ground_object.coalition.player else "red" warehouse["coalition"] = "neutral"
) elif tmu.theater_unit.ground_object.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
warehouse["dynamicCargo"] = settings.dynamic_cargo warehouse["dynamicCargo"] = settings.dynamic_cargo
if tmu.theater_unit.is_ship or tmu.dcs_unit.category == "Heliports": # type: ignore if tmu.theater_unit.is_ship or tmu.dcs_unit.category == "Heliports": # type: ignore
warehouse["dynamicSpawn"] = settings.dynamic_slots warehouse["dynamicSpawn"] = settings.dynamic_slots

View File

@@ -13,6 +13,7 @@ from dcs.vehicles import vehicle_map
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.naming import namegen from game.naming import namegen
from game.theater import Player
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -25,12 +26,12 @@ class RebellionGenerator:
def generate(self) -> None: def generate(self) -> None:
ownfor_country = self.mission.country( ownfor_country = self.mission.country(
self.game.coalition_for(player=True).faction.country.name self.game.coalition_for(player=Player.BLUE).faction.country.name
) )
for rz in self.game.theater.ownfor_rebel_zones: for rz in self.game.theater.ownfor_rebel_zones:
self._generate_rebel_zone(ownfor_country, rz) self._generate_rebel_zone(ownfor_country, rz)
opfor_country = self.mission.country( opfor_country = self.mission.country(
self.game.coalition_for(player=False).faction.country.name self.game.coalition_for(player=Player.RED).faction.country.name
) )
for rz in self.game.theater.opfor_rebel_zones: for rz in self.game.theater.opfor_rebel_zones:
self._generate_rebel_zone(opfor_country, rz) self._generate_rebel_zone(opfor_country, rz)

View File

@@ -67,6 +67,7 @@ from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.runways import RunwayData from game.runways import RunwayData
from game.theater import ( from game.theater import (
ControlPoint, ControlPoint,
Player,
TheaterGroundObject, TheaterGroundObject,
TheaterUnit, TheaterUnit,
NavalControlPoint, NavalControlPoint,
@@ -386,9 +387,9 @@ class GroundObjectGenerator:
) )
self._register_theater_unit(unit, static_group.units[0]) self._register_theater_unit(unit, static_group.units[0])
@staticmethod def enable_eplrs(self, group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None: eplrs_enabled = self.game.settings.eplrs_enabled
if unit_type.eplrs: if eplrs_enabled and unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id)) group.points[0].tasks.append(EPLRS(group.id))
def set_alarm_state(self, group: MovingGroup[Any]) -> None: def set_alarm_state(self, group: MovingGroup[Any]) -> None:
@@ -408,7 +409,7 @@ class GroundObjectGenerator:
# Align the trigger zones to the faction color on the DCS briefing/F10 map. # Align the trigger zones to the faction color on the DCS briefing/F10 map.
color = ( color = (
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15} {1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
if scenery.ground_object.is_friendly(to_player=True) if scenery.ground_object.is_friendly(to_player=Player.BLUE)
else {1: 1, 2: 0.2, 3: 0.2, 4: 0.15} else {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
) )
@@ -809,9 +810,12 @@ class HelipadGenerator:
# capture triggers # capture triggers
pad: BaseFARP pad: BaseFARP
neutral_country = self.m.country(self.game.neutral_country.name) neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country( if self.cp.captured is Player.NEUTRAL:
self.game.coalition_for(self.cp.captured).faction.country.name country = neutral_country
) else:
country = self.m.country(
self.game.coalition_for(self.cp.captured).faction.country.name
)
name = f"{self.cp.name} {helipad_type} {i}" name = f"{self.cp.name} {helipad_type} {i}"
logging.info("Generating helipad static : " + name) logging.info("Generating helipad static : " + name)
@@ -862,7 +866,7 @@ class HelipadGenerator:
if self.game.position_culled(helipad): if self.game.position_culled(helipad):
cull_farp_statics = True cull_farp_statics = True
if self.cp.coalition.player: if self.cp.coalition.player.is_blue:
for package in self.cp.coalition.ato.packages: for package in self.cp.coalition.ato.packages:
for flight in package.flights: for flight in package.flights:
if flight.squadron.location == self.cp: if flight.squadron.location == self.cp:
@@ -878,7 +882,12 @@ class HelipadGenerator:
pad.position, pad.position,
self.m.terrain, self.m.terrain,
).dict() ).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" if self.cp.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif self.cp.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo? # configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse self.m.warehouses.warehouses[pad.id] = warehouse
@@ -989,7 +998,7 @@ class GroundSpawnRoadbaseGenerator:
cull_farp_statics = True cull_farp_statics = True
elif self.game.position_culled(ground_spawn[0]): elif self.game.position_culled(ground_spawn[0]):
cull_farp_statics = True cull_farp_statics = True
if self.cp.coalition.player: if self.cp.coalition.player.is_blue:
for package in self.cp.coalition.ato.packages: for package in self.cp.coalition.ato.packages:
for flight in package.flights: for flight in package.flights:
if flight.squadron.location == self.cp: if flight.squadron.location == self.cp:
@@ -1005,7 +1014,12 @@ class GroundSpawnRoadbaseGenerator:
pad.position, pad.position,
self.m.terrain, self.m.terrain,
).dict() ).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" if self.cp.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif self.cp.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo? # configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse self.m.warehouses.warehouses[pad.id] = warehouse
@@ -1131,7 +1145,12 @@ class GroundSpawnLargeGenerator:
pad.position, pad.position,
self.m.terrain, self.m.terrain,
).dict() ).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" if self.cp.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif self.cp.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo? # configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse self.m.warehouses.warehouses[pad.id] = warehouse
@@ -1258,7 +1277,7 @@ class GroundSpawnGenerator:
cull_farp_statics = True cull_farp_statics = True
elif self.game.position_culled(vtol_pad[0]): elif self.game.position_culled(vtol_pad[0]):
cull_farp_statics = True cull_farp_statics = True
if self.cp.coalition.player: if self.cp.coalition.player.is_blue:
for package in self.cp.coalition.ato.packages: for package in self.cp.coalition.ato.packages:
for flight in package.flights: for flight in package.flights:
if flight.squadron.location == self.cp: if flight.squadron.location == self.cp:
@@ -1275,7 +1294,12 @@ class GroundSpawnGenerator:
pad.position, pad.position,
self.m.terrain, self.m.terrain,
).dict() ).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red" if self.cp.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif self.cp.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo? # configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse self.m.warehouses.warehouses[pad.id] = warehouse
@@ -1382,7 +1406,11 @@ class TgoGenerator:
def generate(self) -> None: def generate(self) -> None:
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
country = self.m.country(cp.coalition.faction.country.name) # Use neutral country for neutral control points
if cp.captured is Player.NEUTRAL:
country = self.m.country(self.game.neutral_country.name)
else:
country = self.m.country(cp.coalition.faction.country.name)
# Generate helipads # Generate helipads
helipad_gen = HelipadGenerator( helipad_gen = HelipadGenerator(

View File

@@ -12,7 +12,6 @@ from dcs.action import (
RemoveSceneObjects, RemoveSceneObjects,
RemoveSceneObjectsMask, RemoveSceneObjectsMask,
SceneryDestructionZone, SceneryDestructionZone,
Smoke,
) )
from dcs.condition import ( from dcs.condition import (
AllOfCoalitionOutsideZone, AllOfCoalitionOutsideZone,
@@ -33,6 +32,7 @@ from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
from game.theater.controlpoint import ControlPoint
PUSH_TRIGGER_SIZE = 3000 PUSH_TRIGGER_SIZE = 3000
PUSH_TRIGGER_ACTIVATION_AGL = 25 PUSH_TRIGGER_ACTIVATION_AGL = 25
@@ -99,9 +99,12 @@ class TriggerGenerator:
raise RuntimeError( raise RuntimeError(
f"Could not find {airfield.airport.name} in the mission" f"Could not find {airfield.airport.name} in the mission"
) )
cp_airport.set_coalition( if airfield.captured.is_neutral:
airfield.captured and player_coalition or enemy_coalition cp_airport.set_coalition("neutral")
) elif airfield.captured.is_blue:
cp_airport.set_coalition(player_coalition)
elif airfield.captured.is_red:
cp_airport.set_coalition(enemy_coalition)
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None: def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
""" """
@@ -138,7 +141,7 @@ class TriggerGenerator:
zone = self.mission.triggers.add_triggerzone( zone = self.mission.triggers.add_triggerzone(
location, radius=10, hidden=True, name="MARK" location, radius=10, hidden=True, name="MARK"
) )
if cp.captured: if cp.captured.is_blue:
name = ground_object.obj_name + " [ALLY]" name = ground_object.obj_name + " [ALLY]"
else: else:
name = ground_object.obj_name + " [ENEMY]" name = ground_object.obj_name + " [ENEMY]"
@@ -177,26 +180,57 @@ class TriggerGenerator:
# clear_trigger.add_action(MessageToAll(text=String("Enable clear trigger"),)) # clear_trigger.add_action(MessageToAll(text=String("Enable clear trigger"),))
self.mission.triggerrules.triggers.append(enable_clear_trigger) self.mission.triggerrules.triggers.append(enable_clear_trigger)
def _generate_capture_triggers( def _create_capture_trigger(
self, player_coalition: str, enemy_coalition: str self,
) -> None: trigger_zone_id: int,
flag: int,
capturing_coalition: str,
losing_coalition: str,
capturing_coalition_int: int,
cp: ControlPoint,
flag_condition_true: bool = False,
) -> TriggerCondition:
"""Helper method to create a single capture trigger with the given parameters."""
trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
trigger.add_condition(
AllOfCoalitionOutsideZone(
losing_coalition, trigger_zone_id, unit_type="GROUND"
)
)
trigger.add_condition(
PartOfCoalitionInZone(
capturing_coalition, trigger_zone_id, unit_type="GROUND"
)
)
if flag_condition_true:
trigger.add_condition(FlagIsTrue(flag=flag))
trigger.add_action(ClearFlag(flag=flag))
else:
trigger.add_condition(FlagIsFalse(flag=flag))
trigger.add_action(SetFlag(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{capturing_coalition_int}||{cp.full_name}"'
)
trigger.add_action(DoScript(script_string))
return trigger
def _generate_capture_triggers(self) -> None:
"""Creates a pair of triggers for each control point of `cls.capture_zone_types`. """Creates a pair of triggers for each control point of `cls.capture_zone_types`.
One for the initial capture of a control point, and one if it is recaptured. One for the initial capture of a control point, and one if it is recaptured.
Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua` Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua`
""" """
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
attacking_coalition = str.lower(cp.captured.opponent.name)
defending_coalition = str.lower(cp.captured.opponent.opponent.name)
if attacking_coalition == "red":
attack_coalition_int = 1 # 1 is the Event int for Red
defend_coalition_int = 2 # 2 is the Event int for Blue
else:
attack_coalition_int = 2
defend_coalition_int = 1
if isinstance(cp, self.capture_zone_types) and not cp.is_carrier: if isinstance(cp, self.capture_zone_types) and not cp.is_carrier:
if cp.captured:
attacking_coalition = enemy_coalition
attack_coalition_int = 1 # 1 is the Event int for Red
defending_coalition = player_coalition
defend_coalition_int = 2 # 2 is the Event int for Blue
else:
attacking_coalition = player_coalition
attack_coalition_int = 2
defending_coalition = enemy_coalition
defend_coalition_int = 1
trigger_zone = self.mission.triggers.add_triggerzone( trigger_zone = self.mission.triggers.add_triggerzone(
cp.position, cp.position,
radius=TRIGGER_RADIUS_CAPTURE, radius=TRIGGER_RADIUS_CAPTURE,
@@ -204,43 +238,48 @@ class TriggerGenerator:
name="CAPTURE", name="CAPTURE",
) )
flag = self.get_capture_zone_flag() flag = self.get_capture_zone_flag()
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger") capture = self._create_capture_trigger(
capture_trigger.add_condition( trigger_zone.id,
AllOfCoalitionOutsideZone( flag,
defending_coalition, trigger_zone.id, unit_type="GROUND" attacking_coalition,
) defending_coalition,
attack_coalition_int,
cp,
) )
capture_trigger.add_condition( recapture = self._create_capture_trigger(
PartOfCoalitionInZone( trigger_zone.id,
attacking_coalition, trigger_zone.id, unit_type="GROUND" flag,
) defending_coalition,
attacking_coalition,
defend_coalition_int,
cp,
flag_condition_true=True,
) )
capture_trigger.add_condition(FlagIsFalse(flag=flag)) self.mission.triggerrules.triggers.extend([capture, recapture])
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"'
)
capture_trigger.add_action(DoScript(script_string))
capture_trigger.add_action(SetFlag(flag=flag))
self.mission.triggerrules.triggers.append(capture_trigger)
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger") if cp.captured.is_neutral:
recapture_trigger.add_condition( # In this case attacking_coalition is blue and defending_coalition is red because
AllOfCoalitionOutsideZone( # neutral control points always have blue set as the opponent
attacking_coalition, trigger_zone.id, unit_type="GROUND" red_capture = self._create_capture_trigger(
trigger_zone.id,
flag,
defending_coalition,
attacking_coalition,
defend_coalition_int,
cp,
) )
) red_recapture = self._create_capture_trigger(
recapture_trigger.add_condition( trigger_zone.id,
PartOfCoalitionInZone( flag,
defending_coalition, trigger_zone.id, unit_type="GROUND" defending_coalition,
attacking_coalition,
defend_coalition_int,
cp,
flag_condition_true=True,
)
self.mission.triggerrules.triggers.extend(
[red_capture, red_recapture]
) )
)
recapture_trigger.add_condition(FlagIsTrue(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"'
)
recapture_trigger.add_action(DoScript(script_string))
recapture_trigger.add_action(ClearFlag(flag=flag))
self.mission.triggerrules.triggers.append(recapture_trigger)
def generate(self) -> None: def generate(self) -> None:
player_coalition = "blue" player_coalition = "blue"
@@ -249,7 +288,7 @@ class TriggerGenerator:
self._set_skill(player_coalition, enemy_coalition) self._set_skill(player_coalition, enemy_coalition)
self._set_allegiances(player_coalition, enemy_coalition) self._set_allegiances(player_coalition, enemy_coalition)
self._gen_markers() self._gen_markers()
self._generate_capture_triggers(player_coalition, enemy_coalition) self._generate_capture_triggers()
if self.game.settings.ground_start_scenery_remove_triggers: if self.game.settings.ground_start_scenery_remove_triggers:
try: try:
self._generate_clear_statics_trigger(self.game.scenery_clear_zones) self._generate_clear_statics_trigger(self.game.scenery_clear_zones)

View File

@@ -55,7 +55,7 @@ class GameStats:
turn_data = GameTurnMetadata() turn_data = GameTurnMetadata()
for cp in game.theater.controlpoints: for cp in game.theater.controlpoints:
if cp.captured: if cp.captured.is_blue:
for squadron in cp.squadrons: for squadron in cp.squadrons:
turn_data.allied_units.aircraft_count += squadron.owned_aircraft turn_data.allied_units.aircraft_count += squadron.owned_aircraft
turn_data.allied_units.vehicles_count += sum(cp.base.armor.values()) turn_data.allied_units.vehicles_count += sum(cp.base.armor.values())

View File

@@ -36,6 +36,18 @@ class DummyObject:
class MigrationUnpickler(pickle.Unpickler): class MigrationUnpickler(pickle.Unpickler):
"""Custom unpickler to migrate campaign save-files for when components have been moved""" """Custom unpickler to migrate campaign save-files for when components have been moved"""
def find_class(self, module: Any, name: str) -> Any: def find_class(self, module: Any, name: str) -> Any:
if name == "Su_30MKA_AG":
from pydcs_extensions.su30 import Su_30MKA
return Su_30MKA
if name == "Su_30MKI_AG":
from pydcs_extensions.su30 import Su_30MKI
return Su_30MKI
if name == "Su_30SM_AG":
from pydcs_extensions.su30 import Su_30SM
return Su_30SM
if name == "Su_30MKM_AG":
from pydcs_extensions.su30 import Su_30MKM
return Su_30MKM
if name == "NightMissions": if name == "NightMissions":
from game.settings import NightMissions from game.settings import NightMissions
return NightMissions return NightMissions
@@ -108,6 +120,16 @@ class MigrationUnpickler(pickle.Unpickler):
elif name == "Bas_100": elif name == "Bas_100":
from dcs.terrain.kola.airports import Vuojarvi from dcs.terrain.kola.airports import Vuojarvi
return Vuojarvi return Vuojarvi
elif name == "Alakourtti":
from dcs.terrain.kola.airports import Alakurtti
return Alakurtti
if module == "dcs.terrain.sinai.airports":
if name == "Borj_El_Arab_International_Airport":
from dcs.terrain.sinai.airports import Borg_El_Arab_International_Airport
return Borg_El_Arab_International_Airport
elif name == "Palmahim":
from dcs.terrain.sinai.airports import Palmachim
return Palmachim
if module == "dcs.terrain.syria.airports": if module == "dcs.terrain.syria.airports":
if name == "Amman": if name == "Amman":
from dcs.terrain.syria.airports import Marka from dcs.terrain.syria.airports import Marka
@@ -119,6 +141,10 @@ class MigrationUnpickler(pickle.Unpickler):
"Helipad_218" "Helipad_218"
]: ]:
return dcs.terrain.Airport # use base-class if airport was removed return dcs.terrain.Airport # use base-class if airport was removed
if module == "dcs.terrain.afghanistan.airports":
if name == "Khost_Heliport":
from dcs.terrain.afghanistan.airports import FOB_Salerno
return FOB_Salerno
if module == "dcs.terrain.falklands.airports": if module == "dcs.terrain.falklands.airports":
if name == "Aerodromo_De_Tolhuin": if name == "Aerodromo_De_Tolhuin":
from dcs.terrain.falklands.airports import Tolhuin from dcs.terrain.falklands.airports import Tolhuin

View File

@@ -35,6 +35,7 @@ from game.runways import RunwayData
from game.settings import Settings from game.settings import Settings
from game.squadrons import AirWing from game.squadrons import AirWing
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater.player import Player
from game.theater.controlpoint import ( from game.theater.controlpoint import (
ControlPoint, ControlPoint,
OffMapSpawn, OffMapSpawn,
@@ -833,7 +834,7 @@ class PretenseAircraftGenerator:
""" """
self.initialize_pretense_data_structures(cp) self.initialize_pretense_data_structures(cp)
is_player = True is_player = Player.BLUE
if country == cp.coalition.faction.country: if country == cp.coalition.faction.country:
offmap_transport_cp = self.find_pretense_cargo_plane_cp(cp) offmap_transport_cp = self.find_pretense_cargo_plane_cp(cp)
@@ -868,7 +869,7 @@ class PretenseAircraftGenerator:
coalition = ( coalition = (
self.game.coalition_for(is_player) self.game.coalition_for(is_player)
if country == self.game.coalition_for(is_player).faction.country if country == self.game.coalition_for(is_player).faction.country
else self.game.coalition_for(False) else self.game.coalition_for(Player.RED)
) )
self.generate_pretense_aircraft_for_other_side(cp, coalition, ato) self.generate_pretense_aircraft_for_other_side(cp, coalition, ato)
@@ -1057,6 +1058,7 @@ class PretenseAircraftGenerator:
and ( and (
not self.need_ecm not self.need_ecm
or flight.any_member_has_weapon_of_type(WeaponType.JAMMER) or flight.any_member_has_weapon_of_type(WeaponType.JAMMER)
or flight.any_member_has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
) )
): ):
self.ewrj_package_dict[id(flight.package)].append(group) self.ewrj_package_dict[id(flight.package)].append(group)

View File

@@ -25,7 +25,7 @@ from game.missiongenerator.aircraft.flightgroupspawner import (
) )
from game.missiongenerator.missiondata import MissionData from game.missiongenerator.missiondata import MissionData
from game.naming import NameGenerator from game.naming import NameGenerator
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, Player
class PretenseNameGenerator(NameGenerator): class PretenseNameGenerator(NameGenerator):
@@ -87,7 +87,7 @@ class PretenseFlightGroupSpawner(FlightGroupSpawner):
def insert_into_pretense(self, name: str) -> None: def insert_into_pretense(self, name: str) -> None:
cp = self.flight.departure cp = self.flight.departure
is_player = True is_player = Player.BLUE
cp_side = ( cp_side = (
2 2
if self.flight.coalition if self.flight.coalition

View File

@@ -1495,13 +1495,13 @@ class PretenseLuaGenerator(LuaGenerator):
cp_name.replace("ä", "a") cp_name.replace("ä", "a")
cp_name.replace("ö", "o") cp_name.replace("ö", "o")
cp_name.replace("ø", "o") cp_name.replace("ø", "o")
cp_side = 2 if cp.captured else 1 cp_side = 2 if cp.captured.is_blue else 1
if isinstance(cp, OffMapSpawn): if isinstance(cp, OffMapSpawn):
continue continue
elif ( elif (
cp.is_fleet cp.is_fleet
and cp.captured and cp.captured.is_blue
and self.game.settings.pretense_controllable_carrier and self.game.settings.pretense_controllable_carrier
): ):
# Friendly carrier, generate carrier parameters # Friendly carrier, generate carrier parameters
@@ -1591,7 +1591,7 @@ class PretenseLuaGenerator(LuaGenerator):
# Also connect carrier and LHA control points to adjacent friendly points # Also connect carrier and LHA control points to adjacent friendly points
if cp.is_fleet and ( if cp.is_fleet and (
not self.game.settings.pretense_controllable_carrier not self.game.settings.pretense_controllable_carrier
or not cp.captured or cp.captured.is_red
): ):
num_of_carrier_connections = 0 num_of_carrier_connections = 0
for ( for (
@@ -1616,13 +1616,13 @@ class PretenseLuaGenerator(LuaGenerator):
try: try:
if ( if (
cp.is_fleet cp.is_fleet
and cp.captured and cp.captured.is_blue
and self.game.settings.pretense_controllable_carrier and self.game.settings.pretense_controllable_carrier
): ):
break break
elif ( elif (
closest_cps[extra_connection].is_fleet closest_cps[extra_connection].is_fleet
and closest_cps[extra_connection].captured and closest_cps[extra_connection].captured.is_blue
and self.game.settings.pretense_controllable_carrier and self.game.settings.pretense_controllable_carrier
): ):
break break
@@ -1647,7 +1647,7 @@ class PretenseLuaGenerator(LuaGenerator):
if isinstance(cp, OffMapSpawn): if isinstance(cp, OffMapSpawn):
continue continue
cp_side_captured = cp_side == 2 cp_side_captured = cp_side == 2
if cp_side_captured != cp.captured: if cp_side_captured != cp.captured.is_blue:
continue continue
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
cp.name cp.name
@@ -1723,6 +1723,13 @@ class PretenseLuaGenerator(LuaGenerator):
def inject_plugin_script( def inject_plugin_script(
self, plugin_mnemonic: str, script: str, script_mnemonic: str self, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None: ) -> None:
# Hard block MOOSE injection for Pretense missions
if script_mnemonic.lower() == "moose" or "moose" in script.lower():
logging.info(
"PretenseLuaGenerator: Skipping hard-blocked Moose.lua injection"
)
return
if script_mnemonic in self.plugin_scripts: if script_mnemonic in self.plugin_scripts:
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}") logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
return return
@@ -1730,7 +1737,6 @@ class PretenseLuaGenerator(LuaGenerator):
self.plugin_scripts.append(script_mnemonic) self.plugin_scripts.append(script_mnemonic)
plugin_path = Path("./resources/plugins", plugin_mnemonic) plugin_path = Path("./resources/plugins", plugin_mnemonic)
script_path = Path(plugin_path, script) script_path = Path(plugin_path, script)
if not script_path.exists(): if not script_path.exists():
logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}") logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")

View File

@@ -28,6 +28,7 @@ from game.missiongenerator.visualsgenerator import VisualsGenerator
from game.naming import namegen from game.naming import namegen
from game.persistency import pre_pretense_backups_dir from game.persistency import pre_pretense_backups_dir
from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator
from game.theater import Player
from game.theater.bullseye import Bullseye from game.theater.bullseye import Bullseye
from game.unitmap import UnitMap from game.unitmap import UnitMap
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@@ -233,7 +234,7 @@ class PretenseMissionGenerator(MissionGenerator):
callsign=callsign, callsign=callsign,
region=frontline, region=frontline,
code=str(code), code=str(code),
blue=True, blue=Player.BLUE,
freq=freq, freq=freq,
) )
) )

View File

@@ -49,6 +49,7 @@ from game.theater import (
TheaterUnit, TheaterUnit,
NavalControlPoint, NavalControlPoint,
PresetLocation, PresetLocation,
Player,
) )
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
CarrierGroundObject, CarrierGroundObject,
@@ -248,7 +249,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
""" """
unit_type = None unit_type = None
faction = self.coalition.faction faction = self.coalition.faction
is_player = True is_player = Player.BLUE
side = ( side = (
2 2
if self.country == self.game.coalition_for(is_player).faction.country if self.country == self.game.coalition_for(is_player).faction.country
@@ -458,7 +459,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
control_point.name control_point.name
) )
is_player = True is_player = Player.BLUE
side = ( side = (
2 2
if self.country if self.country
@@ -567,7 +568,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name( cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
control_point.name control_point.name
) )
is_player = True is_player = Player.BLUE
side = ( side = (
2 2
if self.country == self.game.coalition_for(is_player).faction.country if self.country == self.game.coalition_for(is_player).faction.country

View File

@@ -1,9 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging
import math
import random import random
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING
from dcs import Point from dcs import Point
from dcs.action import ( from dcs.action import (
@@ -11,10 +9,6 @@ from dcs.action import (
DoScript, DoScript,
MarkToAll, MarkToAll,
SetFlag, SetFlag,
RemoveSceneObjects,
RemoveSceneObjectsMask,
SceneryDestructionZone,
Smoke,
) )
from dcs.condition import ( from dcs.condition import (
AllOfCoalitionOutsideZone, AllOfCoalitionOutsideZone,
@@ -22,7 +16,6 @@ from dcs.condition import (
FlagIsTrue, FlagIsTrue,
PartOfCoalitionInZone, PartOfCoalitionInZone,
TimeAfter, TimeAfter,
TimeSinceFlag,
) )
from dcs.mission import Mission from dcs.mission import Mission
from dcs.task import Option from dcs.task import Option
@@ -31,12 +24,11 @@ from dcs.terrain.syria.airports import Damascus, Khalkhalah
from dcs.translation import String from dcs.translation import String
from dcs.triggers import Event, TriggerCondition, TriggerOnce from dcs.triggers import Event, TriggerCondition, TriggerOnce
from dcs.unit import Skill from dcs.unit import Skill
from numpy import cross, einsum, arctan2
from shapely import MultiPolygon, Point as ShapelyPoint from shapely import MultiPolygon, Point as ShapelyPoint
from game.naming import ALPHA_MILITARY from game.naming import ALPHA_MILITARY
from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator
from game.theater import Airfield from game.theater import Airfield, Player
from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -119,7 +111,7 @@ class PretenseTriggerGenerator:
f"Could not find {airfield.airport.name} in the mission" f"Could not find {airfield.airport.name} in the mission"
) )
cp_airport.set_coalition( cp_airport.set_coalition(
airfield.captured and player_coalition or enemy_coalition airfield.captured.is_blue and player_coalition or enemy_coalition
) )
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None: def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
@@ -157,7 +149,7 @@ class PretenseTriggerGenerator:
zone = self.mission.triggers.add_triggerzone( zone = self.mission.triggers.add_triggerzone(
location, radius=10, hidden=True, name="MARK" location, radius=10, hidden=True, name="MARK"
) )
if cp.captured: if cp.captured.is_blue:
name = ground_object.obj_name + " [ALLY]" name = ground_object.obj_name + " [ALLY]"
else: else:
name = ground_object.obj_name + " [ENEMY]" name = ground_object.obj_name + " [ENEMY]"
@@ -174,7 +166,7 @@ class PretenseTriggerGenerator:
""" """
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
if isinstance(cp, self.capture_zone_types) and not cp.is_carrier: if isinstance(cp, self.capture_zone_types) and not cp.is_carrier:
if cp.captured: if cp.captured.is_blue:
attacking_coalition = enemy_coalition attacking_coalition = enemy_coalition
attack_coalition_int = 1 # 1 is the Event int for Red attack_coalition_int = 1 # 1 is the Event int for Red
defending_coalition = player_coalition defending_coalition = player_coalition
@@ -243,7 +235,7 @@ class PretenseTriggerGenerator:
self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh" self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh"
) )
sea_zones_landmap = self.game.coalition_for( sea_zones_landmap = self.game.coalition_for(
player=False player=Player.RED
).nav_mesh.theater.landmap ).nav_mesh.theater.landmap
if ( if (
self.game.settings.pretense_controllable_carrier self.game.settings.pretense_controllable_carrier
@@ -251,7 +243,7 @@ class PretenseTriggerGenerator:
): ):
navmesh_number = 0 navmesh_number = 0
for navmesh_poly in self.game.coalition_for( for navmesh_poly in self.game.coalition_for(
player=use_blue_navmesh player=Player.BLUE if use_blue_navmesh else Player.RED
).nav_mesh.polys: ).nav_mesh.polys:
navmesh_number += 1 navmesh_number += 1
if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly): if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly):
@@ -325,7 +317,7 @@ class PretenseTriggerGenerator:
if ( if (
cp.is_fleet cp.is_fleet
and self.game.settings.pretense_controllable_carrier and self.game.settings.pretense_controllable_carrier
and cp.captured and cp.captured.is_blue
): ):
# Friendly carrier zones are generated above # Friendly carrier zones are generated above
continue continue

View File

@@ -8,7 +8,7 @@ from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
from game.config import RUNWAY_REPAIR_COST from game.config import RUNWAY_REPAIR_COST
from game.data.units import UnitClass from game.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, MissionTarget, ParkingType from game.theater import ControlPoint, MissionTarget, ParkingType, Player
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -34,20 +34,20 @@ class ProcurementAi:
def __init__( def __init__(
self, self,
game: Game, game: Game,
for_player: bool, owner: Player,
faction: Faction, faction: Faction,
manage_runways: bool, manage_runways: bool,
manage_front_line: bool, manage_front_line: bool,
manage_aircraft: bool, manage_aircraft: bool,
) -> None: ) -> None:
self.game = game self.game = game
self.is_player = for_player self.is_player = owner
self.air_wing = game.air_wing_for(for_player) self.air_wing = game.air_wing_for(owner)
self.faction = faction self.faction = faction
self.manage_runways = manage_runways self.manage_runways = manage_runways
self.manage_front_line = manage_front_line self.manage_front_line = manage_front_line
self.manage_aircraft = manage_aircraft self.manage_aircraft = manage_aircraft
self.threat_zones = self.game.threat_zone_for(not self.is_player) self.threat_zones = self.game.threat_zone_for(self.is_player.opponent)
def calculate_ground_unit_budget_share(self) -> float: def calculate_ground_unit_budget_share(self) -> float:
armor_investment = 0 armor_investment = 0
@@ -78,7 +78,7 @@ class ProcurementAi:
balance = ( balance = (
self.game.settings.auto_procurement_balance self.game.settings.auto_procurement_balance
if self.is_player if self.is_player.is_blue
else self.game.settings.auto_procurement_balance_red else self.game.settings.auto_procurement_balance_red
) )
air = balance / 100.0 air = balance / 100.0
@@ -114,7 +114,7 @@ class ProcurementAi:
if control_point.runway_can_be_repaired: if control_point.runway_can_be_repaired:
control_point.begin_runway_repair() control_point.begin_runway_repair()
budget -= RUNWAY_REPAIR_COST budget -= RUNWAY_REPAIR_COST
if self.is_player: if self.is_player.is_blue:
self.game.message( self.game.message(
"We have begun repairing the runway at " f"{control_point}" "We have begun repairing the runway at " f"{control_point}"
) )
@@ -223,7 +223,7 @@ class ProcurementAi:
@property @property
def owned_points(self) -> List[ControlPoint]: def owned_points(self) -> List[ControlPoint]:
if self.is_player: if self.is_player.is_blue:
return self.game.theater.player_points() return self.game.theater.player_points()
else: else:
return self.game.theater.enemy_points() return self.game.theater.enemy_points()
@@ -269,7 +269,7 @@ class ProcurementAi:
reserves_factor = ( reserves_factor = (
self.game.settings.frontline_reserves_factor self.game.settings.frontline_reserves_factor
if self.is_player if self.is_player.is_blue
else self.game.settings.frontline_reserves_factor_red else self.game.settings.frontline_reserves_factor_red
) )
fr_factor = reserves_factor / 100.0 fr_factor = reserves_factor / 100.0
@@ -308,7 +308,7 @@ class ProcurementAi:
) )
target = ( target = (
self.game.settings.reserves_procurement_target self.game.settings.reserves_procurement_target
if self.is_player if self.is_player.is_blue
else self.game.settings.reserves_procurement_target_red else self.game.settings.reserves_procurement_target_red
) )
if allocated.total >= target: if allocated.total >= target:

View File

@@ -219,6 +219,27 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
return "SCR-522" return "SCR-522"
@dataclass(frozen=True)
class ARC5RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the ARC-5 WW2 radios. (4 channels)"""
def assign_channels_for_flight(
self, flight: FlightData, mission_data: MissionData
) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 2, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@classmethod
def name(cls) -> str:
return "ARC-5"
class ChannelNamer: class ChannelNamer:
"""Base class allowing channel name customization per-aircraft. """Base class allowing channel name customization per-aircraft.
@@ -376,6 +397,23 @@ class SCR522ChannelNamer(ChannelNamer):
return "SCR-522" return "SCR-522"
class ARC5ChannelNamer(ChannelNamer):
"""
Channel namer for F4U-1D Corsair
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Channel " + "1234"[channel_id - 1]
@classmethod
def name(cls) -> str:
return "ARC-5"
class LegacyWarthogChannelNamer(ChannelNamer): class LegacyWarthogChannelNamer(ChannelNamer):
"""Channel namer for the legacy A-10C.""" """Channel namer for the legacy A-10C."""
@@ -434,3 +472,15 @@ class KiowaChannelNamer(ChannelNamer):
@classmethod @classmethod
def name(cls) -> str: def name(cls) -> str:
return "kiowa" return "kiowa"
class FulcrumChannelNamer(ChannelNamer):
"""Channel namer for the MiG-29 Fulcrum A"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"Ch {channel_id -1}"
@classmethod
def name(cls) -> str:
return "fulcrum"

View File

@@ -84,6 +84,10 @@ def kHz(
return RadioFrequency(num * 1000 + hz, modulation) return RadioFrequency(num * 1000 + hz, modulation)
def Hz(num: int, hz: int = 0, modulation: Modulation = Modulation.AM) -> RadioFrequency:
return RadioFrequency(num + hz, modulation)
@dataclass(frozen=True) @dataclass(frozen=True)
class RadioRange: class RadioRange:
"""Defines the minimum (inclusive) and maximum (exclusive) range of the radio.""" """Defines the minimum (inclusive) and maximum (exclusive) range of the radio."""
@@ -231,6 +235,15 @@ RADIOS: List[Radio] = [
# P-51 / P-47 Radio # P-51 / P-47 Radio
# 4 preset channels (A/B/C/D) # 4 preset channels (A/B/C/D)
Radio("SCR522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)), Radio("SCR522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
# F4U-1D Radio
# 4 preset channels (1/2/3/4)
Radio(
"ARC-5",
(
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),
RadioRange(MHz(220), MHz(390), kHz(25), Modulation.AM),
),
),
# JF-17 Radios should use AM # JF-17 Radios should use AM
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), kHz(25), 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),)), Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
@@ -343,6 +356,20 @@ RADIOS: List[Radio] = [
"AN/ARC-27", "AN/ARC-27",
(RadioRange(MHz(225), MHz(400), kHz(100), Modulation.AM),), (RadioRange(MHz(225), MHz(400), kHz(100), Modulation.AM),),
), ),
# MiG-29 Fulcrum A
Radio(
"R-862",
(
RadioRange(MHz(220), MHz(400), kHz(25), Modulation.AM),
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),
RadioRange(MHz(220), MHz(400), kHz(25), Modulation.FM),
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.FM),
),
),
Radio(
"ARK-19",
(RadioRange(kHz(150), kHz(1300), Hz(500), Modulation.AM),),
),
] ]

View File

@@ -29,12 +29,16 @@ class ControlPointJs(BaseModel):
destination = None destination = None
if control_point.target_position is not None: if control_point.target_position is not None:
destination = control_point.target_position.latlng() destination = control_point.target_position.latlng()
if control_point.captured.is_blue:
blue = True
else:
blue = False
return ControlPointJs( return ControlPointJs(
id=control_point.id, id=control_point.id,
name=control_point.name, name=control_point.name,
blue=control_point.captured, blue=blue,
position=control_point.position.latlng(), position=control_point.position.latlng(),
mobile=control_point.moveable and control_point.captured, mobile=control_point.moveable and control_point.captured.is_blue,
destination=destination, destination=destination,
sidc=str(control_point.sidc()), sidc=str(control_point.sidc()),
) )

View File

@@ -6,6 +6,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, status
from starlette.responses import Response from starlette.responses import Response
from game import Game from game import Game
from game.theater.player import Player
from .models import ControlPointJs from .models import ControlPointJs
from ..dependencies import GameContext from ..dependencies import GameContext
from ..leaflet import LeafletPoint from ..leaflet import LeafletPoint
@@ -75,7 +76,7 @@ def set_destination(
) )
if not cp.moveable: if not cp.moveable:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile") raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile")
if not cp.captured: if not cp.captured.is_blue:
raise HTTPException( raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player" status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player"
) )
@@ -120,7 +121,7 @@ def cancel_travel(cp_id: UUID, game: Game = Depends(GameContext.require)) -> Non
) )
if not cp.moveable: if not cp.moveable:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile") raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile")
if not cp.captured: if not cp.captured.is_blue:
raise HTTPException( raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player" status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player"
) )

View File

@@ -15,6 +15,7 @@ from game.server.mapzones.models import ThreatZonesJs, UnculledZoneJs
from game.server.navmesh.models import NavMeshJs from game.server.navmesh.models import NavMeshJs
from game.server.supplyroutes.models import SupplyRouteJs from game.server.supplyroutes.models import SupplyRouteJs
from game.server.tgos.models import TgoJs from game.server.tgos.models import TgoJs
from game.theater import Player
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -26,9 +27,9 @@ class GameUpdateEventsJs(BaseModel):
new_combats: list[FrozenCombatJs] new_combats: list[FrozenCombatJs]
updated_combats: list[FrozenCombatJs] updated_combats: list[FrozenCombatJs]
ended_combats: list[UUID] ended_combats: list[UUID]
navmesh_updates: dict[bool, NavMeshJs] navmesh_updates: dict[Player, NavMeshJs]
updated_unculled_zones: list[UnculledZoneJs] updated_unculled_zones: list[UnculledZoneJs]
threat_zones_updated: dict[bool, ThreatZonesJs] threat_zones_updated: dict[Player, ThreatZonesJs]
new_flights: list[FlightJs] new_flights: list[FlightJs]
updated_flights: list[FlightJs] updated_flights: list[FlightJs]
deleted_flights: set[UUID] deleted_flights: set[UUID]

View File

@@ -41,9 +41,13 @@ class FlightJs(BaseModel):
waypoints = None waypoints = None
if with_waypoints: if with_waypoints:
waypoints = waypoints_for_flight(flight) waypoints = waypoints_for_flight(flight)
if flight.blue.is_blue:
blue = True
else:
blue = False
return FlightJs( return FlightJs(
id=flight.id, id=flight.id,
blue=flight.blue, blue=blue,
position=position, position=position,
sidc=str(flight.sidc()), sidc=str(flight.sidc()),
waypoints=waypoints, waypoints=waypoints,

View File

@@ -49,7 +49,9 @@ class GameJs(BaseModel):
iads_network=IadsNetworkJs.from_network(game.theater.iads_network), iads_network=IadsNetworkJs.from_network(game.theater.iads_network),
threat_zones=ThreatZoneContainerJs.for_game(game), threat_zones=ThreatZoneContainerJs.for_game(game),
navmeshes=NavMeshesJs.from_game(game), navmeshes=NavMeshesJs.from_game(game),
map_center=game.theater.terrain.map_view_default.position.latlng(), map_center=LeafletPoint.from_latlng(
game.theater.terrain.map_view_default.position.latlng()
),
unculled_zones=UnculledZoneJs.from_game(game), unculled_zones=UnculledZoneJs.from_game(game),
map_zones=MapZonesJs.from_game(game), map_zones=MapZonesJs.from_game(game),
) )

View File

@@ -5,6 +5,7 @@ from uuid import UUID
from pydantic import BaseModel from pydantic import BaseModel
from game.server.leaflet import LeafletPoint from game.server.leaflet import LeafletPoint
from game.theater.player import Player
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork
@@ -34,8 +35,16 @@ class IadsConnectionJs(BaseModel):
iads_connections = [] iads_connections = []
tgo = network_node.group.ground_object tgo = network_node.group.ground_object
for id, connection in network_node.connections.items(): for id, connection in network_node.connections.items():
if connection.ground_object.is_friendly(True) != tgo.is_friendly(True): if connection.ground_object.is_friendly(Player.BLUE) != tgo.is_friendly(
Player.BLUE
):
continue # Skip connections which are not from same coalition continue # Skip connections which are not from same coalition
if tgo.is_friendly(Player.BLUE):
blue = True
elif tgo.is_friendly(Player.RED):
blue = False
else:
continue # Skip neutral
iads_connections.append( iads_connections.append(
IadsConnectionJs( IadsConnectionJs(
id=id, id=id,
@@ -49,7 +58,7 @@ class IadsConnectionJs(BaseModel):
network_node.group.alive_units > 0 network_node.group.alive_units > 0
and connection.alive_units > 0 and connection.alive_units > 0
), ),
blue=tgo.is_friendly(True), blue=blue,
is_power="power" is_power="power"
in [tgo.category, connection.ground_object.category], in [tgo.category, connection.ground_object.category],
) )

View File

@@ -19,6 +19,10 @@ class LeafletPoint(BaseModel):
title = "LatLng" title = "LatLng"
@staticmethod
def from_latlng(latlng: LatLng) -> LeafletPoint:
return LeafletPoint(lat=latlng.lat, lng=latlng.lng)
LeafletLine = list[LeafletPoint] LeafletLine = list[LeafletPoint]

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from pydantic import BaseModel from pydantic import BaseModel
from game.server.leaflet import LeafletPoint, LeafletPoly, ShapelyUtil from game.server.leaflet import LeafletPoint, LeafletPoly, ShapelyUtil
from game.theater import ConflictTheater from game.theater import ConflictTheater, Player
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -85,9 +85,9 @@ class ThreatZoneContainerJs(BaseModel):
def for_game(game: Game) -> ThreatZoneContainerJs: def for_game(game: Game) -> ThreatZoneContainerJs:
return ThreatZoneContainerJs( return ThreatZoneContainerJs(
blue=ThreatZonesJs.from_zones( blue=ThreatZonesJs.from_zones(
game.threat_zone_for(player=True), game.theater game.threat_zone_for(player=Player.BLUE), game.theater
), ),
red=ThreatZonesJs.from_zones( red=ThreatZonesJs.from_zones(
game.threat_zone_for(player=False), game.theater game.threat_zone_for(player=Player.RED), game.theater
), ),
) )

View File

@@ -2,12 +2,13 @@ from fastapi import APIRouter, Depends
from game import Game from game import Game
from game.server import GameContext from game.server import GameContext
from game.theater.player import Player
from .models import NavMeshJs from .models import NavMeshJs
router: APIRouter = APIRouter(prefix="/navmesh") router: APIRouter = APIRouter(prefix="/navmesh")
@router.get("/", operation_id="get_navmesh", response_model=NavMeshJs) @router.get("/", operation_id="get_navmesh", response_model=NavMeshJs)
def get(for_player: bool, game: Game = Depends(GameContext.require)) -> NavMeshJs: def get(for_player: Player, game: Game = Depends(GameContext.require)) -> NavMeshJs:
mesh = game.coalition_for(for_player).nav_mesh mesh = game.coalition_for(for_player).nav_mesh
return NavMeshJs.from_navmesh(mesh, game) return NavMeshJs.from_navmesh(mesh, game)

View File

@@ -76,6 +76,10 @@ class SupplyRouteJs(BaseModel):
def for_link( def for_link(
game: Game, a: ControlPoint, b: ControlPoint, points: list[Point], sea: bool game: Game, a: ControlPoint, b: ControlPoint, points: list[Point], sea: bool
) -> SupplyRouteJs: ) -> SupplyRouteJs:
if a.captured.is_blue:
blue = True
else:
blue = False
return SupplyRouteJs( return SupplyRouteJs(
# Although these are not persistent objects in the backend, the frontend # Although these are not persistent objects in the backend, the frontend
# needs unique IDs for anything that it will use in a list. That means that # needs unique IDs for anything that it will use in a list. That means that
@@ -93,7 +97,7 @@ class SupplyRouteJs(BaseModel):
points=[p.latlng() for p in points], points=[p.latlng() for p in points],
front_active=not sea and a.front_is_active(b), front_active=not sea and a.front_is_active(b),
is_sea=sea, is_sea=sea,
blue=a.captured, blue=blue,
active_transports=TransportFinder(game, a, b).describe_active_transports( active_transports=TransportFinder(game, a, b).describe_active_transports(
sea sea
), ),

View File

@@ -34,12 +34,16 @@ class TgoJs(BaseModel):
def for_tgo(tgo: TheaterGroundObject) -> TgoJs: def for_tgo(tgo: TheaterGroundObject) -> TgoJs:
threat_ranges = [group.max_threat_range().meters for group in tgo.groups] threat_ranges = [group.max_threat_range().meters for group in tgo.groups]
detection_ranges = [group.max_detection_range().meters for group in tgo.groups] detection_ranges = [group.max_detection_range().meters for group in tgo.groups]
if tgo.control_point.captured.is_blue:
blue = True
else:
blue = False
return TgoJs( return TgoJs(
id=tgo.id, id=tgo.id,
name=tgo.name, name=tgo.name,
control_point_name=tgo.control_point.name, control_point_name=tgo.control_point.name,
category=tgo.category, category=tgo.category,
blue=tgo.control_point.captured, blue=blue,
position=tgo.position.latlng(), position=tgo.position.latlng(),
units=[unit.display_name for unit in tgo.units], units=[unit.display_name for unit in tgo.units],
threat_ranges=threat_ranges, threat_ranges=threat_ranges,

View File

@@ -816,6 +816,12 @@ class Settings:
GAMEPLAY_SECTION, GAMEPLAY_SECTION,
default=True, default=True,
) )
eplrs_enabled: bool = boolean_option(
"Enable EPLRS",
MISSION_GENERATOR_PAGE,
GAMEPLAY_SECTION,
default=True,
)
generate_dark_kneeboard: bool = boolean_option( generate_dark_kneeboard: bool = boolean_option(
"Generate dark kneeboard", "Generate dark kneeboard",
MISSION_GENERATOR_PAGE, MISSION_GENERATOR_PAGE,

View File

@@ -46,7 +46,7 @@ class AirCombat(JoinableCombat):
blue_flights = [] blue_flights = []
red_flights = [] red_flights = []
for flight in self.flights: for flight in self.flights:
if flight.squadron.player: if flight.squadron.player.is_blue:
blue_flights.append(str(flight)) blue_flights.append(str(flight))
else: else:
red_flights.append(str(flight)) red_flights.append(str(flight))
@@ -71,7 +71,7 @@ class AirCombat(JoinableCombat):
blue = [] blue = []
red = [] red = []
for flight in self.flights: for flight in self.flights:
if flight.squadron.player: if flight.squadron.player.is_blue:
blue.append(flight) blue.append(flight)
else: else:
red.append(flight) red.append(flight)

View File

@@ -6,6 +6,7 @@ from collections.abc import Iterator
from datetime import timedelta from datetime import timedelta
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from game.theater.player import Player
from .aircombat import AirCombat from .aircombat import AirCombat
from .aircraftengagementzones import AircraftEngagementZones from .aircraftengagementzones import AircraftEngagementZones
from .atip import AtIp from .atip import AtIp
@@ -31,8 +32,10 @@ class CombatInitiator:
def update_active_combats(self) -> None: def update_active_combats(self) -> None:
blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato) blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato)
red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato) red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato)
blue_sam = SamEngagementZones.from_theater(self.game.theater, player=True) blue_sam = SamEngagementZones.from_theater(
red_sam = SamEngagementZones.from_theater(self.game.theater, player=False) self.game.theater, player=Player.BLUE
)
red_sam = SamEngagementZones.from_theater(self.game.theater, player=Player.RED)
# Check each vulnerable flight to see if it has initiated combat. If any flight # Check each vulnerable flight to see if it has initiated combat. If any flight
# initiates combat, a single FrozenCombat will be created for all involved # initiates combat, a single FrozenCombat will be created for all involved
@@ -46,7 +49,7 @@ class CombatInitiator:
if flight.state.in_combat: if flight.state.in_combat:
return return
if flight.squadron.player: if flight.squadron.player.is_blue:
a2a = red_a2a a2a = red_a2a
own_a2a = blue_a2a own_a2a = blue_a2a
sam = red_sam sam = red_sam

View File

@@ -9,7 +9,7 @@ from shapely.ops import unary_union
from game.utils import dcs_to_shapely_point from game.utils import dcs_to_shapely_point
if TYPE_CHECKING: if TYPE_CHECKING:
from game.theater import ConflictTheater, TheaterGroundObject from game.theater import ConflictTheater, TheaterGroundObject, Player
from game.threatzones import ThreatPoly from game.threatzones import ThreatPoly
@@ -31,7 +31,9 @@ class SamEngagementZones:
yield tgo yield tgo
@classmethod @classmethod
def from_theater(cls, theater: ConflictTheater, player: bool) -> SamEngagementZones: def from_theater(
cls, theater: ConflictTheater, player: Player
) -> SamEngagementZones:
commit_regions = [] commit_regions = []
individual_zones = [] individual_zones = []
for cp in theater.control_points_for(player): for cp in theater.control_points_for(player):

View File

@@ -12,7 +12,7 @@ if TYPE_CHECKING:
from game.ato import Flight, Package from game.ato import Flight, Package
from game.navmesh import NavMesh from game.navmesh import NavMesh
from game.sim.combat import FrozenCombat from game.sim.combat import FrozenCombat
from game.theater import ControlPoint, FrontLine, TheaterGroundObject from game.theater import ControlPoint, FrontLine, TheaterGroundObject, Player
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode
@@ -24,9 +24,9 @@ class GameUpdateEvents:
updated_combats: list[FrozenCombat] = field(default_factory=list) updated_combats: list[FrozenCombat] = field(default_factory=list)
ended_combats: list[FrozenCombat] = field(default_factory=list) ended_combats: list[FrozenCombat] = field(default_factory=list)
updated_flight_positions: list[tuple[Flight, Point]] = field(default_factory=list) updated_flight_positions: list[tuple[Flight, Point]] = field(default_factory=list)
navmesh_updates: dict[bool, NavMesh] = field(default_factory=dict) navmesh_updates: dict[Player, NavMesh] = field(default_factory=dict)
unculled_zones_updated: list[Point] = field(default_factory=list) unculled_zones_updated: list[Point] = field(default_factory=list)
threat_zones_updated: dict[bool, ThreatZones] = field(default_factory=dict) threat_zones_updated: dict[Player, ThreatZones] = field(default_factory=dict)
new_flights: set[Flight] = field(default_factory=set) new_flights: set[Flight] = field(default_factory=set)
updated_flights: set[Flight] = field(default_factory=set) updated_flights: set[Flight] = field(default_factory=set)
deleted_flights: set[UUID] = field(default_factory=set) deleted_flights: set[UUID] = field(default_factory=set)
@@ -70,7 +70,7 @@ class GameUpdateEvents:
self.updated_flight_positions.append((flight, new_position)) self.updated_flight_positions.append((flight, new_position))
return self return self
def update_navmesh(self, player: bool, navmesh: NavMesh) -> GameUpdateEvents: def update_navmesh(self, player: Player, navmesh: NavMesh) -> GameUpdateEvents:
self.navmesh_updates[player] = navmesh self.navmesh_updates[player] = navmesh
return self return self
@@ -78,7 +78,9 @@ class GameUpdateEvents:
self.unculled_zones_updated = zones self.unculled_zones_updated = zones
return self return self
def update_threat_zones(self, player: bool, zones: ThreatZones) -> GameUpdateEvents: def update_threat_zones(
self, player: Player, zones: ThreatZones
) -> GameUpdateEvents:
self.threat_zones_updated[player] = zones self.threat_zones_updated[player] = zones
return self return self

View File

@@ -145,7 +145,7 @@ class MissionResultsProcessor:
def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None: def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
for captured in debriefing.base_captures: for captured in debriefing.base_captures:
try: try:
if captured.captured_by_player: if captured.captured_by_player.is_blue:
self.game.message( self.game.message(
f"{captured.control_point} captured!", f"{captured.control_point} captured!",
f"We took control of {captured.control_point}.", f"We took control of {captured.control_point}.",
@@ -174,7 +174,7 @@ class MissionResultsProcessor:
self, debriefing: Debriefing, events: GameUpdateEvents self, debriefing: Debriefing, events: GameUpdateEvents
) -> None: ) -> None:
for cp in self.game.theater.player_points(): for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured] enemy_cps = [e for e in cp.connected_points if e.captured.is_red]
for enemy_cp in enemy_cps: for enemy_cp in enemy_cps:
front_line = cp.front_line_with(enemy_cp) front_line = cp.front_line_with(enemy_cp)
front_line.update_position() front_line.update_position()
@@ -321,7 +321,7 @@ class MissionResultsProcessor:
settings = cp.coalition.game.settings settings = cp.coalition.game.settings
factor = ( factor = (
settings.frontline_reserves_factor settings.frontline_reserves_factor
if cp.captured if cp.captured.is_blue
else settings.frontline_reserves_factor_red else settings.frontline_reserves_factor_red
) )
@@ -345,7 +345,7 @@ class MissionResultsProcessor:
1, 1,
( (
settings.reserves_procurement_target settings.reserves_procurement_target
if source.captured if source.captured.is_blue
else settings.reserves_procurement_target_red else settings.reserves_procurement_target_red
), ),
) )
@@ -367,7 +367,7 @@ class MissionResultsProcessor:
source.base.commit_losses(moved_units) source.base.commit_losses(moved_units)
# Also transfer pending deliveries. # Also transfer pending deliveries.
for unit_type, count in source.ground_unit_orders.units.items(): for unit_type, count in list(source.ground_unit_orders.units.items()):
move_count = int(count * move_factor) move_count = int(count * move_factor)
source.ground_unit_orders.sell({unit_type: move_count}) source.ground_unit_orders.sell({unit_type: move_count})
destination.ground_unit_orders.order({unit_type: move_count}) destination.ground_unit_orders.order({unit_type: move_count})

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import itertools import itertools
from collections import defaultdict from collections import defaultdict
from typing import Iterator, Optional, Sequence, TYPE_CHECKING from typing import Iterator, Optional, Sequence, TYPE_CHECKING, Any
from game.ato.closestairfields import ObjectiveDistanceCache from game.ato.closestairfields import ObjectiveDistanceCache
from game.dcs.aircrafttype import AircraftType from game.dcs.aircrafttype import AircraftType
@@ -14,18 +14,31 @@ from ..utils import Distance
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
from game.theater.player import Player
from ..ato.flighttype import FlightType from ..ato.flighttype import FlightType
from .squadron import Squadron from .squadron import Squadron
class AirWing: class AirWing:
def __init__(self, player: bool, game: Game, faction: Faction) -> None: def __init__(self, player: Player, game: Game, faction: Faction) -> None:
self.player = player self.player = player
self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
self.squadron_defs = SquadronDefLoader(game, faction).load() self.squadron_defs = SquadronDefLoader(game, faction).load()
self.squadron_def_generator = SquadronDefGenerator(faction) self.squadron_def_generator = SquadronDefGenerator(faction)
self.settings = game.settings self.settings = game.settings
def __setstate__(self, state: dict[str, Any]) -> None:
# Migration: Convert old boolean player values to Player enum
if "player" in state and isinstance(state["player"], bool):
from game.theater.player import Player
if state["player"]:
state["player"] = Player.BLUE
else:
state["player"] = Player.RED
self.__dict__.update(state)
def unclaim_squadron_def(self, squadron: Squadron) -> None: def unclaim_squadron_def(self, squadron: Squadron) -> None:
if squadron.aircraft in self.squadron_defs: if squadron.aircraft in self.squadron_defs:
for squadron_def in self.squadron_defs[squadron.aircraft]: for squadron_def in self.squadron_defs[squadron.aircraft]:

View File

@@ -15,6 +15,7 @@ from faker import Faker
from game.ato import Flight, FlightType, Package from game.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings from game.settings import AutoAtoBehavior, Settings
from game.theater import ParkingType from game.theater import ParkingType
from game.theater.player import Player
from .pilot import Pilot, PilotStatus from .pilot import Pilot, PilotStatus
from ..db.database import Database from ..db.database import Database
from ..radio.radios import RadioFrequency from ..radio.radios import RadioFrequency
@@ -96,7 +97,7 @@ class Squadron:
self._livery_pool: list[str] = [] self._livery_pool: list[str] = []
@property @property
def player(self) -> bool: def player(self) -> Player:
return self.coalition.player return self.coalition.player
def assign_to_base(self, base: ControlPoint) -> None: def assign_to_base(self, base: ControlPoint) -> None:
@@ -134,7 +135,7 @@ class Squadron:
return self.claim_new_pilot_if_allowed() return self.claim_new_pilot_if_allowed()
# For opfor, so player/AI option is irrelevant. # For opfor, so player/AI option is irrelevant.
if not self.player: if self.player != Player.BLUE:
return self.available_pilots.pop() return self.available_pilots.pop()
preference = self.settings.auto_ato_behavior preference = self.settings.auto_ato_behavior

View File

@@ -2,5 +2,6 @@ from .base import *
from .conflicttheater import * from .conflicttheater import *
from .controlpoint import * from .controlpoint import *
from .frontline import FrontLine from .frontline import FrontLine
from .landmap import *
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import SamGroundObject from .theatergroundobject import SamGroundObject

View File

@@ -15,6 +15,7 @@ from .daytimemap import DaytimeMap
from .frontline import FrontLine from .frontline import FrontLine
from .iadsnetwork.iadsnetwork import IadsNetwork from .iadsnetwork.iadsnetwork import IadsNetwork
from .landmap import poly_contains, load_landmap from .landmap import poly_contains, load_landmap
from .player import Player
from .seasonalconditions import SeasonalConditions from .seasonalconditions import SeasonalConditions
from ..utils import Heading from ..utils import Heading
@@ -170,10 +171,10 @@ class ConflictTheater:
return new_point return new_point
def control_points_for( def control_points_for(
self, player: bool, state_check: bool = False self, player: Player, state_check: bool = False
) -> Iterator[ControlPoint]: ) -> Iterator[ControlPoint]:
for point in self.controlpoints: for point in self.controlpoints:
if point.captured == player: if point.captured is player:
if not state_check: if not state_check:
yield point yield point
elif point.is_carrier and point.runway_is_operational(): elif point.is_carrier and point.runway_is_operational():
@@ -182,14 +183,21 @@ class ConflictTheater:
yield point yield point
def player_points(self, state_check: bool = False) -> List[ControlPoint]: def player_points(self, state_check: bool = False) -> List[ControlPoint]:
return list(self.control_points_for(player=True, state_check=state_check)) return list(
self.control_points_for(player=Player.BLUE, state_check=state_check)
)
def conflicts(self) -> Iterator[FrontLine]: def conflicts(self) -> Iterator[FrontLine]:
for cp in self.player_points(): for cp in self.player_points():
yield from cp.front_lines.values() yield from cp.front_lines.values()
def enemy_points(self, state_check: bool = False) -> List[ControlPoint]: def enemy_points(self, state_check: bool = False) -> List[ControlPoint]:
return list(self.control_points_for(player=False, state_check=state_check)) return list(self.control_points_for(player=Player.RED, state_check=state_check))
def neutral_points(self, state_check: bool = False) -> List[ControlPoint]:
return list(
self.control_points_for(player=Player.NEUTRAL, state_check=state_check)
)
def closest_control_point( def closest_control_point(
self, point: Point, allow_naval: bool = False self, point: Point, allow_naval: bool = False
@@ -259,10 +267,12 @@ class ConflictTheater:
""" """
closest_cps = list() closest_cps = list()
distances_to_cp = dict() distances_to_cp = dict()
if cp.captured: if cp.captured.is_blue:
control_points = self.player_points() control_points = self.player_points()
else: elif cp.captured.is_red:
control_points = self.enemy_points() control_points = self.enemy_points()
elif cp.captured.is_neutral:
control_points = self.neutral_points()
for other_cp in control_points: for other_cp in control_points:
if cp == other_cp: if cp == other_cp:
continue continue
@@ -292,6 +302,12 @@ class ConflictTheater:
return cp return cp
raise KeyError(f"Cannot find ControlPoint named {name}") raise KeyError(f"Cannot find ControlPoint named {name}")
def control_point_by_full_name(self, full_name: str) -> ControlPoint:
for cp in self.controlpoints:
if cp.full_name == full_name:
return cp
raise KeyError(f"Cannot find ControlPoint with full name {full_name}")
def find_carriers(self) -> List[ControlPoint]: def find_carriers(self) -> List[ControlPoint]:
try: try:
carriers = [cp for cp in self.controlpoints if cp.is_carrier] carriers = [cp for cp in self.controlpoints if cp.is_carrier]

View File

@@ -40,12 +40,14 @@ from dcs.ships import (
Stennis, Stennis,
Type_071, Type_071,
hms_invincible, hms_invincible,
Essex,
) )
from dcs.terrain.terrain import Airport, ParkingSlot from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unitgroup import ShipGroup, StaticGroup from dcs.unitgroup import ShipGroup, StaticGroup
from dcs.unittype import ShipType from dcs.unittype import ShipType
from game.ato.closestairfields import ObjectiveDistanceCache from game.ato.closestairfields import ObjectiveDistanceCache
from game.controlpoint_influenceradius import ControlPointInfluenceRadius
from game.ground_forces.combat_stance import CombatStance from game.ground_forces.combat_stance import CombatStance
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.runways import RunwayAssigner, RunwayData from game.runways import RunwayAssigner, RunwayData
@@ -66,6 +68,7 @@ from .base import Base
from .frontline import FrontLine from .frontline import FrontLine
from .interfaces.CTLD import CTLD from .interfaces.CTLD import CTLD
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .player import Player
from .theatergroundobject import ( from .theatergroundobject import (
GenericCarrierGroundObject, GenericCarrierGroundObject,
TheaterGroundObject, TheaterGroundObject,
@@ -375,15 +378,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
position: Point, position: Point,
at: StartingPosition, at: StartingPosition,
theater: ConflictTheater, theater: ConflictTheater,
starts_blue: bool, starting_coalition: Player,
cptype: ControlPointType = ControlPointType.AIRBASE, cptype: ControlPointType = ControlPointType.AIRBASE,
is_invisible: bool = False,
) -> None: ) -> None:
super().__init__(name, position) super().__init__(name, position)
self.id = uuid.uuid4() self.id = uuid.uuid4()
self.full_name = name self.full_name = name
self.at = at self.at = at
self.theater = theater self.theater = theater
self.starts_blue = starts_blue self.is_invisible = is_invisible
self.starting_coalition = starting_coalition
self.connected_objectives: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = [] self.helipads: List[PointWithHeading] = []
@@ -414,6 +419,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
# Initialized late because ControlPoints are constructed before the game is. # Initialized late because ControlPoints are constructed before the game is.
self._front_line_db: Database[FrontLine] | None = None self._front_line_db: Database[FrontLine] | None = None
self.influence_radius: ControlPointInfluenceRadius | None = None
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.__class__}: {self.name}>" return f"<{self.__class__}: {self.name}>"
@@ -429,7 +436,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def finish_init(self, game: Game) -> None: def finish_init(self, game: Game) -> None:
assert self._coalition is None assert self._coalition is None
self._coalition = game.coalition_for(self.starts_blue) self._coalition = game.coalition_for(self.starting_coalition)
assert self._front_line_db is None assert self._front_line_db is None
self._front_line_db = game.db.front_lines self._front_line_db = game.db.front_lines
@@ -438,7 +445,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
# the entire game state when it comes up. # the entire game state when it comes up.
from game.sim import GameUpdateEvents from game.sim import GameUpdateEvents
self._create_missing_front_lines(laser_code_registry, GameUpdateEvents()) if self.captured != Player.NEUTRAL:
self._create_missing_front_lines(laser_code_registry, GameUpdateEvents())
@property @property
def front_line_db(self) -> Database[FrontLine]: def front_line_db(self) -> Database[FrontLine]:
@@ -449,9 +457,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self, laser_code_registry: LaserCodeRegistry, events: GameUpdateEvents self, laser_code_registry: LaserCodeRegistry, events: GameUpdateEvents
) -> None: ) -> None:
for connection in self.convoy_routes.keys(): for connection in self.convoy_routes.keys():
if not connection.front_line_active_with( if (
self not connection.front_line_active_with(self)
) and not connection.is_friendly_to(self): and not connection.is_friendly_to(self)
and connection.captured != Player.NEUTRAL
):
self._create_front_line_with(laser_code_registry, connection, events) self._create_front_line_with(laser_code_registry, connection, events)
def _create_front_line_with( def _create_front_line_with(
@@ -485,6 +495,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
@property @property
def has_frontline(self) -> bool: def has_frontline(self) -> bool:
if self.captured.is_neutral:
return False
return bool(self.front_lines) return bool(self.front_lines)
def front_line_active_with(self, other: ControlPoint) -> bool: def front_line_active_with(self, other: ControlPoint) -> bool:
@@ -494,14 +506,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
return self.front_lines[other] return self.front_lines[other]
@property @property
def captured(self) -> bool: def captured(self) -> Player:
return self.coalition.player return self.coalition.player
@property @property
def standard_identity(self) -> StandardIdentity: def standard_identity(self) -> StandardIdentity:
return ( if self.captured.is_neutral:
StandardIdentity.FRIEND if self.captured else StandardIdentity.HOSTILE_FAKER return StandardIdentity.UNKNOWN
) elif self.captured.is_blue:
return StandardIdentity.FRIEND
else:
return StandardIdentity.HOSTILE_FAKER
@property @property
def sidc_status(self) -> Status: def sidc_status(self) -> Status:
@@ -813,7 +828,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
found.append(g) found.append(g)
return found return found
def is_friendly(self, to_player: bool) -> bool: def is_friendly(self, to_player: Player) -> bool:
return self.captured == to_player return self.captured == to_player
def is_friendly_to(self, control_point: ControlPoint) -> bool: def is_friendly_to(self, control_point: ControlPoint) -> bool:
@@ -822,7 +837,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def capture_equipment(self, game: Game) -> None: def capture_equipment(self, game: Game) -> None:
total = self.base.total_armor_value total = self.base.total_armor_value
self.base.armor.clear() self.base.armor.clear()
game.adjust_budget(total, player=not self.captured) game.adjust_budget(
total, player=Player.BLUE if self.captured.is_red else Player.RED
)
game.message( game.message(
f"{self.name} is not connected to any friendly points. Ground " f"{self.name} is not connected to any friendly points. Ground "
f"vehicles have been captured and sold for ${total}M." f"vehicles have been captured and sold for ${total}M."
@@ -851,7 +868,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None: def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None:
value = airframe.price * count value = airframe.price * count
game.adjust_budget(value, player=not self.captured) game.adjust_budget(
value, player=Player.BLUE if self.captured.is_red else Player.RED
)
game.message( game.message(
f"No valid retreat destination in range of {self.name} for {airframe} " f"No valid retreat destination in range of {self.name} for {airframe} "
f"{count} aircraft have been captured and sold for ${value}M." f"{count} aircraft have been captured and sold for ${value}M."
@@ -954,7 +973,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
pass pass
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
new_coalition = game.coalition_for(for_player) new_coalition = game.coalition_for(for_player)
self.ground_unit_orders.refund_all(self.coalition) self.ground_unit_orders.refund_all(self.coalition)
self.retreat_ground_units(game) self.retreat_ground_units(game)
@@ -1119,6 +1138,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
@property @property
def has_active_frontline(self) -> bool: def has_active_frontline(self) -> bool:
if self.captured.is_neutral:
return False
return any(not c.is_friendly(self.captured) for c in self.connected_points) return any(not c.is_friendly(self.captured) for c in self.connected_points)
def front_is_active(self, other: ControlPoint) -> bool: def front_is_active(self, other: ControlPoint) -> bool:
@@ -1205,20 +1226,22 @@ class Airfield(ControlPoint, CTLD):
self, self,
airport: Airport, airport: Airport,
theater: ConflictTheater, theater: ConflictTheater,
starts_blue: bool, starting_coalition: Player,
ctld_zones: Optional[List[Tuple[Point, float]]] = None, ctld_zones: Optional[List[Tuple[Point, float]]] = None,
influence_zone: Optional[List[Tuple[Point, float]]] = None,
) -> None: ) -> None:
super().__init__( super().__init__(
airport.name, airport.name,
airport.position, airport.position,
airport, airport,
theater, theater,
starts_blue, starting_coalition,
cptype=ControlPointType.AIRBASE, cptype=ControlPointType.AIRBASE,
) )
self.airport = airport self.airport = airport
self._runway_status = RunwayStatus() self._runway_status = RunwayStatus()
self.ctld_zones = ctld_zones self.ctld_zones = ctld_zones
self.influence_zone = influence_zone
@property @property
def dcs_airport(self) -> Airport: def dcs_airport(self) -> Airport:
@@ -1244,7 +1267,7 @@ class Airfield(ControlPoint, CTLD):
return True return True
return self.runway_is_operational() return self.runway_is_operational()
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: Player) -> Iterator[FlightType]:
from game.ato import FlightType from game.ato import FlightType
if not self.is_friendly(for_player): if not self.is_friendly(for_player):
@@ -1365,7 +1388,7 @@ class NavalControlPoint(
def is_fleet(self) -> bool: def is_fleet(self) -> bool:
return True return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: Player) -> Iterator[FlightType]:
from game.ato import FlightType from game.ato import FlightType
if self.is_friendly(for_player): if self.is_friendly(for_player):
@@ -1426,6 +1449,7 @@ class NavalControlPoint(
CVN_72, CVN_72,
CVN_73, CVN_73,
CVN_75, CVN_75,
Essex,
]: ]:
return True return True
return False return False
@@ -1473,7 +1497,7 @@ class NavalControlPoint(
class Carrier(NavalControlPoint): class Carrier(NavalControlPoint):
def __init__( def __init__(
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool self, name: str, at: Point, theater: ConflictTheater, starts_blue: Player
): ):
super().__init__( super().__init__(
name, name,
@@ -1488,7 +1512,7 @@ class Carrier(NavalControlPoint):
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
raise RuntimeError("Carriers cannot be captured") raise RuntimeError("Carriers cannot be captured")
@property @property
@@ -1506,9 +1530,14 @@ class Carrier(NavalControlPoint):
return "cv" return "cv"
class EssexCarrier(Carrier):
def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft.lha_capable
class Lha(NavalControlPoint): class Lha(NavalControlPoint):
def __init__( def __init__(
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool self, name: str, at: Point, theater: ConflictTheater, starts_blue: Player
): ):
super().__init__( super().__init__(
name, at, at, theater, starts_blue, cptype=ControlPointType.LHA_GROUP name, at, at, theater, starts_blue, cptype=ControlPointType.LHA_GROUP
@@ -1518,7 +1547,7 @@ class Lha(NavalControlPoint):
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
raise RuntimeError("LHAs cannot be captured") raise RuntimeError("LHAs cannot be captured")
@property @property
@@ -1541,7 +1570,7 @@ class OffMapSpawn(ControlPoint):
return True return True
def __init__( def __init__(
self, name: str, position: Point, theater: ConflictTheater, starts_blue: bool self, name: str, position: Point, theater: ConflictTheater, starts_blue: Player
): ):
super().__init__( super().__init__(
name, name,
@@ -1556,10 +1585,10 @@ class OffMapSpawn(ControlPoint):
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.AIPORT_AIR_BASE return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.AIPORT_AIR_BASE
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None: def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
raise RuntimeError("Off map control points cannot be captured") raise RuntimeError("Off map control points cannot be captured")
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: Player) -> Iterator[FlightType]:
yield from [] yield from []
def total_aircraft_parking(self, parking_type: ParkingType) -> int: def total_aircraft_parking(self, parking_type: ParkingType) -> int:
@@ -1616,14 +1645,18 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
name: str, name: str,
at: Point, at: Point,
theater: ConflictTheater, theater: ConflictTheater,
starts_blue: bool, starts_blue: Player,
ctld_zones: Optional[List[Tuple[Point, float]]] = None, ctld_zones: Optional[List[Tuple[Point, float]]] = None,
is_invisible: bool = False,
influence_zone: Optional[List[Tuple[Point, float]]] = None,
) -> None: ) -> None:
super().__init__( super().__init__(
name, at, at, theater, starts_blue, cptype=ControlPointType.FOB name, at, at, theater, starts_blue, cptype=ControlPointType.FOB
) )
self.name = name self.name = name
self.ctld_zones = ctld_zones self.ctld_zones = ctld_zones
self.is_invisible = is_invisible
self.influence_zone = influence_zone
@property @property
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]: def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
@@ -1649,7 +1682,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
def runway_status(self) -> RunwayStatus: def runway_status(self) -> RunwayStatus:
return RunwayStatus() return RunwayStatus()
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: Player) -> Iterator[FlightType]:
from game.ato import FlightType from game.ato import FlightType
if not self.is_friendly(for_player): if not self.is_friendly(for_player):

View File

@@ -8,6 +8,7 @@ from typing import Any, Iterator, List, TYPE_CHECKING, Tuple
from dcs.mapping import Point from dcs.mapping import Point
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .player import Player
from ..lasercodes.lasercode import LaserCode from ..lasercodes.lasercode import LaserCode
from ..utils import Heading, pairwise from ..utils import Heading, pairwise
@@ -89,19 +90,22 @@ class FrontLine(MissionTarget):
def update_position(self) -> None: def update_position(self) -> None:
self.position = self._compute_position() self.position = self._compute_position()
def control_point_friendly_to(self, player: bool) -> ControlPoint: def control_point_friendly_to(self, player: Player) -> ControlPoint:
if player: if player.is_blue:
return self.blue_cp return self.blue_cp
return self.red_cp return self.red_cp
def control_point_hostile_to(self, player: bool) -> ControlPoint: def control_point_hostile_to(self, player: Player) -> ControlPoint:
return self.control_point_friendly_to(not player) if player.is_blue:
return self.red_cp
else:
return self.blue_cp
def is_friendly(self, to_player: bool) -> bool: def is_friendly(self, to_player: Player) -> bool:
"""Returns True if the objective is in friendly territory.""" """Returns True if the objective is in friendly territory."""
return False return False
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: Player) -> Iterator[FlightType]:
from game.ato import FlightType from game.ato import FlightType
yield from [ yield from [
@@ -210,6 +214,6 @@ class FrontLine(MissionTarget):
raise ValueError( raise ValueError(
"Cannot sort control points that are friendly to each other" "Cannot sort control points that are friendly to each other"
) )
if a.captured: if a.captured.is_blue:
return a, b return a, b
return b, a return b, a

View File

@@ -16,6 +16,7 @@ from game.theater.theatergroundobject import (
TheaterGroundObject, TheaterGroundObject,
) )
from game.theater.theatergroup import IadsGroundGroup from game.theater.theatergroup import IadsGroundGroup
from game.theater.player import Player
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@@ -31,7 +32,7 @@ class SkynetNode:
"""Dataclass for a SkynetNode used in the LUA Data table by the luagenerator""" """Dataclass for a SkynetNode used in the LUA Data table by the luagenerator"""
dcs_name: str dcs_name: str
player: bool player: Player
iads_role: IadsRole iads_role: IadsRole
properties: dict[str, str] = field(default_factory=dict) properties: dict[str, str] = field(default_factory=dict)
connections: dict[str, list[str]] = field(default_factory=lambda: defaultdict(list)) connections: dict[str, list[str]] = field(default_factory=lambda: defaultdict(list))
@@ -62,7 +63,7 @@ class SkynetNode:
def from_group(cls, group: IadsGroundGroup) -> SkynetNode: def from_group(cls, group: IadsGroundGroup) -> SkynetNode:
node = cls( node = cls(
cls.dcs_name_for_group(group), cls.dcs_name_for_group(group),
group.ground_object.is_friendly(True), group.ground_object.coalition.player,
group.iads_role, group.iads_role,
) )
unit_type = group.units[0].unit_type unit_type = group.units[0].unit_type
@@ -314,8 +315,8 @@ class IadsNetwork:
self._make_advanced_connections_by_range(node) self._make_advanced_connections_by_range(node)
def _is_friendly(self, node: IadsNetworkNode, tgo: TheaterGroundObject) -> bool: def _is_friendly(self, node: IadsNetworkNode, tgo: TheaterGroundObject) -> bool:
node_friendly = node.group.ground_object.is_friendly(True) node_friendly = node.group.ground_object.is_friendly(Player.BLUE)
tgo_friendly = tgo.is_friendly(True) tgo_friendly = tgo.is_friendly(Player.BLUE)
return node_friendly == tgo_friendly return node_friendly == tgo_friendly
def _update_network( def _update_network(

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