Compare commits

..

104 Commits

Author SHA1 Message Date
Dan Albert
001e7dfed9 Ignore inconsistent DCS beacon information.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3021.
2023-06-20 18:28:17 -07:00
Dan Albert
cf985d3d37 Test NavMeshLayer. 2023-06-16 22:18:46 -07:00
Dan Albert
1044a1f45f Test FrontLinesLayer. 2023-06-16 22:05:12 -07:00
Starfire13
9fe31859d3 Add Sinai campaign: Exercise Bright Star. 2023-06-16 21:49:13 -07:00
Dan Albert
09417322e7 Partial tests for FrontLine.
We need to mock the backend to usefully test the contextmenu handler.
I'd like to finish all the low hanging fruit before going for that.
2023-06-16 21:39:50 -07:00
Dan Albert
136a9b5f02 Test FlightPlansLayer. 2023-06-16 11:18:16 -07:00
Dan Albert
02f22d4930 Test CullingExclusionZones. 2023-06-16 10:35:31 -07:00
Dan Albert
ca133b9fd1 Add a coverage badge to the readme. 2023-06-15 23:54:19 -07:00
Dan Albert
b1af6dfbe1 Test ControlPointsLayer. 2023-06-15 22:50:39 -07:00
Dan Albert
647d1f57f9 Add tests for CombatLayer. 2023-06-15 22:50:39 -07:00
zhexu14
b250fe2f1e Make waypoint altitudes editable. 2023-06-15 22:41:49 -07:00
Starfire13
3be57bf6bb Add S-300 SAM to Egypt 2000 (#3004)
Adds S-300 as an alternative to Egypt's S-300VM for those who are not
using the High Digits SAM mod.
2023-06-15 22:39:50 -07:00
Dan Albert
3c8d0b023e Test Combat. 2023-06-15 22:24:37 -07:00
Dan Albert
adceb3a224 Add tests for AirDefenseRangeLayer. 2023-06-15 21:58:05 -07:00
zhexu14
ab02cd34c5 bump pyinstaller version
This PR bumps pyinstaller version to fix issues with build.
2023-06-15 12:57:32 -07:00
zhexu14
c74b603d81 restore killed_ground_units as it is relied on to track scenery kills
This PR fixes a regression introduced in
https://github.com/dcs-liberation/dcs_liberation/pull/2792 where
refactoring meant that scenery deaths were not tracked correctly.

This PR has been tested by striking a scenery target and confirming that
it appears in state.json and is updated in Liberation. I've also
confirmed that ground units are tracked.
2023-06-15 03:07:54 +00:00
Nosajthedevil
5815401e73 Update OV10A Weapons files 2023-06-14 20:01:16 -07:00
Starfire13
f463fe50f2 Add Chengdu J-7B to Egypt_2000 2023-06-14 20:00:47 -07:00
Starfire13
f60bf62897 Add missing units to Bluefor Modern 2023-06-14 20:00:32 -07:00
Dan Albert
9d43eb8f03 Handle game over.
The contents are completely uninteresting, but at least it's visible.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/978.
2023-06-12 23:43:26 -07:00
Dan Albert
8f0ca08b89 Move turn passing to post-debrief.
If we're going to show a game over dialog, we need to do so before
moving to the next turn, but we will still want to show the debriefing
window. Move those steps to happen after the debrief window.

https://github.com/dcs-liberation/dcs_liberation/issues/978
2023-06-12 23:43:26 -07:00
Dan Albert
0534f66b30 Allow NGW to be called from anywhere.
Any real end game dialog needs a "new game" button. If only the main
window can usefully call the NGW we'd have to plumb that object through
and call into it from that dialog, which is gross. Just make it easier
to call the wizard.

https://github.com/dcs-liberation/dcs_liberation/issues/978
2023-06-12 22:39:17 -07:00
Nosajthedevil
1162e0aa29 Added and updated weapons files.
Added or updated weapons files including 

The various Hellfire II iterations - this covers fallbacks to rockets
for all 3 Apache variants, the Supercobra, and the Kiowa.
This also adds the 184 Long and 131 pods. 
Lastly, this adds date and fallback information to the shrikes in
advance of the AGM-45B being added to the A-4E mod.
2023-06-13 01:04:14 +00:00
Dan Albert
36c4bb88be Sinai support.
The rest of the work is done, so bump the campaign version and update
the changelog.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2979.
2023-06-12 17:56:38 -07:00
Dan Albert
dc6624a159 Fix Sinai timezone offset.
https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-12 17:56:38 -07:00
Nosajthedevil
8b55331326 Fix F-16A icons, loadouts, and tasking.
Updates the filename of the F-16A banner so that liberation can read
it.

Updates the F-16A.yaml resource file to add BAI / CAS / antiship
mission types - since the 15A is capable of these.

Updated the F-16A payload provide more capability - primarily adding
jamming pods to the centerline, moving the fuel tanks from the
centerline to the inner wings, replacing the AIS_ASQ_T50 on the
wingtips with 120Bs, and changing the CAP loadout to have 120Bs on
pylons 8 and 3 so they fall back to sparrows on historical campaigns.
2023-06-12 17:54:47 -07:00
Starfire13
33ca77e3d1 Add Egyptian faction.
I figured it's a good time to add an Egyptian faction since we now have
the Sinai map.
2023-06-12 17:52:19 -07:00
Dan Albert
b92b01b245 Add Sinai landmap.
https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-12 17:44:57 -07:00
Dan Albert
b18b371904 Basic Sinai support.
Not ready (most importantly no landmap).

https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-11 23:54:41 -07:00
Dan Albert
9c7e16d121 Beacons for Sinai.
https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-11 23:54:41 -07:00
Dan Albert
87e869d963 Fix Scenic Inland yaml, fixing NGW. 2023-06-11 23:48:56 -07:00
Dan Albert
4a059a4f8b Update pydcs.
Includes Sinai terrain export.

https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-11 23:44:37 -07:00
Dan Albert
674254e55b Note button relocation in the changelog. 2023-06-11 21:31:46 -07:00
Dan Albert
9fd0e06c05 Make patch coverage task informational.
Not reasonable to require all PRs to avoid regressing coverage yet...
2023-06-11 21:23:31 -07:00
Dan Albert
ecaf84ea55 Update Fuzzle's campaigns.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2970.
2023-06-11 21:07:46 -07:00
Dan Albert
e4028cb013 Update pydcs.
Includes the updates needed to fix the Gazelle, and a terrain export for
Normandy for the new airfields added in the latest update.

No Sinai support yet.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2984.
2023-06-11 20:48:40 -07:00
Dan Albert
c45ac50370 Make overfull airbase display scrollable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2974.
2023-06-08 04:59:34 +00:00
Dan Albert
6640609caf Move misc buttons to the toolbar.
The top panel is a bit overfull on some displays whenever the weather
description is verbose.
2023-06-08 04:50:31 +00:00
Dan Albert
e44b6b416b Stop preloading images that are rarely used.
The aircraft banners are only used for the unit info window, and that's
not a normal part of gameplay. We spend a bit over 1.5 seconds
preloading this data at startup (about 25% of the non-game load startup
time). This data is only expensive to load in aggregate, and we never
need all of it. The unit info window is not noticeably slowed by this,
but startup is noticeably faster without it.
2023-06-07 05:01:06 +00:00
Dan Albert
8a861d3da5 Remove pointless suffixes on banner files.
I think someone just copied this pattern from the icons where the suffix
represented the icon size. These are definitely not 24px banners, and
some of the suffixes are even wrong (_25).
2023-06-07 05:01:06 +00:00
Dan Albert
380d6551be Add tests for AircraftLayer. 2023-06-06 07:08:57 +00:00
Dan Albert
4cb035b955 Fix Python coverage reporting.
Apparently the fact that I want the coverage report to be XML isn't
enough of a hint that I want coverage.
2023-06-06 03:12:49 +00:00
Dan Albert
e50be9bbde Update bug templates for 7.1.0 release. 2023-06-03 22:27:14 +00:00
Dan Albert
ec49a10135 Configure squadron sizes for Abu Dhabi. 2023-06-03 22:01:15 +00:00
Dan Albert
23e3630169 Fix Black Sea LHA parking limits.
Everything else was within the limits, but I had forgotten to check the
LHAs.
2023-06-03 21:56:53 +00:00
Dan Albert
e20ab5fbc0 Ack campaign versions for new squadron limits.
I haven't tested all of them, but I know these are compatible, so
advertise them as such.
2023-06-03 21:53:49 +00:00
Dan Albert
4fd2bb131b Warn for new squadron rules with old campaigns.
It's not feasible to actually check the parking limits because we can't
identify parking limits for carriers until the theater is populated.
Doing so is expensive (and depends on other NGW inputs). Instead,
compare against the version of the campaign and guess.

A new (minor) campaign version has been introduced which makes this
required to improve the UI hint. Campaigns that are compatible with the
new rules should update their version to advertise support.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2909.
2023-06-03 21:32:42 +00:00
Dan Albert
42a7102948 Disallow air wing generation with overfull bases.
This also changes the window close button of the air wing configuration
dialog to cancel rather than revert and continue, because otherwise
there's no way for the user to back out of the dialog without fixing all
the overfull bases first.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2910.
2023-06-03 20:55:23 +00:00
Dan Albert
d271ff17c2 Show overfull airbase details in air wing config.
https://github.com/dcs-liberation/dcs_liberation/issues/2910
2023-06-03 20:47:56 +00:00
Dan Albert
cb61dfccc4 Show parking capacities in air wing config.
This does show the theoretical parking use of full squadrons even when
the new rules are not enabled. Since limits can be enabled manually
later in the game, it's still useful information, even if it's a bit
misleading.

https://github.com/dcs-liberation/dcs_liberation/issues/2910
2023-06-03 19:31:35 +00:00
Dan Albert
56f93c76eb Add new-game option to show air wing config.
Working on this UI was a huge pain because it required manually creating
a game before the UI could be used.
2023-06-03 19:11:29 +00:00
Dan Albert
36cb3a386c Move CLI game generation after UI init. 2023-06-03 19:11:29 +00:00
Dan Albert
c25e830e6c Factor out game creation parameters in main.
Want to move this deeper into the launch process so that it can use the
UI, but don't want to pass the loosely typed argparse namespace any
more than we have to.
2023-06-03 19:11:29 +00:00
Dan Albert
5d08990cd0 Fix line endings. 2023-06-01 22:49:27 -07:00
Starfire13
2a45cd8899 Add Final Countdown II campaign.
Designed for Normandy 2.0
2023-06-01 22:34:42 -07:00
ColonelAkirNakesh
90b880ec3c Updates china_2010.yaml
Replaces T-55 with Type 59 MBT, adds Type 093 attack sub from China Assets pack
2023-06-01 22:33:11 -07:00
ColonelAkirNakesh
5f0c570d65 Update russia_2010.yaml
Adds Ropucha landing ship, Improved Kilo sub
2023-06-01 22:32:40 -07:00
ColonelAkirNakesh
ce102fcc50 Update allies_1944.yaml
Adds 105mm field howitzer to allies
2023-06-01 22:32:23 -07:00
ColonelAkirNakesh
30c792c15a Enforces Topgun: Maverick Rogue Nation livery for Iranian Tomcat 2023-06-01 22:32:09 -07:00
ColonelAkirNakesh
2f45b856d6 Adds support for Chinese sub Type_093.yaml 2023-06-01 22:31:52 -07:00
ColonelAkirNakesh
31d2b756ab Create TYPE-59.yaml 2023-06-01 22:31:30 -07:00
ColonelAkirNakesh
b5cf889c09 Create Horch_901_typ_40_kfz_21.yaml 2023-06-01 22:31:20 -07:00
ColonelAkirNakesh
19958f91ca Create Pak40.yaml 2023-06-01 22:31:08 -07:00
ColonelAkirNakesh
c775a898a4 Create Wespe124.yaml 2023-06-01 22:30:55 -07:00
ColonelAkirNakesh
535244f6f3 Create LeFH_18-40-105.yaml 2023-06-01 22:30:42 -07:00
ColonelAkirNakesh
9d1d3bdcfa Create Higgins_boat.yaml 2023-06-01 22:29:45 -07:00
ColonelAkirNakesh
36eef2b1b9 Create M2A1-105.yaml 2023-06-01 22:29:27 -07:00
ColonelAkirNakesh
7788425c5c Create IMPROVED_KILO.yaml 2023-06-01 22:28:57 -07:00
ColonelAkirNakesh
ee0c21b3e5 Create BDK-775.yaml 2023-06-01 22:28:43 -07:00
ColonelAkirNakesh
54cd619f75 Create santafe.yaml 2023-06-01 22:28:27 -07:00
ColonelAkirNakesh
051940e23c Create leander-gun-condell.yaml 2023-06-01 22:28:13 -07:00
ColonelAkirNakesh
4fbd7defa3 Create leander-gun-lynch.yaml 2023-06-01 22:27:59 -07:00
Dan Albert
90bda9383d Add missing note about 7.0.0 -> 7.1.0 save compat. 2023-05-31 00:17:02 -07:00
Dan Albert
7798e2970c Minor campaign version bump for Normandy 2.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2804.
2023-05-30 23:49:42 -07:00
Dan Albert
410c25b331 Update beacon data.
Did this for Normandy 2... but unsurprisingly there aren't a whole lot
of beacons in a WW2 map.
2023-05-30 23:49:42 -07:00
Dan Albert
cff74525d6 Update pydcs.
Normandy 2 support.

https://github.com/dcs-liberation/dcs_liberation/issues/2804
2023-05-30 23:49:42 -07:00
Dan Albert
8b7f107044 Update Normandy landmap for Normandy 2.
https://github.com/dcs-liberation/dcs_liberation/issues/2804
2023-05-30 23:24:58 -07:00
Dan Albert
c365a0d739 Add Normandy 2 landmap inputs.
https://github.com/dcs-liberation/dcs_liberation/issues/2804
2023-05-30 23:24:58 -07:00
Dan Albert
1f4fd0fd04 Force polygons into validity during GIS import.
Not sure why, but some polygons become invalid (which usually means a
self-intersecting "polygon", such as two triangles that meet at a point)
during this transformation. Shapely includes a tool to reshape polygons
into validity, so use that.
2023-05-30 23:24:58 -07:00
Dan Albert
4bb60cb500 Tolerate empty settings files. 2023-05-30 23:24:58 -07:00
Dan Albert
fe96a415be Add settings for battlefield commander slots.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2235.
2023-05-30 22:06:47 -07:00
Dan Albert
6699289bf7 Add performance option to prevent missile tasks.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2508.
2023-05-30 21:47:16 -07:00
Dan Albert
a85d3243fb Add changelog note for BAI fix.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2922.
2023-05-30 21:12:45 -07:00
Dan Albert
7f2607cf08 Replace more Patriot STRs with real EWRs.
Not all of these nations actually field this radar (according to
Wikipedia), but at least it's a real EWR, and it's the only blue one
we've got.
2023-05-30 21:05:15 -07:00
zhexu14
e50ee976ed Add ability to convert landmap to/from miz.
This PR adds utility functions that import/export landmap files to .miz
polygons. In addition to the unit test, this PR has been tested by
writing the Caucuses & Syria landmaps to a .miz file, loading the
generated .miz file back in and checking that the loaded landmap object
is identical to the original files.
2023-05-30 21:01:05 -07:00
ColonelAkirNakesh
29ffb526f2 Replaces Patriot STR with AN/FPS-117 EWR, adds USS Harry Truman 2023-05-30 20:41:00 -07:00
zhexu14
e024013093 issue 2922: make BAI plannable against missile and costal sites 2023-05-30 20:39:39 -07:00
Dan Albert
257dabe4fa Fix formatting of takeoff time. 2023-05-25 22:35:50 -07:00
Dan Albert
406fb61fa4 Add UI for TOT offset adjustment.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2912.
2023-05-25 22:35:50 -07:00
Dan Albert
49dfa95c61 Save the TOT offset in the flight plan.
Prep work for exposing this to the UI.
2023-05-25 22:35:50 -07:00
Dan Albert
c80e5b259f Allow save compat to exist for two versions.
We want to clean up eventually, but allowing it to exist in both develop
and the release branch makes cherry picks easier.
2023-05-25 22:35:50 -07:00
Dan Albert
64e2213f28 Make the flight details menu modal.
Prevents players from accidentally deleting flights they're currently
viewing, which would cause an error.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2911.
2023-05-25 21:10:21 -07:00
Dan Albert
ced93afd49 Update bug templates now that 7.0.0 is out. 2023-05-25 21:10:21 -07:00
Dan Albert
f719a5ec34 Update bug templates now that 7.0.0 is out. 2023-05-23 01:38:16 -07:00
Dan Albert
6f4ac1dc39 Fix line endings.
These get broken whenever someone uses the GitHub file upload editor,
since that doesn't understand .gitattributes.
2023-05-23 00:37:28 -07:00
Starfire13
f831c8efdd Update Golan Heights and Caen to Evreux campaigns.
I had asked Khopa for permission to edit two of his campaigns to fix
some issues. Only the YAMLs have been edited, .miz files did not need
changes. I have tested both YAMLs to make sure campaigns will generate.
Also tested generating turn 1 .miz and ran it in DCS.

Golan Heights:
1. Removed the 2 problematic squadrons from Marj Ruhayyil that were
causing aircraft losses due to larger aircraft sizes not fitting at that
airfield (which is intended for helicopters).
2. Implemented squadron limits.

Caen to Evreux:
1. Re-arranged squadrons for better force distribution and revised
primary and secondary mission types for better default play experience.
2. Implemented squadron limits.
2023-05-23 00:23:41 -07:00
Starfire13
e3c6b03603 Update Apache loadouts.
BAI loadout updated to use the new radar guided hellfires. Aux tanks
removed in favour of extra cannon ammo.
2023-05-20 09:21:16 +00:00
Dan Albert
7a2e8279cd Fix syntax error in bluefor_modern.yaml. 2023-05-19 17:51:22 -07:00
Dan Albert
24e72475b4 Checkpoint game before sim, auto-revert on abort.
An alternative to
https://github.com/dcs-liberation/dcs_liberation/pull/2891 that I ended
up liking much better (I had assumed some part of the UI would fail or
at least look terrible with this approach, but it seems to work quite
well).

On by default now since it's far less frightening than the previous
thing.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2735.
2023-05-19 17:44:18 -07:00
Starfire13
f10350dac4 Update scenic_inland.yaml
A formatting fix for scenic route 2 that was preventing new campaign start. Fixing at Fuzzle's request as he doesn't have the time for it right now.
2023-05-19 17:36:29 -07:00
Dan Albert
f068976749 Fuzzle campaign updates.
https://github.com/dcs-liberation/dcs_liberation/issues/2889
2023-05-19 01:17:51 -07:00
Dan Albert
4b4c45e90f Attempt to reset the simulation on abort.
This is optional because I really don't know if I trust it. I don't see
much wrong with it (aside from the warning about not using it with auto-
resolve, because it won't restore lost aircraft), but it's really not
something I'd built for since it's not going to be possible as the RTS
features grow.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2735.
2023-05-19 01:05:25 -07:00
Dan Albert
527eac1f4a Fix docs version.
This ought to be auto-imported but that requires liberation being
importable here and that's proving non-trivial.
2023-05-18 23:17:08 -07:00
Dan Albert
92c3087187 Advance develop to 8.0.0-preview. 2023-05-18 22:44:39 -07:00
255 changed files with 3322 additions and 258 deletions

View File

@@ -31,7 +31,7 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 6.1.1
- 7.1.0
- Development build
- type: textarea
attributes:
@@ -53,9 +53,9 @@ body:
description: >
Attach any files needed to reproduce the bug here. **A save game is
required.** We typically cannot help without a save game (the
`.liberation` (or `.liberation.zip`, for 7.x) file found in
`%USERPROFILE%/Saved Games/DCS/Liberation/Saves`), so most bugs filed
without saved games will be closed without investigation.
`.liberation.zip` file found in `%USERPROFILE%/Saved
Games/DCS/Liberation/Saves`), so most bugs filed without saved games
will be closed without investigation.
Other useful files to include are:
@@ -73,10 +73,7 @@ body:
The `state.json` file for the most recently completed turn, located at
`<Liberation install directory>/state.json`. This file is essential for
investigating any issues with end-of-turn results processing. **If you
include this file, also include `last_turn.liberation`** (unless the
save is from 7.x or newer, which includes that information in the save
automatically).
investigating any issues with end-of-turn results processing.
You can attach files to the bug by dragging and dropping the file into

View File

@@ -39,7 +39,7 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 6.1.1
- 7.1.0
- Development build
- type: textarea
attributes:

View File

@@ -15,7 +15,7 @@ jobs:
- name: run tests
run: |
./venv/scripts/activate
pytest --cov-report=xml tests
pytest --cov --cov-report=xml tests
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ venv
.vscode/settings.json
dist/**
/.coverage
/coverage.xml
# User-specific stuff
.idea/
.env

View File

@@ -8,6 +8,7 @@
[![Discord](https://img.shields.io/discord/595702951800995872?label=Discord&logo=discord)](https://discord.gg/bKrtrkJ)
[![codecov](https://codecov.io/gh/dcs-liberation/dcs_liberation/branch/develop/graph/badge.svg?token=EEQ7G76K2L)](https://codecov.io/gh/dcs-liberation/dcs_liberation)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/dcs-liberation/dcs_liberation?style=social)

View File

@@ -1,3 +1,21 @@
# 8.0.0
Saves from 7.x are not compatible with 8.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41066, including the new Sinai map.
* **[UI]** Limited size of overfull airbase display and added scrollbar.
* **[UI]** Waypoint altitudes can be edited in Waypoints tab of Edit Flight window.
* **[UI]** Moved air wing and transfer menus to the toolbar to improve UI fit on low resolution displays.
* **[UI]** Added basic game over dialog.
## Fixes
* **[Campaign]** Fix bug introduced in 7.0 where map strike target deaths are no longer tracked.
* **[Mission Generation]** Fix crash during mission generation caused by out of date DCS data for the Gazelle.
* **[Mission Generation]** Fix crash during mission generation when DCS beacon data is inconsistent.
# 7.1.0
Saves from 7.0.0 are compatible with 7.1.0

View File

@@ -1,11 +1,11 @@
import App from "./App";
import { store } from "./app/store";
import { setupStore } from "./app/store";
import { render } from "@testing-library/react";
import { Provider } from "react-redux";
test("app renders", () => {
render(
<Provider store={store}>
<Provider store={setupStore()}>
<App />
</Provider>
);

View File

@@ -3,36 +3,48 @@ import combatReducer from "../api/combatSlice";
import controlPointsReducer from "../api/controlPointsSlice";
import flightsReducer from "../api/flightsSlice";
import frontLinesReducer from "../api/frontLinesSlice";
import iadsNetworkReducer from "../api/iadsNetworkSlice";
import mapReducer from "../api/mapSlice";
import navMeshReducer from "../api/navMeshSlice";
import supplyRoutesReducer from "../api/supplyRoutesSlice";
import tgosReducer from "../api/tgosSlice";
import iadsNetworkReducer from "../api/iadsNetworkSlice";
import threatZonesReducer from "../api/threatZonesSlice";
import unculledZonesReducer from "../api/unculledZonesSlice";
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
import unculledZonesReducer from "../api/unculledZonesSlice";
import {
Action,
PreloadedState,
ThunkAction,
combineReducers,
configureStore,
} from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {
combat: combatReducer,
controlPoints: controlPointsReducer,
flights: flightsReducer,
frontLines: frontLinesReducer,
map: mapReducer,
navmeshes: navMeshReducer,
supplyRoutes: supplyRoutesReducer,
iadsNetwork: iadsNetworkReducer,
tgos: tgosReducer,
threatZones: threatZonesReducer,
[baseApi.reducerPath]: baseApi.reducer,
unculledZones: unculledZonesReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
const rootReducer = combineReducers({
combat: combatReducer,
controlPoints: controlPointsReducer,
flights: flightsReducer,
frontLines: frontLinesReducer,
map: mapReducer,
navmeshes: navMeshReducer,
supplyRoutes: supplyRoutesReducer,
iadsNetwork: iadsNetworkReducer,
tgos: tgosReducer,
threatZones: threatZonesReducer,
[baseApi.reducerPath]: baseApi.reducer,
unculledZones: unculledZonesReducer,
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export function setupStore(preloadedState?: PreloadedState<RootState>) {
return configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
preloadedState: preloadedState,
});
}
export type AppStore = ReturnType<typeof setupStore>;
export type AppDispatch = AppStore["dispatch"];
export type RootState = ReturnType<typeof rootReducer>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,

View File

@@ -0,0 +1,53 @@
import { renderWithProviders } from "../../testutils";
import AircraftLayer from "./AircraftLayer";
import { PropsWithChildren } from "react";
const mockLayerGroup = jest.fn();
const mockMarker = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Marker: (props: any) => {
mockMarker(props);
},
}));
test("layer is empty by default", async () => {
renderWithProviders(<AircraftLayer />);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockMarker).not.toHaveBeenCalled();
});
test("layer has aircraft if non-empty", async () => {
renderWithProviders(<AircraftLayer />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
position: {
lat: 0,
lng: 0,
},
},
bar: {
id: "bar",
blue: false,
sidc: "",
position: {
lat: 0,
lng: 0,
},
},
},
selected: null,
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockMarker).toHaveBeenCalledTimes(2);
});

View File

@@ -0,0 +1,146 @@
import { renderWithProviders } from "../../testutils";
import AirDefenseRangeLayer, { colorFor } from "./AirDefenseRangeLayer";
import { PropsWithChildren } from "react";
const mockLayerGroup = jest.fn();
const mockCircle = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Circle: (props: any) => {
mockCircle(props);
},
}));
describe("colorFor", () => {
it("has a unique color for each configuration", () => {
const params = [
[false, false],
[false, true],
[true, false],
[true, true],
];
var colors = new Set<string>();
for (const [blue, detection] of params) {
colors.add(colorFor(blue, detection));
}
expect(colors.size).toEqual(4);
});
});
describe("AirDefenseRangeLayer", () => {
it("draws nothing when there are no TGOs", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).not.toHaveBeenCalled();
});
it("does not draw wrong range types", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: false,
position: {
lat: 0,
lng: 0,
},
units: [],
threat_ranges: [],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).not.toHaveBeenCalled();
});
it("draws threat ranges", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: true,
position: {
lat: 10,
lng: 20,
},
units: [],
threat_ranges: [10],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 10,
lng: 20,
},
radius: 10,
color: colorFor(true, false),
interactive: false,
})
);
});
it("draws detection ranges", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} detection />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: true,
position: {
lat: 10,
lng: 20,
},
units: [],
threat_ranges: [10],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 10,
lng: 20,
},
radius: 20,
color: colorFor(true, true),
interactive: false,
})
);
});
});

View File

@@ -9,7 +9,7 @@ interface TgoRangeCirclesProps {
detection?: boolean;
}
function colorFor(blue: boolean, detection: boolean) {
export function colorFor(blue: boolean, detection: boolean) {
if (blue) {
return detection ? "#bb89ff" : "#0084ff";
}

View File

@@ -0,0 +1,132 @@
import { renderWithProviders } from "../../testutils";
import Combat from "./Combat";
import { LatLng } from "leaflet";
const mockPolyline = jest.fn();
const mockPolygon = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: any) => {
mockPolyline(props);
},
Polygon: (props: any) => {
mockPolygon(props);
},
}));
describe("Combat", () => {
describe("footprint", () => {
it("is not interactive", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [[new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)]],
}}
/>
);
expect(mockPolygon).toBeCalledWith(
expect.objectContaining({ interactive: false })
);
});
// Fails because we don't handle multi-poly combat footprints correctly.
it.skip("renders single polygons", () => {
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [boundary],
}}
/>
);
expect(mockPolygon).toBeCalledWith(
expect.objectContaining({ positions: boundary })
);
});
// Fails because we don't handle multi-poly combat footprints correctly.
it.skip("renders multiple polygons", () => {
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [boundary, boundary],
}}
/>
);
expect(mockPolygon).toBeCalledTimes(2);
});
});
describe("lines", () => {
it("is not interactive", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(1, 0)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledWith(
expect.objectContaining({ interactive: false })
);
});
it("renders single line", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledWith(
expect.objectContaining({
positions: [new LatLng(0, 0), new LatLng(0, 1)],
})
);
});
it("renders multiple lines", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1), new LatLng(1, 0)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledTimes(2);
});
});
it("renders nothing if no footprint or targets", () => {
const { container } = renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: null,
footprint: null,
}}
/>
);
expect(container).toBeEmptyDOMElement();
});
});

View File

@@ -0,0 +1,48 @@
import { renderWithProviders } from "../../testutils";
import CombatLayer from "./CombatLayer";
import { LatLng } from "leaflet";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
describe("CombatLayer", () => {
it("renders each combat", () => {
renderWithProviders(<CombatLayer />, {
preloadedState: {
combat: {
combat: {
foo: {
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
},
bar: {
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
},
},
},
},
});
expect(mockPolyline).toBeCalledTimes(2);
});
it("renders LayerGroup but no contents if no combat", () => {
renderWithProviders(<CombatLayer />);
expect(mockLayerGroup).toBeCalledTimes(1);
expect(mockPolyline).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,52 @@
import { renderWithProviders } from "../../testutils";
import ControlPointsLayer from "./ControlPointsLayer";
import { LatLng } from "leaflet";
import { PropsWithChildren } from "react";
const mockMarker = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Marker: (props: any) => {
mockMarker(props);
},
}));
describe("ControlPointsLayer", () => {
it("renders each control point", () => {
renderWithProviders(<ControlPointsLayer />, {
preloadedState: {
controlPoints: {
controlPoints: {
foo: {
id: "foo",
name: "Foo",
blue: true,
position: new LatLng(0, 0),
mobile: false,
sidc: "",
},
bar: {
id: "bar",
name: "Bar",
blue: false,
position: new LatLng(1, 0),
mobile: false,
sidc: "",
},
},
},
},
});
expect(mockMarker).toBeCalledTimes(2);
});
it("renders LayerGroup but no contents if no combat", () => {
renderWithProviders(<ControlPointsLayer />);
expect(mockLayerGroup).toBeCalledTimes(1);
expect(mockMarker).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,78 @@
import { renderWithProviders } from "../../testutils";
import CullingExclusionZones from "./CullingExclusionZones";
import { PropsWithChildren } from "react";
const mockCircle = jest.fn();
const mockLayerGroup = jest.fn();
const mockLayerControlOverlay = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
LayersControl: {
Overlay: (props: PropsWithChildren<any>) => {
mockLayerControlOverlay(props);
return <>{props.children}</>;
},
},
Circle: (props: any) => {
mockCircle(props);
},
}));
describe("CullingExclusionZones", () => {
it("is empty there are no exclusion zones", () => {
renderWithProviders(<CullingExclusionZones />);
expect(mockCircle).not.toHaveBeenCalled();
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockLayerControlOverlay).toHaveBeenCalledTimes(1);
});
describe("zone circles", () => {
it("are drawn in the correct locations", () => {
renderWithProviders(<CullingExclusionZones />, {
preloadedState: {
unculledZones: {
zones: [
{
position: {
lat: 0,
lng: 0,
},
radius: 10,
},
{
position: {
lat: 1,
lng: 1,
},
radius: 2,
},
],
},
},
});
expect(mockCircle).toHaveBeenCalledTimes(2);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 0,
lng: 0,
},
radius: 10,
})
);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 1,
lng: 1,
},
radius: 2,
})
);
});
it("are not interactive", () => {});
});
});

View File

@@ -30,18 +30,10 @@ const CullingExclusionCircles = (props: CullingExclusionCirclesProps) => {
export default function CullingExclusionZones() {
const data = useAppSelector(selectUnculledZones).zones;
var cez = <></>;
if (!data) {
console.log("Empty response when loading culling exclusion zones");
} else {
cez = (
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
);
}
return (
<LayersControl.Overlay name="Culling exclusion zones">
{cez}
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
</LayersControl.Overlay>
);
}

View File

@@ -0,0 +1,405 @@
import { renderWithProviders } from "../../testutils";
import FlightPlansLayer from "./FlightPlansLayer";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("FlightPlansLayer", () => {
describe("unselected flights", () => {
it("are drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn if wrong coalition", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: false,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn when only selected flights are to be drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} selectedOnly />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});
describe("selected flights", () => {
it("are drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn twice", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn if red", () => {
renderWithProviders(<FlightPlansLayer blue={false} selectedOnly />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: false,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});
it("are not drawn if there are no flights", () => {
renderWithProviders(<FlightPlansLayer blue={true} />);
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});

View File

@@ -0,0 +1,32 @@
import { renderWithProviders } from "../../testutils";
import FrontLine from "./FrontLine";
import { PolylineProps } from "react-leaflet";
const mockPolyline = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: PolylineProps) => {
mockPolyline(props);
},
}));
describe("FrontLine", () => {
it("is drawn in the correct location", () => {
const extents = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 0 },
];
renderWithProviders(
<FrontLine
front={{
id: "",
extents: extents,
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: extents,
})
);
});
});

View File

@@ -0,0 +1,56 @@
import { renderWithProviders } from "../../testutils";
import FrontLinesLayer from "./FrontLinesLayer";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("FrontLinesLayer", () => {
it("draws nothing when there are no front lines", () => {
renderWithProviders(<FrontLinesLayer />);
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
it("draws front lines", () => {
const extents = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 1 },
];
renderWithProviders(<FrontLinesLayer />, {
preloadedState: {
frontLines: {
fronts: {
foo: {
id: "foo",
extents: extents,
},
bar: {
id: "bar",
extents: extents,
},
},
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: extents,
})
);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
});

View File

@@ -3,7 +3,7 @@ import { useAppSelector } from "../../app/hooks";
import FrontLine from "../frontline";
import { LayerGroup } from "react-leaflet";
export default function SupplyRoutesLayer() {
export default function FrontLinesLayer() {
const fronts = useAppSelector(selectFrontLines).fronts;
return (
<LayerGroup>

View File

@@ -0,0 +1,125 @@
import { renderWithProviders } from "../../testutils";
import NavMeshLayer from "./NavMeshLayer";
import { PropsWithChildren } from "react";
const mockPolygon = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polygon: (props: any) => {
mockPolygon(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("NavMeshLayer", () => {
it("draws blue meshes", () => {
const poly1 = [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 1 },
{ lat: 1, lng: 0 },
],
];
const poly2 = [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: -1 },
{ lat: 1, lng: 0 },
],
];
renderWithProviders(<NavMeshLayer blue={true} />, {
preloadedState: {
navmeshes: {
blue: [
{
poly: poly1,
threatened: false,
},
{
poly: poly2,
threatened: true,
},
],
red: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 2 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
],
},
},
});
expect(mockPolygon).toHaveBeenCalledTimes(2);
expect(mockPolygon).toHaveBeenCalledWith(
expect.objectContaining({
fillColor: "#00ff00",
positions: poly1,
interactive: false,
})
);
expect(mockPolygon).toHaveBeenCalledWith(
expect.objectContaining({
fillColor: "#ff0000",
positions: poly2,
interactive: false,
})
);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
it("draws red navmesh", () => {
renderWithProviders(<NavMeshLayer blue={false} />, {
preloadedState: {
navmeshes: {
blue: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 1 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: -1 },
{ lat: 1, lng: 0 },
],
],
threatened: true,
},
],
red: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 2 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
],
},
},
});
expect(mockPolygon).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,5 +1,5 @@
import App from "./App";
import { store } from "./app/store";
import { setupStore } from "./app/store";
import { SocketProvider } from "./components/socketprovider/socketprovider";
import "./index.css";
import * as serviceWorker from "./serviceWorker";
@@ -12,7 +12,7 @@ const root = ReactDOM.createRoot(
);
root.render(
<React.StrictMode>
<Provider store={store}>
<Provider store={setupStore()}>
<SocketProvider>
<App />
</SocketProvider>

View File

@@ -0,0 +1,30 @@
// https://redux.js.org/usage/writing-tests
import { setupStore } from "../app/store";
import type { AppStore, RootState } from "../app/store";
import type { PreloadedState } from "@reduxjs/toolkit";
import { render } from "@testing-library/react";
import type { RenderOptions } from "@testing-library/react";
import React, { PropsWithChildren } from "react";
import { Provider } from "react-redux";
// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
preloadedState?: PreloadedState<RootState>;
store?: AppStore;
}
export function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
}: ExtendedRenderOptions = {}
) {
function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
return <Provider store={store}>{children}</Provider>;
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

View File

@@ -2,7 +2,7 @@ coverage:
status:
patch:
default:
informational: false
informational: true
project:
default:
informational: true

View File

@@ -9,7 +9,7 @@
project = "DCS Liberation"
copyright = "2023, DCS Liberation Team"
author = "DCS Liberation Team"
release = "7.1.0"
release = "8.0.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@@ -133,6 +133,7 @@ class StateData:
+ data["kill_events"]
+ data["crash_events"]
+ data["dead_events"]
+ data["killed_ground_units"]
)
for unit in killed_units: # organize killed units into aircraft vs ground
if unit_map.flight(unit) is not None:

View File

@@ -5,7 +5,6 @@ import logging
import math
from collections.abc import Iterator
from datetime import date, datetime, time, timedelta
from enum import Enum
from typing import Any, List, TYPE_CHECKING, Type, Union, cast
from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers
@@ -37,6 +36,7 @@ from .theater.theatergroundobject import (
)
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .timeofday import TimeOfDay
from .turnstate import TurnState
from .weather.conditions import Conditions
if TYPE_CHECKING:
@@ -81,12 +81,6 @@ AWACS_BUDGET_COST = 4
PLAYER_BUDGET_IMPORTANCE_LOG = 2
class TurnState(Enum):
WIN = 0
LOSS = 1
CONTINUE = 2
class Game:
def __init__(
self,

View File

@@ -1,6 +1,7 @@
"""Runway information and selection."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Iterator, Optional, TYPE_CHECKING
@@ -51,7 +52,20 @@ class RunwayData:
atc = atc_radio.uhf
for beacon_data in airport.beacons:
beacon = Beacons.with_id(beacon_data.id, theater)
try:
beacon = Beacons.with_id(beacon_data.id, theater)
except KeyError:
# DCS data is not always correct. At time of writing, Hatzor in Sinai
# claims to have a beacon named airfield20_0, but the Sinai beacons.lua
# has no such beacon.
# See https://github.com/dcs-liberation/dcs_liberation/issues/3021.
logging.exception(
"Airport %s claims to have beacon %s but the map has no beacon "
"with that ID",
airport.name,
beacon_data.id,
)
continue
if beacon.is_tacan:
tacan = beacon.tacan_channel
tacan_callsign = beacon.callsign

View File

@@ -4,10 +4,17 @@ from functools import cached_property
from typing import Optional, Tuple, Union
import logging
from pathlib import Path
from typing import List
from shapely import geometry
from shapely.geometry import MultiPolygon, Polygon
from dcs.drawing.drawing import LineStyle, Rgba
from dcs.drawing.polygon import FreeFormPolygon
from dcs.mapping import Point
from dcs.mission import Mission
from dcs.terrain.terrain import Terrain
@dataclass(frozen=True)
class Landmap:
@@ -39,3 +46,94 @@ def load_landmap(filename: Path) -> Optional[Landmap]:
def poly_contains(x: float, y: float, poly: Union[MultiPolygon, Polygon]) -> bool:
return poly.contains(geometry.Point(x, y))
def to_miz(landmap: Landmap, terrain: Terrain, mission_filename: str) -> None:
"""
Writes landmap to .miz file so that zones can be visualized and edited in the
mission editor.
"""
def multi_polygon_to_miz(
mission: Mission,
terrain: Terrain,
multi_polygon: MultiPolygon,
color: Rgba,
prefix: str,
layer_index: int = 4,
layer_name: str = "Author",
) -> None:
reference_position = Point(0, 0, terrain)
for i in range(len(multi_polygon.geoms)):
polygon = multi_polygon.geoms[i]
if len(polygon.interiors) > 0:
raise ValueError(
f"Polygon hole found when trying to export {prefix} {i}. to_miz() does not support landmap zones with holes."
)
coordinates = polygon.exterior.xy
points = []
for j in range(len(coordinates[0])):
points.append(Point(coordinates[0][j], coordinates[1][j], terrain))
polygon_drawing = FreeFormPolygon(
visible=True,
position=reference_position,
name=f"{prefix}-{i}",
color=color,
layer_name=layer_name,
fill=color,
line_thickness=10,
line_style=LineStyle.Solid,
points=points,
)
mission.drawings.layers[layer_index].objects.append(polygon_drawing)
mission = Mission(terrain=terrain)
multi_polygon_to_miz(
mission, terrain, landmap.exclusion_zones, Rgba(255, 0, 0, 128), "Exclusion"
)
multi_polygon_to_miz(
mission, terrain, landmap.sea_zones, Rgba(0, 0, 255, 128), "Sea"
)
multi_polygon_to_miz(
mission, terrain, landmap.inclusion_zones, Rgba(0, 255, 0, 128), "Inclusion"
)
mission.save(mission_filename)
def from_miz(mission_filename: str, layer_index: int = 4) -> Landmap:
"""
Generate Landmap object from Free Form Polygons drawn in a .miz file.
Landmap.inclusion_zones are expected to be named Inclusion-<suffix>
Landmap.exclusion_zones are expected to be named Exclusion-<suffix>
Landmap.sea_zones are expected to be named Sea-<suffix>
"""
mission = Mission()
mission.load_file(mission_filename)
polygons: dict[str, List[Polygon]] = {"Inclusion": [], "Exclusion": [], "Sea": []}
for draw_object in mission.drawings.layers[layer_index].objects:
if type(draw_object) != FreeFormPolygon:
logging.debug(
f"Object {draw_object.name} is not a FreeFormPolygon, ignoring"
)
continue
name_split = draw_object.name.split(
"-"
) # names are in the format <Inclusion|Exclusion|Sea>-<suffix>
zone_type = name_split[0]
if len(name_split) != 2 or zone_type not in ("Exclusion", "Sea", "Inclusion"):
logging.debug(
f"Object name {draw_object.name} does not conform to expected format <Exclusion|Sea|Inclusion>-<suffix>, ignoring"
)
continue
polygon_points = []
for point in draw_object.points:
polygon_points.append(
(point.x + draw_object.position.x, point.y + draw_object.position.y)
)
polygons[zone_type].append(Polygon(polygon_points))
landmap = Landmap(
inclusion_zones=MultiPolygon(polygons["Inclusion"]),
exclusion_zones=MultiPolygon(polygons["Exclusion"]),
sea_zones=MultiPolygon(polygons["Sea"]),
)
return landmap

View File

@@ -14,6 +14,7 @@ from dcs.terrain import (
Nevada,
Normandy,
PersianGulf,
Sinai,
Syria,
TheChannel,
)
@@ -31,6 +32,7 @@ ALL_TERRAINS = [
MarianaIslands(),
Nevada(),
TheChannel(),
Sinai(),
Syria(),
]

9
game/turnstate.py Normal file
View File

@@ -0,0 +1,9 @@
from __future__ import annotations
from enum import Enum
class TurnState(Enum):
WIN = 0
LOSS = 1
CONTINUE = 2

View File

@@ -1,8 +1,8 @@
from pathlib import Path
MAJOR_VERSION = 7
MINOR_VERSION = 1
MAJOR_VERSION = 8
MINOR_VERSION = 0
MICRO_VERSION = 0
VERSION_NUMBER = ".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
@@ -182,4 +182,7 @@ VERSION = _build_version_string()
#: Version 10.9
#: * Campaign is compatible with new squadron rules. The default air wing configuration
#: has enough parking available at each base when squadrons begin at full strength.
CAMPAIGN_FORMAT_VERSION = (10, 9)
#:
#: Version 10.10
#: * Support for Sinai.
CAMPAIGN_FORMAT_VERSION = (10, 10)

View File

@@ -9,8 +9,14 @@ from pydcs_extensions.weapon_injector import inject_weapons
class WeaponsOV10A:
LAU_33A = {"clsid": "{LAU-33A}", "name": "LAU-33A", "weight": 155}
Mk4_mod_0 = {"clsid": "{MK4_Mod0_OV10}", "name": "Mk4 mod 0", "weight": 612.35}
OV10_SMOKE = {"clsid": "{OV10_SMOKE}", "name": "OV10_SMOKE", "weight": 1}
ParaTrooper = {"clsid": "{PARA}", "name": "ParaTrooper", "weight": 80}
OV10_Paratrooper = {
"clsid": "OV10_Paratrooper",
"name": "OV10_Paratrooper",
"weight": 400,
}
Fuel_Tank_150_gallons_ = {
"clsid": "{150gal}",
"name": "Fuel Tank 150 gallons",
@@ -47,6 +53,11 @@ class Bronco_OV_10A(PlaneType):
1,
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
)
LAU_7_with_AIM_9B_Sidewinder_IR_AAM = (
1,
Weapons.LAU_7_with_AIM_9B_Sidewinder_IR_AAM,
)
LAU_33A = (1, Weapons.LAU_33A)
# ERRR {MK-81}
@@ -61,6 +72,7 @@ class Bronco_OV_10A(PlaneType):
LAU3_HE5 = (2, Weapons.LAU3_HE5)
LAU3_HE151 = (2, Weapons.LAU3_HE151)
M260_HYDRA = (2, Weapons.M260_HYDRA)
M260_HYDRA_WP = (2, Weapons.M260_HYDRA_WP)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
2,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
@@ -69,6 +81,62 @@ class Bronco_OV_10A(PlaneType):
2,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
2,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos = (
2,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
2,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
2,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
# ERRR {MK-81}
@@ -83,6 +151,7 @@ class Bronco_OV_10A(PlaneType):
LAU3_HE5 = (3, Weapons.LAU3_HE5)
LAU3_HE151 = (3, Weapons.LAU3_HE151)
M260_HYDRA = (3, Weapons.M260_HYDRA)
M260_HYDRA_WP = (3, Weapons.M260_HYDRA_WP)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
3,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
@@ -91,6 +160,62 @@ class Bronco_OV_10A(PlaneType):
3,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
3,
Weapons.LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
3,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
3,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos = (
3,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
3,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
3,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
class Pylon4:
Fuel_Tank_150_gallons_ = (4, Weapons.Fuel_Tank_150_gallons_)
@@ -99,6 +224,7 @@ class Bronco_OV_10A(PlaneType):
Mk_82_Snakeye___500lb_GP_Bomb_HD = (4, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
Mk_83___1000lb_GP_Bomb_LD = (4, Weapons.Mk_83___1000lb_GP_Bomb_LD)
M117___750lb_GP_Bomb_LD = (4, Weapons.M117___750lb_GP_Bomb_LD)
Mk4_mod_0 = (4, Weapons.Mk4_mod_0)
# ERRR {MK-81}
@@ -113,6 +239,7 @@ class Bronco_OV_10A(PlaneType):
LAU3_HE5 = (5, Weapons.LAU3_HE5)
LAU3_HE151 = (5, Weapons.LAU3_HE151)
M260_HYDRA = (5, Weapons.M260_HYDRA)
M260_HYDRA_WP = (5, Weapons.M260_HYDRA_WP)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
5,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
@@ -121,6 +248,62 @@ class Bronco_OV_10A(PlaneType):
5,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
5,
Weapons.LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
5,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
5,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos = (
5,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
5,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
5,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
# ERRR {MK-81}
@@ -135,6 +318,7 @@ class Bronco_OV_10A(PlaneType):
LAU3_HE5 = (6, Weapons.LAU3_HE5)
LAU3_HE151 = (6, Weapons.LAU3_HE151)
M260_HYDRA = (6, Weapons.M260_HYDRA)
M260_HYDRA_WP = (6, Weapons.M260_HYDRA_WP)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
6,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
@@ -143,15 +327,76 @@ class Bronco_OV_10A(PlaneType):
6,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
6,
Weapons.LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
6,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
6,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos = (
6,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
6,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
6,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
class Pylon7:
LAU_7_with_AIM_9P_Sidewinder_IR_AAM = (
7,
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
)
LAU_7_with_AIM_9B_Sidewinder_IR_AAM = (
7,
Weapons.LAU_7_with_AIM_9B_Sidewinder_IR_AAM,
)
LAU_33A = (7, Weapons.LAU_33A)
class Pylon8:
ParaTrooper = (8, Weapons.ParaTrooper)
OV10_Paratrooper = (8, Weapons.OV10_Paratrooper)
class Pylon9:
OV10_SMOKE = (9, Weapons.OV10_SMOKE)

28
qt_ui/cheatcontext.py Normal file
View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from typing import TYPE_CHECKING
from game.server import EventStream
from game.turnstate import TurnState
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.gameoverdialog import GameOverDialog
if TYPE_CHECKING:
from game import Game
from game.sim import GameUpdateEvents
@contextmanager
def game_state_modifying_cheat_context(game: Game) -> Iterator[GameUpdateEvents]:
with EventStream.event_context() as events:
yield events
state = game.check_win_loss()
if state is not TurnState.CONTINUE:
dialog = GameOverDialog(won=state is TurnState.WIN)
dialog.exec()
else:
game.initialize_turn(events)
GameUpdateSignal.get_instance().updateGame(game)

View File

@@ -111,8 +111,6 @@ def run_ui(create_game_params: CreateGameParams | None, ui_flags: UiFlags) -> No
uiconstants.load_event_icons()
uiconstants.load_aircraft_icons()
uiconstants.load_vehicle_icons()
uiconstants.load_aircraft_banners()
uiconstants.load_vehicle_banners()
# Show warning if no DCS Installation directory was set
if liberation_install.get_dcs_install_directory() == "":

View File

@@ -8,15 +8,12 @@ from .liberation_theme import get_theme_icons
LABELS_OPTIONS = ["Full", "Abbreviated", "Dot Only", "Neutral Dot", "Off"]
SKILL_OPTIONS = ["Average", "Good", "High", "Excellent"]
AIRCRAFT_BANNERS: Dict[str, QPixmap] = {}
AIRCRAFT_ICONS: Dict[str, QPixmap] = {}
VEHICLE_BANNERS: Dict[str, QPixmap] = {}
VEHICLES_ICONS: Dict[str, QPixmap] = {}
ICONS: Dict[str, QPixmap] = {}
def load_icons():
ICONS["New"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/new.png")
ICONS["Open"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/open.png")
ICONS["Save"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/save.png")
@@ -213,25 +210,3 @@ def load_vehicle_icons():
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(
os.path.join("./resources/ui/units/vehicles/icons/", vehicle)
)
def load_aircraft_banners():
for aircraft in os.listdir("./resources/ui/units/aircrafts/banners/"):
if aircraft.endswith(".jpg"):
AIRCRAFT_BANNERS[aircraft[:-7]] = QPixmap(
os.path.join("./resources/ui/units/aircrafts/banners/", aircraft)
)
variants = ["Mirage-F1CT", "Mirage-F1EE", "Mirage-F1M-EE", "Mirage-F1EQ"]
for f1 in variants:
AIRCRAFT_BANNERS[f1] = AIRCRAFT_BANNERS["Mirage-F1C-200"]
variants = ["Mirage-F1CE", "Mirage-F1M-CE"]
for f1 in variants:
AIRCRAFT_BANNERS[f1] = AIRCRAFT_BANNERS["Mirage-F1C"]
def load_vehicle_banners():
for aircraft in os.listdir("./resources/ui/units/vehicles/banners/"):
if aircraft.endswith(".jpg"):
VEHICLE_BANNERS[aircraft[:-7]] = QPixmap(
os.path.join("./resources/ui/units/vehicles/banners/", aircraft)
)

View File

@@ -25,9 +25,7 @@ from qt_ui.widgets.QFactionsInfos import QFactionsInfos
from qt_ui.widgets.QIntelBox import QIntelBox
from qt_ui.widgets.clientslots import MaxPlayerCount
from qt_ui.widgets.simspeedcontrols import SimSpeedControls
from qt_ui.windows.AirWingDialog import AirWingDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
@@ -69,24 +67,8 @@ class QTopPanel(QFrame):
self.factionsInfos = QFactionsInfos(self.game)
self.air_wing = QPushButton("Air Wing")
self.air_wing.setDisabled(True)
self.air_wing.setProperty("style", "btn-primary")
self.air_wing.clicked.connect(self.open_air_wing)
self.transfers = QPushButton("Transfers")
self.transfers.setDisabled(True)
self.transfers.setProperty("style", "btn-primary")
self.transfers.clicked.connect(self.open_transfers)
self.intel_box = QIntelBox(self.game)
self.buttonBox = QGroupBox("Misc")
self.buttonBoxLayout = QHBoxLayout()
self.buttonBoxLayout.addWidget(self.air_wing)
self.buttonBoxLayout.addWidget(self.transfers)
self.buttonBox.setLayout(self.buttonBoxLayout)
self.proceedBox = QGroupBox("Proceed")
self.proceedBoxLayout = QHBoxLayout()
if ui_flags.show_sim_speed_controls:
@@ -102,7 +84,6 @@ class QTopPanel(QFrame):
self.layout.addWidget(self.conditionsWidget)
self.layout.addWidget(self.budgetBox)
self.layout.addWidget(self.intel_box)
self.layout.addWidget(self.buttonBox)
self.layout.addStretch(1)
self.layout.addWidget(self.proceedBox)
@@ -121,9 +102,6 @@ class QTopPanel(QFrame):
if game is None:
return
self.air_wing.setEnabled(True)
self.transfers.setEnabled(True)
self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
if game.conditions.weather.clouds:
@@ -147,14 +125,6 @@ class QTopPanel(QFrame):
else:
self.proceedButton.setEnabled(True)
def open_air_wing(self):
self.dialog = AirWingDialog(self.game_model, self.window())
self.dialog.show()
def open_transfers(self):
self.dialog = PendingTransfersDialog(self.game_model)
self.dialog.show()
def passTurn(self):
with logged_duration("Skipping turn"):
self.game.pass_turn(no_action=True)

View File

@@ -664,6 +664,8 @@ class OverfullAirbasesDisplay(QGroupBox):
parent: QWidget | None = None,
) -> None:
super().__init__("Overfull airbases", parent)
self.setMaximumHeight(200)
self.parking_tracker = parking_tracker
self.parking_tracker.allocation_changed.connect(self.on_allocation_changed)
@@ -671,7 +673,12 @@ class OverfullAirbasesDisplay(QGroupBox):
self.setLayout(layout)
self.label = QLabel()
layout.addWidget(self.label)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(self.label)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
layout.addWidget(scroll)
self.on_allocation_changed()

View File

@@ -16,6 +16,7 @@ class GameUpdateSignal(QObject):
debriefingReceived = Signal(Debriefing)
game_loaded = Signal(Game)
game_generated = Signal(Game)
def __init__(self):
super(GameUpdateSignal, self).__init__()

View File

@@ -24,6 +24,7 @@ from game.persistence import SaveManager
from game.server import EventStream, GameContext
from game.server.dependencies import QtCallbacks, QtContext
from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
from game.turnstate import TurnState
from qt_ui import liberation_install
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
@@ -33,10 +34,13 @@ from qt_ui.uncaughtexceptionhandler import UncaughtExceptionHandler
from qt_ui.widgets.QTopPanel import QTopPanel
from qt_ui.widgets.ato import QAirTaskingOrderPanel
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
from qt_ui.windows.AirWingDialog import AirWingDialog
from qt_ui.windows.BugReportDialog import BugReportDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from qt_ui.windows.gameoverdialog import GameOverDialog
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
from qt_ui.windows.logs.QLogsWindow import QLogsWindow
@@ -150,6 +154,7 @@ class QLiberationWindow(QMainWindow):
def connectSignals(self):
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
GameUpdateSignal.get_instance().debriefingReceived.connect(self.onDebriefing)
GameUpdateSignal.get_instance().game_generated.connect(self.onGameGenerated)
def initActions(self):
self.newGameAction = QAction("&New Game", self)
@@ -221,6 +226,12 @@ class QLiberationWindow(QMainWindow):
self.openNotesAction.setIcon(CONST.ICONS["Notes"])
self.openNotesAction.triggered.connect(self.showNotesDialog)
self.openAirWingAction = QAction("Air Wing", self)
self.openAirWingAction.triggered.connect(self.showAirWingDialog)
self.openTransfersAction = QAction("Transfers", self)
self.openTransfersAction.triggered.connect(self.showTransfersDialog)
self.importTemplatesAction = QAction("Import Layouts", self)
self.importTemplatesAction.triggered.connect(self.import_templates)
@@ -253,6 +264,8 @@ class QLiberationWindow(QMainWindow):
self.actions_bar.addAction(self.openSettingsAction)
self.actions_bar.addAction(self.openStatsAction)
self.actions_bar.addAction(self.openNotesAction)
self.actions_bar.addAction(self.openAirWingAction)
self.actions_bar.addAction(self.openTransfersAction)
def initMenuBar(self):
self.menu = self.menuBar()
@@ -322,7 +335,6 @@ class QLiberationWindow(QMainWindow):
def newGame(self):
wizard = NewGameWizard(self)
wizard.show()
wizard.accepted.connect(lambda: self.onGameGenerated(wizard.generatedGame))
def openFile(self):
if (
@@ -526,6 +538,14 @@ class QLiberationWindow(QMainWindow):
self.dialog = QNotesWindow(self.game)
self.dialog.show()
def showAirWingDialog(self) -> None:
self.dialog = AirWingDialog(self.game_model, self)
self.dialog.show()
def showTransfersDialog(self) -> None:
self.dialog = PendingTransfersDialog(self.game_model)
self.dialog.show()
def import_templates(self):
LAYOUTS.import_templates()
@@ -540,7 +560,14 @@ class QLiberationWindow(QMainWindow):
def onDebriefing(self, debrief: Debriefing):
logging.info("On Debriefing")
self.debriefing = QDebriefingWindow(debrief)
self.debriefing.show()
self.debriefing.exec()
state = self.game.check_win_loss()
if state is not TurnState.CONTINUE:
GameOverDialog(won=state is TurnState.WIN, parent=self).exec()
else:
self.game.pass_turn()
GameUpdateSignal.get_instance().updateGame(self.game)
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show()

View File

@@ -1,14 +1,46 @@
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtWidgets import QDialog, QFrame, QGridLayout, QLabel, QTextBrowser
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.unittype import UnitType
from game.game import Game
from qt_ui.uiconstants import AIRCRAFT_BANNERS, VEHICLE_BANNERS
AIRCRAFT_BANNERS_BASE = Path("resources/ui/units/aircrafts/banners")
VEHICLE_BANNERS_BASE = Path("resources/ui/units/vehicles/banners")
MISSING_BANNER_PATH = AIRCRAFT_BANNERS_BASE / "Missing.jpg"
def aircraft_banner_for(unit_type: AircraftType) -> Path:
if unit_type.dcs_id in {
"Mirage-F1CT",
"Mirage-F1EE",
"Mirage-F1M-EE",
"Mirage-F1EQ",
}:
name = "Mirage-F1C-200"
elif unit_type.dcs_id in {"Mirage-F1CE", "Mirage-F1M-CE"}:
name = "Mirage-F1C"
else:
name = unit_type.dcs_id
return AIRCRAFT_BANNERS_BASE / f"{name}.jpg"
def vehicle_banner_for(unit_type: GroundUnitType) -> Path:
return VEHICLE_BANNERS_BASE / f"{unit_type.dcs_id}.jpg"
def banner_path_for(unit_type: UnitType) -> Path:
if isinstance(unit_type, AircraftType):
return aircraft_banner_for(unit_type)
if isinstance(unit_type, GroundUnitType):
return vehicle_banner_for(unit_type)
raise NotImplementedError(f"Unhandled UnitType subclass: {unit_type.__class__}")
class QUnitInfoWindow(QDialog):
@@ -29,14 +61,10 @@ class QUnitInfoWindow(QDialog):
header = QLabel(self)
header.setGeometry(0, 0, 720, 360)
pixmap = None
if isinstance(self.unit_type, AircraftType):
pixmap = AIRCRAFT_BANNERS.get(self.unit_type.dcs_id)
elif isinstance(self.unit_type, GroundUnitType):
pixmap = VEHICLE_BANNERS.get(self.unit_type.dcs_id)
if pixmap is None:
pixmap = AIRCRAFT_BANNERS.get("Missing")
banner_path = banner_path_for(unit_type)
if not banner_path.exists():
banner_path = MISSING_BANNER_PATH
pixmap = QPixmap(banner_path)
header.setPixmap(pixmap.scaled(header.width(), header.height()))
self.layout.addWidget(header, 0, 0)

View File

@@ -220,10 +220,7 @@ class QWaitingForMissionResultWindow(QDialog):
def process_debriefing(self):
with logged_duration("Turn processing"):
self.sim_controller.process_results(self.debriefing)
self.game.pass_turn()
GameUpdateSignal.get_instance().sendDebriefing(self.debriefing)
GameUpdateSignal.get_instance().updateGame(self.game)
self.accept()
def closeEvent(self, evt):

View File

@@ -9,19 +9,17 @@ from PySide6.QtWidgets import (
QVBoxLayout,
QWidget,
)
from dcs.ships import Stennis, KUZNECOW
from game import Game
from game.ato.flighttype import FlightType
from game.config import RUNWAY_REPAIR_COST
from game.server import EventStream
from game.sim import GameUpdateEvents
from game.theater import (
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION,
ControlPoint,
ControlPointType,
FREE_FRONTLINE_UNIT_SUPPLY,
)
from qt_ui.cheatcontext import game_state_modifying_cheat_context
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS
@@ -119,13 +117,11 @@ class QBaseMenu2(QDialog):
return self.game_model.game.settings.enable_base_capture_cheat
def cheat_capture(self) -> None:
events = GameUpdateEvents()
self.cp.capture(self.game_model.game, events, for_player=not self.cp.captured)
# Reinitialized ground planners and the like. The ATO needs to be reset because
# missions planned against the flipped base are no longer valid.
self.game_model.game.initialize_turn(events)
EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
with game_state_modifying_cheat_context(self.game_model.game) as events:
self.cp.capture(
self.game_model.game, events, for_player=not self.cp.captured
)
self.close()
@property
def has_transfer_destinations(self) -> bool:

View File

@@ -3,10 +3,8 @@ from collections.abc import Callable
from PySide6.QtWidgets import QGroupBox, QLabel, QPushButton, QVBoxLayout
from game import Game
from game.server import EventStream
from game.sim.gameupdateevents import GameUpdateEvents
from game.theater import ControlPoint
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.cheatcontext import game_state_modifying_cheat_context
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import (
QGroundForcesStrategySelector,
)
@@ -52,15 +50,12 @@ class QGroundForcesStrategy(QGroupBox):
self.setLayout(layout)
def cheat_alter_front_line(self, enemy_point: ControlPoint, advance: bool) -> None:
amount = 0.2
if not advance:
amount *= -1
self.cp.base.affect_strength(amount)
enemy_point.base.affect_strength(-amount)
front_line = self.cp.front_line_with(enemy_point)
front_line.update_position()
events = GameUpdateEvents().update_front_line(front_line)
# Clear the ATO to replan missions affected by the front line.
self.game.initialize_turn(events)
EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game)
with game_state_modifying_cheat_context(self.game) as events:
amount = 0.2
if not advance:
amount *= -1
self.cp.base.affect_strength(amount)
enemy_point.base.affect_strength(-amount)
front_line = self.cp.front_line_with(enemy_point)
front_line.update_position()
events.update_front_line(front_line)

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QLabel,
QHBoxLayout,
QPushButton,
QWidget,
)
from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
class GameOverDialog(QDialog):
def __init__(self, won: bool, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setModal(True)
self.setWindowTitle("Game Over")
layout = QVBoxLayout()
self.setLayout(layout)
layout.addWidget(
QLabel(
f"<strong>You {'won' if won else 'lost'}!</strong><br />"
"<br />"
"Click below to start a new game."
)
)
button_row = QHBoxLayout()
layout.addLayout(button_row)
button_row.addStretch()
new_game = QPushButton("New Game")
new_game.clicked.connect(self.on_new_game)
button_row.addWidget(new_game)
def on_new_game(self) -> None:
wizard = NewGameWizard(self)
wizard.show()
wizard.accepted.connect(self.accept)

View File

@@ -1,14 +1,35 @@
from PySide6.QtCore import QItemSelectionModel, QPoint
from PySide6.QtCore import QItemSelectionModel, QPoint, QModelIndex
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import QHeaderView, QTableView
from PySide6.QtWidgets import (
QHeaderView,
QTableView,
QStyledItemDelegate,
QDoubleSpinBox,
QWidget,
QStyleOptionViewItem,
)
from game.ato.flight import Flight
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.package import Package
from game.utils import Distance
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import QWaypointItem
HEADER_LABELS = ["Name", "Alt (ft)", "Alt Type", "TOT/DEPART"]
class AltitudeEditorDelegate(QStyledItemDelegate):
def createEditor(
self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex
) -> QDoubleSpinBox:
editor = QDoubleSpinBox(parent)
editor.setMinimum(0)
editor.setMaximum(40000)
return editor
class QFlightWaypointList(QTableView):
def __init__(self, package: Package, flight: Flight):
super().__init__()
@@ -16,8 +37,9 @@ class QFlightWaypointList(QTableView):
self.flight = flight
self.model = QStandardItemModel(self)
self.model.itemChanged.connect(self.on_changed)
self.setModel(self.model)
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
self.model.setHorizontalHeaderLabels(HEADER_LABELS)
header = self.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
@@ -27,27 +49,36 @@ class QFlightWaypointList(QTableView):
self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select
)
def update_list(self):
# We need to keep just the row and rebuild the index later because the
# QModelIndex will not be valid after the model is cleared.
current_index = self.currentIndex().row()
self.model.clear()
self.altitude_editor_delegate = AltitudeEditorDelegate(self)
self.setItemDelegateForColumn(1, self.altitude_editor_delegate)
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
def update_list(self) -> None:
# ignore signals when updating list so on_changed does not fire
self.model.blockSignals(True)
try:
# We need to keep just the row and rebuild the index later because the
# QModelIndex will not be valid after the model is cleared.
current_index = self.currentIndex().row()
self.model.clear()
waypoints = self.flight.flight_plan.waypoints
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(
self.model.index(current_index, 0), QItemSelectionModel.Select
)
self.resizeColumnsToContents()
total_column_width = self.verticalHeader().width() + self.lineWidth()
for i in range(0, self.model.columnCount()):
total_column_width += self.columnWidth(i) + self.lineWidth()
self.setFixedWidth(total_column_width)
self.model.setHorizontalHeaderLabels(HEADER_LABELS)
def add_waypoint_row(
waypoints = self.flight.flight_plan.waypoints
for row, waypoint in enumerate(waypoints):
self._add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(
self.model.index(current_index, 0), QItemSelectionModel.Select
)
self.resizeColumnsToContents()
total_column_width = self.verticalHeader().width() + self.lineWidth()
for i in range(0, self.model.columnCount()):
total_column_width += self.columnWidth(i) + self.lineWidth()
self.setFixedWidth(total_column_width)
finally:
# stop ignoring signals
self.model.blockSignals(False)
def _add_waypoint_row(
self, row: int, flight: Flight, waypoint: FlightWaypoint
) -> None:
self.model.insertRow(self.model.rowCount())
@@ -55,15 +86,25 @@ class QFlightWaypointList(QTableView):
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
altitude = int(waypoint.alt.feet)
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
altitude_item = QStandardItem(f"{altitude} ft {altitude_type}")
altitude_item.setEditable(False)
altitude_item = QStandardItem(f"{altitude}")
altitude_item.setEditable(True)
self.model.setItem(row, 1, altitude_item)
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
altitude_type_item = QStandardItem(f"{altitude_type}")
altitude_type_item.setEditable(False)
self.model.setItem(row, 2, altitude_type_item)
tot = self.tot_text(flight, waypoint)
tot_item = QStandardItem(tot)
tot_item.setEditable(False)
self.model.setItem(row, 2, tot_item)
self.model.setItem(row, 3, tot_item)
def on_changed(self) -> None:
for i in range(self.model.rowCount()):
altitude = self.model.item(i, 1).text()
altitude_feet = float(altitude)
self.flight.flight_plan.waypoints[i].alt = Distance.from_feet(altitude_feet)
def tot_text(self, flight: Flight, waypoint: FlightWaypoint) -> str:
if waypoint.waypoint_type == FlightWaypointType.TAKEOFF:

View File

@@ -28,6 +28,7 @@ from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSe
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
from qt_ui.widgets.spinsliders import CurrencySpinner, FloatSpinSlider, TimeInputs
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.newgame.QCampaignList import QCampaignList
jinja_env = Environment(
@@ -94,6 +95,7 @@ def wrap_label_text(text: str, width: int = 100) -> str:
class NewGameWizard(QtWidgets.QWizard):
def __init__(self, parent=None):
super(NewGameWizard, self).__init__(parent)
self.setModal(True)
# The wizard should probably be refactored to edit this directly, but for now we
# just create a Settings object so that we can load the player's preserved
@@ -139,7 +141,6 @@ class NewGameWizard(QtWidgets.QWizard):
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
self.setWindowTitle("New Game")
self.generatedGame = None
def accept(self):
logging.info("New Game Wizard accept")
@@ -228,16 +229,14 @@ class NewGameWizard(QtWidgets.QWizard):
mod_settings,
self.lua_plugin_manager,
)
self.generatedGame = generator.generate()
game = generator.generate()
if (
AirWingConfigurationDialog(self.generatedGame, self).exec()
== QDialog.DialogCode.Rejected
):
if AirWingConfigurationDialog(game, self).exec() == QDialog.DialogCode.Rejected:
logging.info("Aborted air wing configuration")
return
self.generatedGame.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
game.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
GameUpdateSignal.get_instance().game_generated.emit(game)
super(NewGameWizard, self).accept()

View File

@@ -32,8 +32,8 @@ platformdirs==2.6.2
pluggy==1.0.0
pre-commit==2.21.0
pydantic==1.10.7
git+https://github.com/pydcs/dcs@e74c3885a55affda09b36907aa7afe5588b0702f#egg=pydcs
pyinstaller==5.7.0
git+https://github.com/pydcs/dcs@60f5121d89f23a062a0494803f8105361cdf36e8#egg=pydcs
pyinstaller==5.12.0
pyinstaller-hooks-contrib==2022.14
pyproj==3.4.1
PySide6==6.4.1

Binary file not shown.

View File

@@ -0,0 +1,133 @@
---
name: Sinai - Exercise Bright Star
theater: Sinai
authors: Starfire
recommended_player_faction: Bluefor Modern
recommended_enemy_faction: Egypt 2000s
description: <p>For over 4 decades, the United States and Egypt have run a series of biannual joint military exercises called Bright Star. Over the years, the number of participating countries has grown substantially. Exercise Bright Star 2025 boasts 8 participant nations and 14 observer nations. The United States and a portion of the exercise coalition will play the part of a fictional hostile nation dubbed Orangeland, staging a mock invasion against Cairo. Israel, having for the first time accepted the invitation to observe, is hosting the aggressor faction of the exercise coalition at its airfields.</p>
miz: exercise_bright_star.miz
performance: 1
recommended_start_date: 2025-09-01
version: "10.9"
squadrons:
# Hatzerim (141)
7:
- primary: SEAD
secondary: any
aircraft:
- F/A-18C Hornet (Lot 20)
size: 20
- primary: TARCAP
secondary: any
aircraft:
- F-15C Eagle
size: 20
- primary: Strike
secondary: air-to-ground
aircraft:
- F-15E Strike Eagle
size: 12
- primary: DEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: BAI
secondary: any
aircraft:
- JF-17 Thunder
size: 16
- primary: BARCAP
secondary: any
aircraft:
- Mirage 2000C
size: 16
# Kedem
12:
- primary: Transport
aircraft:
- CH-47D
size: 20
- primary: Air Assault
secondary: air-to-ground
aircraft:
- UH-1H Iroquois
size: 4
# Nevatim (106)
8:
- primary: AEW&C
aircraft:
- E-3A
size: 2
- primary: Refueling
aircraft:
- KC-135 Stratotanker
size: 1
- primary: Refueling
aircraft:
- KC-135 Stratotanker MPRS
size: 1
- primary: CAS
secondary: air-to-ground
aircraft:
- A-10C Thunderbolt II (Suite 7)
size: 8
# Melez (30)
5:
- primary: CAS
secondary: air-to-ground
aircraft:
- Ka-50 Hokum (Blackshark 3)
size: 4
- primary: TARCAP
secondary: air-to-air
aircraft:
- Mirage 2000C
size: 12
- primary: Strike
secondary: air-to-ground
aircraft:
- Mirage 2000C
size: 12
# Wadi al Jandali (72)
13:
- primary: AEW&C
aircraft:
- E-2C Hawkeye
size: 2
- primary: SEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: DEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: Air Assault
secondary: air-to-ground
aircraft:
- Mi-24P Hind-F
size: 4
- primary: OCA/Aircraft
secondary: air-to-ground
aircraft:
- SA 342L Gazelle
size: 4
# Cairo West (95)
18:
- primary: Transport
aircraft:
- C-130
size: 8
- primary: Escort
secondary: air-to-air
aircraft:
- MiG-29S Fulcrum-C
size: 20
- primary: BARCAP
secondary: any
aircraft:
- J-7B
size: 20

View File

@@ -14,7 +14,7 @@ description:
fighting to the west, a USN battle group is dispatched from the east coast of
the US to clear the Chinese forces from the continent and crush their carrier
group.</p>
version: "10.4"
version: "10.9"
recommended_player_faction: US Navy 2005
recommended_enemy_faction: China 2010
miz: gran_polvorin.miz
@@ -111,10 +111,6 @@ squadrons:
secondary: air-to-ground
aircraft:
- H-6J Badger
- primary: Refueling
aircraft:
- IL-78M
size: 2
# Rio Gallegos
7:
- primary: BARCAP

View File

@@ -176,7 +176,7 @@ description:
northern border. With the arrival of a US carrier group, Israel prepares its
counterattack. The US Navy will handle the Beirut region's coastal arena,
while the IAF will push through Damascus and the inland mountain ranges.</p>
version: "10.6"
version: "10.9"
miz: operation_allied_sword.miz
performance: 2
recommended_start_date: 2004-07-17
@@ -325,10 +325,6 @@ squadrons:
secondary: air-to-ground
aircraft:
- Su-24M Fencer-D
- primary: Refueling
aircraft:
- IL-78M
size: 2
# Bassel Al-Assad
21:
- primary: TARCAP
@@ -342,15 +338,15 @@ squadrons:
- primary: Refueling
aircraft:
- IL-78M
size: 2
size: 1
- primary: Transport
aircraft:
- IL-76MD
size: 2
size: 1
- primary: AEW&C
aircraft:
- A-50
size: 2
size: 1
- primary: Strike
secondary: air-to-ground
aircraft:
@@ -399,4 +395,4 @@ squadrons:
secondary: air-to-ground
aircraft:
- Mi-8MTV2 Hip
size: 4
size: 2

View File

@@ -3,7 +3,7 @@ name: Syria - Operation Blackball
theater: Syria
authors: Fuzzle
description: <p>A lightweight fictional showcase of Cyprus for the Syria terrain. A US Navy force must deploy from a carrier group to push through the island. <strong>This is a purely naval campaign, meaning you will need to use the Air Assault mission type with transports to take the first FOB. Ensure you soften it up enough first!</strong></p><p><strong>Backstory:</strong> The world is at war. With the help of her eastern allies Russia has taken the Suez Canal and deployed a large naval force to the Mediterranean, trapping a US carrier group near the Turkish-Syrian border. Now they must break out by taking Cyprus back.</p>
version: "10.1"
version: "10.9"
recommended_player_faction: US Navy 2005
recommended_enemy_faction: Russia 2010
miz: operation_blackball.miz
@@ -123,15 +123,15 @@ squadrons:
- primary: AEW&C
aircraft:
- A-50
size: 2
size: 1
- primary: Refueling
aircraft:
- IL-78M
size: 2
size: 1
- primary: Transport
aircraft:
- IL-78MD
size: 2
size: 1
# OPFOR First FOB
FOB Gecitkale:
- primary: CAS

View File

@@ -3,7 +3,7 @@ name: Marianas - Pacific Repartee
theater: MarianaIslands
authors: Fuzzle
description: <p>A naval campaign where a US carrier group must retake Guam, Saipan and the Marianas Islands from the Chinese. <strong>This is a purely naval campaign, meaning you will need to use the Air Assault mission type with transports to take FOBs/airbases. Ensure you soften them up enough first!</strong></p><p><strong>Backstory:</strong> After an escalation in the South China Sea, the PLAN has taken the US by surprise and invaded Guam, setting up supporting positions throughout the Marianas island chain. With the rest of the US Navy engaged near Japan, a carrier task group must push through China's forces, assist a small Marine contingent holding out on Farallon de Pajaros and liberate Guam.</p>
version: "10.4"
version: "10.9"
recommended_player_faction: US Navy 2005
recommended_enemy_faction: China 2010
miz: pacific_repartee.miz

View File

@@ -3,7 +3,7 @@ name: Persian Gulf - Scenic Route 2 - Dust To Dust
theater: Persian Gulf
authors: Fuzzle
description: <p>A continuation of Scenic Route. A NATO coalition pushes inland along a protracted axis of advance. Built with helicopters/FOB-based gameplay in mind. <p><strong>Backstory:</strong> With Iran's coastal defences pacified and their forces pushed inland, a beleaguered US Navy is reinforced by a NATO coalition task force. The going will not be easy however; Iran has assembled the full might of its armoured and mechanized divisions alongside rotary support to defend their heartland. The conflict intensifies.</p>
version: "10.1"
version: "10.9"
advanced_iads: true
recommended_player_faction: NATO OIF
recommended_enemy_faction: Iran 2015
@@ -260,4 +260,4 @@ squadrons:
- primary: CAS
secondary: air-to-ground
aircraft:
- Mi-24P Hind-F
- Mi-24P Hind-F

View File

@@ -3,7 +3,7 @@ name: Persian Gulf - Scenic Route
theater: Persian Gulf
authors: Fuzzle
description: <p>A lightweight naval campaign involving a US Navy carrier group pushing across the coast of Iran. <strong>This is a purely naval campaign, meaning you will need to use the Air Assault mission type with transports to take the first FOB. Ensure you soften it up enough first!</strong></p><p><strong>Backstory:</strong> Iran has declared war on all US forces in the Gulf resulting in all local allies withdrawing their support for American troops. A lone carrier group must pacify the southern coast of Iran and hold out until backup can arrive lest the US and her interests be ejected from the region permanently.</p>
version: "10.4"
version: "10.9"
advanced_iads: true
recommended_player_faction: US Navy 2005
recommended_enemy_faction: Iran 2015
@@ -77,15 +77,7 @@ squadrons:
- primary: AEW&C
aircraft:
- A-50
size: 2
- primary: Refueling
aircraft:
- IL-78M
size: 2
- primary: Transport
aircraft:
- IL-78MD
size: 2
size: 1
- primary: BARCAP
secondary: any
aircraft:

View File

@@ -7,7 +7,7 @@ recommended_player_faction: USA 2005
recommended_enemy_faction: Iraq 1991
miz: tripoint_hostility.miz
performance: 2
version: "10.1"
version: "10.9"
recommended_start_date: 2006-08-03
recommended_player_money: 900
recommended_enemy_money: 1200

View File

@@ -5,15 +5,15 @@ local unitPayloads = {
["name"] = "CAP",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
["CLSID"] = "{8D399DDA-FF81-4F14-904D-099B34FE7918}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 8,
},
[3] = {
["CLSID"] = "{8D399DDA-FF81-4F14-904D-099B34FE7918}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 3,
},
[4] = {
@@ -25,14 +25,21 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
},
[8] = {
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
}, },
["tasks"] = {
[1] = 11,
},
@@ -41,7 +48,7 @@ local unitPayloads = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
@@ -61,19 +68,19 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
[8] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
},
@@ -85,15 +92,15 @@ local unitPayloads = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 8,
},
[3] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 3,
},
[4] = {
@@ -105,19 +112,19 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
[8] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
},
@@ -129,7 +136,7 @@ local unitPayloads = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
@@ -149,19 +156,19 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
[8] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
},
@@ -173,7 +180,7 @@ local unitPayloads = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
@@ -181,7 +188,7 @@ local unitPayloads = {
["num"] = 8,
},
[3] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{E6A6262A-CA08-4B3D-B030-E1A993B98452}",
["num"] = 3,
},
[4] = {
@@ -193,19 +200,19 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
[8] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
},

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,10 @@ aircrafts:
- B-52H Stratofortress
- C-130
- C-130J-30 Super Hercules
- C-17A
- CH-47D
- CH-53E
- F-117A Nighthawk
- F-14B Tomcat
- F-15C Eagle
- F-15E Strike Eagle
@@ -44,7 +48,11 @@ aircrafts:
- SA 342M Gazelle
- Su-25T Frogfoot
- Su-27 Flanker-B
- S-3B Viking
- SH-60B Seahawk
- UH-1H Iroquois
- UH-60A
- UH-60L
awacs:
- E-2D Advanced Hawkeye
- E-3A
@@ -75,6 +83,7 @@ infantry_units:
- Infantry M249
- Infantry M4
- MANPADS Stinger
- Mortar 2B11 120mm
preset_groups:
- Hawk
- Patriot

View File

@@ -0,0 +1,79 @@
---
country: Egypt
name: Egypt 2000s
authors: Starfire
description: <p>Egyptian military in the 21st century.</p>
locales:
- ar_SA
aircrafts:
- MiG-29S Fulcrum-C
- J-7B
- Mirage 2000C
- F-16CM Fighting Falcon (Block 50)
- IL-76MD
- C-130
- C-130J-30 Super Hercules
- AH-64D Apache Longbow
- AH-64D Apache Longbow (AI)
- SA 342L Gazelle
- SA 342M Gazelle
- CH-47D
- Ka-50 Hokum
- Ka-50 Hokum (Blackshark 3)
- Mi-24V Hind-E
- Mi-24P Hind-F
- Mi-8MTV2 Hip
awacs:
- E-2C Hawkeye
frontline_units:
- M1A2 Abrams
- M60A3 "Patton"
- T-90A
- T-55A
- BMP-1
- M113
- BTR-80
- M1043 HMMWV (M2 HMG)
- M1045 HMMWV (BGM-71 TOW)
- M1097 Heavy HMMWV Avenger
- ZSU-23-4 Shilka
- M163 Vulcan Air Defense System
artillery_units:
- M109A6 Paladin
- M270 Multiple Launch Rocket System
logistics_units:
- Truck Ural-375
- Truck Ural-4320T
- Truck GAZ-66
infantry_units:
- Infantry RPG
- Infantry AK-74 Rus
- MANPADS SA-18 Igla "Grouse"
- MANPADS Stinger
- Mortar 2B11 120mm
- Paratrooper AKS
- Paratrooper RPG-16
preset_groups:
- SA-2/S-75
- SA-6
- SA-17
- SA-10/S-300PS
- SA-23/S-300VM
- Patriot
- Hawk
missiles:
- SSM SS-1C Scud-B
air_defense_units:
- EWR AN/FPS-117 Radar
- EWR 55G6
- M1097 Heavy HMMWV Avenger
- M48 Chaparral
- M163 Vulcan Air Defense System
- SA-9 Strela
- SA-15 Tor
- ZSU-23-4 Shilka
- AAA ZU-23 Closed Emplacement
- ZU-23 on Ural-375
- ZSU-57-2 'Sparka'
has_jtac: true
jtac_unit: MQ-9 Reaper

View File

@@ -10,6 +10,7 @@ unit_lost_events = {} -- killed units will be added via S_EVENT_UNIT_LOST
kill_events = {} -- killed units will be added via S_EVENT_KILL
base_capture_events = {}
destroyed_objects_positions = {} -- will be added via S_EVENT_DEAD event
killed_ground_units = {} -- keep track of static ground object deaths
mission_ended = false
local function ends_with(str, ending)
@@ -39,6 +40,7 @@ function write_state()
["kill_events"] = kill_events,
["mission_ended"] = mission_ended,
["destroyed_objects_positions"] = destroyed_objects_positions,
["killed_ground_units"] = killed_ground_units,
}
if not json then
local message = string.format("Unable to save DCS Liberation state to %s, JSON library is not loaded !", _debriefing_file_location)

View File

@@ -63,6 +63,10 @@ QToolBar::separator {
height:1px;
}
QToolBar QToolButton {
font: bold 18px;
}
QMenu::item:selected {
background: #435466;
}

View File

@@ -0,0 +1,44 @@
---
name: Sinai
timezone: +2
daytime:
dawn: [6, 8]
day: [8, 16]
dusk: [16, 18]
night: [0, 5]
climate:
day_night_temperature_difference: 8.0
seasons:
winter:
average_pressure: 29.86 # TODO: Find real-world data
average_temperature: 10.0
weather:
thunderstorm: 1
raining: 25
cloudy: 35
clear: 40
spring:
weather:
thunderstorm: 1
raining: 10
cloudy: 30
clear: 60
summer:
average_pressure: 29.98 # TODO: Find real-world data
average_temperature: 28.5
weather:
thunderstorm: 1
raining: 5
cloudy: 30
clear: 65
fall:
weather:
thunderstorm: 1
raining: 15
cloudy: 35
clear: 50
turbulence:
high_avg_yearly_turbulence_per_10cm: 9
low_avg_yearly_turbulence_per_10cm: 3.5
solar_noon_turbulence_per_10cm: 3.5
midnight_turbulence_per_10cm: -3

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

View File

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 333 KiB

After

Width:  |  Height:  |  Size: 333 KiB

View File

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

View File

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 175 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

View File

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 242 KiB

View File

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 222 KiB

View File

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 147 KiB

View File

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 286 KiB

View File

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 281 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

View File

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

View File

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 251 KiB

View File

Before

Width:  |  Height:  |  Size: 303 KiB

After

Width:  |  Height:  |  Size: 303 KiB

View File

Before

Width:  |  Height:  |  Size: 347 KiB

After

Width:  |  Height:  |  Size: 347 KiB

View File

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 191 KiB

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