mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
104 Commits
develop-7.
...
8.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
001e7dfed9 | ||
|
|
cf985d3d37 | ||
|
|
1044a1f45f | ||
|
|
9fe31859d3 | ||
|
|
09417322e7 | ||
|
|
136a9b5f02 | ||
|
|
02f22d4930 | ||
|
|
ca133b9fd1 | ||
|
|
b1af6dfbe1 | ||
|
|
647d1f57f9 | ||
|
|
b250fe2f1e | ||
|
|
3be57bf6bb | ||
|
|
3c8d0b023e | ||
|
|
adceb3a224 | ||
|
|
ab02cd34c5 | ||
|
|
c74b603d81 | ||
|
|
5815401e73 | ||
|
|
f463fe50f2 | ||
|
|
f60bf62897 | ||
|
|
9d43eb8f03 | ||
|
|
8f0ca08b89 | ||
|
|
0534f66b30 | ||
|
|
1162e0aa29 | ||
|
|
36c4bb88be | ||
|
|
dc6624a159 | ||
|
|
8b55331326 | ||
|
|
33ca77e3d1 | ||
|
|
b92b01b245 | ||
|
|
b18b371904 | ||
|
|
9c7e16d121 | ||
|
|
87e869d963 | ||
|
|
4a059a4f8b | ||
|
|
674254e55b | ||
|
|
9fd0e06c05 | ||
|
|
ecaf84ea55 | ||
|
|
e4028cb013 | ||
|
|
c45ac50370 | ||
|
|
6640609caf | ||
|
|
e44b6b416b | ||
|
|
8a861d3da5 | ||
|
|
380d6551be | ||
|
|
4cb035b955 | ||
|
|
e50be9bbde | ||
|
|
ec49a10135 | ||
|
|
23e3630169 | ||
|
|
e20ab5fbc0 | ||
|
|
4fd2bb131b | ||
|
|
42a7102948 | ||
|
|
d271ff17c2 | ||
|
|
cb61dfccc4 | ||
|
|
56f93c76eb | ||
|
|
36cb3a386c | ||
|
|
c25e830e6c | ||
|
|
5d08990cd0 | ||
|
|
2a45cd8899 | ||
|
|
90b880ec3c | ||
|
|
5f0c570d65 | ||
|
|
ce102fcc50 | ||
|
|
30c792c15a | ||
|
|
2f45b856d6 | ||
|
|
31d2b756ab | ||
|
|
b5cf889c09 | ||
|
|
19958f91ca | ||
|
|
c775a898a4 | ||
|
|
535244f6f3 | ||
|
|
9d1d3bdcfa | ||
|
|
36eef2b1b9 | ||
|
|
7788425c5c | ||
|
|
ee0c21b3e5 | ||
|
|
54cd619f75 | ||
|
|
051940e23c | ||
|
|
4fbd7defa3 | ||
|
|
90bda9383d | ||
|
|
7798e2970c | ||
|
|
410c25b331 | ||
|
|
cff74525d6 | ||
|
|
8b7f107044 | ||
|
|
c365a0d739 | ||
|
|
1f4fd0fd04 | ||
|
|
4bb60cb500 | ||
|
|
fe96a415be | ||
|
|
6699289bf7 | ||
|
|
a85d3243fb | ||
|
|
7f2607cf08 | ||
|
|
e50ee976ed | ||
|
|
29ffb526f2 | ||
|
|
e024013093 | ||
|
|
257dabe4fa | ||
|
|
406fb61fa4 | ||
|
|
49dfa95c61 | ||
|
|
c80e5b259f | ||
|
|
64e2213f28 | ||
|
|
ced93afd49 | ||
|
|
f719a5ec34 | ||
|
|
6f4ac1dc39 | ||
|
|
f831c8efdd | ||
|
|
e3c6b03603 | ||
|
|
7a2e8279cd | ||
|
|
24e72475b4 | ||
|
|
f10350dac4 | ||
|
|
f068976749 | ||
|
|
4b4c45e90f | ||
|
|
527eac1f4a | ||
|
|
92c3087187 |
13
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
13
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -39,7 +39,7 @@ body:
|
||||
If the bug was found in a development build, select "Development build"
|
||||
and provide a link to the build in the field below.
|
||||
options:
|
||||
- 6.1.1
|
||||
- 7.1.0
|
||||
- Development build
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -9,6 +9,7 @@ venv
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
/.coverage
|
||||
/coverage.xml
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
.env
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
[](https://discord.gg/bKrtrkJ)
|
||||
|
||||
[](https://codecov.io/gh/dcs-liberation/dcs_liberation)
|
||||
[](https://github.com/dcs-liberation/dcs_liberation)
|
||||
[](https://github.com/dcs-liberation/dcs_liberation/issues)
|
||||

|
||||
|
||||
40
changelog.md
40
changelog.md
@@ -1,3 +1,42 @@
|
||||
# 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
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for Normandy 2 airfields.
|
||||
* **[Factions]** Replaced Patriot STRs "EWRs" with AN/FPS-117 for blue factions 1980 or newer.
|
||||
* **[Mission Generation]** Added option to prevent scud and V2 sites from firing at the start of the mission.
|
||||
* **[Mission Generation]** Added settings for controlling number of tactical commander, observer, JTAC, and game master slots.
|
||||
* **[Mission Planning]** Per-flight TOT offsets can now be set in the flight details UI. This allows individual flights to be scheduled ahead of or behind the rest of the package.
|
||||
* **[New Game Wizard]** The air wing configuration dialog will check for and reject overfull airbases before continuing when the new squadron rules are used.
|
||||
* **[New Game Wizard]** Closing the air wing configuration dialog will now cancel and return to the new game wizard rather than reverting changes and continuing.
|
||||
* **[New Game Wizard]** A warning will be displayed next to the new squadron rules button if the campaign predates the new rules and will likely require user intervention before continuing.
|
||||
* **[UI]** Parking capacity of each squadron's base is now shown during air wing configuration to avoid overcrowding bases when beginning the game with full squadrons.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Mission Planning]** BAI is once again plannable against missile sites and coastal defense batteries.
|
||||
* **[UI]** Fixed formatting of departure time in flight details dialog.
|
||||
|
||||
# 7.0.0
|
||||
|
||||
Saves from 6.x are not compatible with 7.0.
|
||||
@@ -16,6 +55,7 @@ Saves from 6.x are not compatible with 7.0.
|
||||
* **[Mission Generation]** Wind speeds no longer follow a uniform distribution. Median wind speeds are now much lower and the standard deviation has been reduced considerably at altitude but increased somewhat at MSL.
|
||||
* **[Mission Generation]** Improved task generation for SEAD flights carrying TALDs.
|
||||
* **[Mission Generation]** Added task timeout for SEAD flights with TALDs to prevent AI from overflying the target.
|
||||
* **[Mission Generation]** Game state will automatically be checkpointed before fast-forwarding the mission, and restored on mission abort. This means that it's now possible to abort a mission and make changes without needing to manually re-load your game.
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
|
||||
* **[Modding]** Add support for VSN F-4B and F-4C mod.
|
||||
* **[Modding]** Added support for AI C-47 mod.
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
53
client/src/components/aircraftlayer/AircraftLayer.test.tsx
Normal file
53
client/src/components/aircraftlayer/AircraftLayer.test.tsx
Normal 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);
|
||||
});
|
||||
@@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
132
client/src/components/combat/Combat.test.tsx
Normal file
132
client/src/components/combat/Combat.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
48
client/src/components/combatlayer/CombatLayer.test.tsx
Normal file
48
client/src/components/combatlayer/CombatLayer.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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", () => {});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
405
client/src/components/flightplanslayer/FlightPlansLayer.test.tsx
Normal file
405
client/src/components/flightplanslayer/FlightPlansLayer.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
32
client/src/components/frontline/FrontLine.test.tsx
Normal file
32
client/src/components/frontline/FrontLine.test.tsx
Normal 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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
125
client/src/components/navmesh/NavMeshLayer.test.tsx
Normal file
125
client/src/components/navmesh/NavMeshLayer.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
30
client/src/testutils/index.tsx
Normal file
30
client/src/testutils/index.tsx
Normal 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 }) };
|
||||
}
|
||||
@@ -2,7 +2,7 @@ coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
informational: false
|
||||
informational: true
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
project = "DCS Liberation"
|
||||
copyright = "2023, DCS Liberation Team"
|
||||
author = "DCS Liberation Team"
|
||||
release = "7.0.0"
|
||||
release = "8.0.0"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
@@ -21,6 +21,7 @@ from .planningerror import PlanningError
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ..starttype import StartType
|
||||
from ..traveltime import GroundSpeed, TravelTime
|
||||
from ...savecompat import has_save_compat_for
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import FuelConsumption
|
||||
@@ -62,6 +63,13 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def __init__(self, flight: Flight, layout: LayoutT) -> None:
|
||||
self.flight = flight
|
||||
self.layout = layout
|
||||
self.tot_offset = self.default_tot_offset()
|
||||
|
||||
@has_save_compat_for(7)
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
if "tot_offset" not in state:
|
||||
state["tot_offset"] = self.default_tot_offset()
|
||||
self.__dict__.update(state)
|
||||
|
||||
@property
|
||||
def package(self) -> Package:
|
||||
@@ -195,8 +203,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
|
||||
)
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
"""This flight's offset from the package's TOT.
|
||||
|
||||
Positive values represent later TOTs. An offset of -2 minutes is used
|
||||
|
||||
@@ -25,10 +25,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
|
||||
@property
|
||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {
|
||||
@@ -50,13 +46,6 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.targets[0]
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
try:
|
||||
return -self.lead_time
|
||||
except AttributeError:
|
||||
return timedelta()
|
||||
|
||||
@property
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
|
||||
@@ -16,9 +16,8 @@ class SeadFlightPlan(FormationAttackFlightPlan):
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta(minutes=1)
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=1)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
|
||||
|
||||
@@ -38,10 +38,6 @@ class SweepLayout(LoiterLayout):
|
||||
|
||||
|
||||
class SweepFlightPlan(LoiterFlightPlan):
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta(minutes=5)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@@ -54,9 +50,8 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.sweep_end
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
return -self.lead_time
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=5)
|
||||
|
||||
@property
|
||||
def sweep_start_time(self) -> datetime:
|
||||
|
||||
@@ -34,10 +34,6 @@ class TarCapLayout(PatrollingLayout):
|
||||
|
||||
|
||||
class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta(minutes=2)
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
# Note that this duration only has an effect if there are no
|
||||
@@ -64,9 +60,8 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {self.layout.patrol_start, self.layout.patrol_end}
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
return -self.lead_time
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=2)
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.patrol_end:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -353,6 +353,14 @@ class MissionGenerator:
|
||||
gen.generate()
|
||||
|
||||
def setup_combined_arms(self) -> None:
|
||||
self.mission.groundControl.pilot_can_control_vehicles = COMBINED_ARMS_SLOTS > 0
|
||||
self.mission.groundControl.blue_tactical_commander = COMBINED_ARMS_SLOTS
|
||||
self.mission.groundControl.blue_observer = 1
|
||||
self.mission.groundControl.blue_game_masters = (
|
||||
self.game.settings.game_master_slots
|
||||
)
|
||||
self.mission.groundControl.blue_tactical_commander = (
|
||||
self.game.settings.tactical_commander_slots
|
||||
)
|
||||
self.mission.groundControl.pilot_can_control_vehicles = (
|
||||
self.mission.groundControl.blue_tactical_commander > 0
|
||||
)
|
||||
self.mission.groundControl.blue_jtac = self.game.settings.jtac_operator_slots
|
||||
self.mission.groundControl.blue_observer = self.game.settings.observer_slots
|
||||
|
||||
@@ -315,6 +315,10 @@ class MissileSiteGenerator(GroundObjectGenerator):
|
||||
|
||||
def generate(self) -> None:
|
||||
super(MissileSiteGenerator, self).generate()
|
||||
|
||||
if not self.game.settings.generate_fire_tasks_for_missile_sites:
|
||||
return
|
||||
|
||||
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
||||
# TODO : Should be pre-planned ?
|
||||
# TODO : Add delay to task to spread fire task over mission duration ?
|
||||
|
||||
@@ -25,6 +25,7 @@ class SaveGameBundle:
|
||||
MANUAL_SAVE_NAME = "player.liberation"
|
||||
LAST_TURN_SAVE_NAME = "last_turn.liberation"
|
||||
START_OF_TURN_SAVE_NAME = "start_of_turn.liberation"
|
||||
PRE_SIM_CHECKPOINT_SAVE_NAME = "pre_sim_checkpoint.liberation"
|
||||
|
||||
def __init__(self, bundle_path: Path) -> None:
|
||||
self.bundle_path = bundle_path
|
||||
@@ -58,6 +59,19 @@ class SaveGameBundle:
|
||||
game, self.START_OF_TURN_SAVE_NAME, copy_from=self
|
||||
)
|
||||
|
||||
def save_pre_sim_checkpoint(self, game: Game) -> None:
|
||||
"""Writes the save file for the state before beginning simulation.
|
||||
|
||||
This save is the state of the game after the player presses "TAKE OFF", but
|
||||
before the fast-forward simulation begins. It is not practical to rewind, but
|
||||
players commonly will want to cancel and continue planning after pressing that
|
||||
button, so we make a checkpoint that we can reload on abort.
|
||||
"""
|
||||
with logged_duration("Saving pre-sim checkpoint"):
|
||||
self._update_bundle_member(
|
||||
game, self.PRE_SIM_CHECKPOINT_SAVE_NAME, copy_from=self
|
||||
)
|
||||
|
||||
def load_player(self) -> Game:
|
||||
"""Loads the save manually created by the player via save/save-as."""
|
||||
return self._load_from(self.MANUAL_SAVE_NAME)
|
||||
@@ -70,6 +84,10 @@ class SaveGameBundle:
|
||||
"""Loads the save automatically created at the end of the last turn."""
|
||||
return self._load_from(self.LAST_TURN_SAVE_NAME)
|
||||
|
||||
def load_pre_sim_checkpoint(self) -> Game:
|
||||
"""Loads the save automatically created before the simulation began."""
|
||||
return self._load_from(self.PRE_SIM_CHECKPOINT_SAVE_NAME)
|
||||
|
||||
def _load_from(self, name: str) -> Game:
|
||||
with ZipFile(self.bundle_path) as zip_bundle:
|
||||
with zip_bundle.open(name, "r") as save:
|
||||
|
||||
@@ -51,6 +51,10 @@ class SaveManager:
|
||||
with self._save_bundle_context() as bundle:
|
||||
bundle.save_start_of_turn(self.game)
|
||||
|
||||
def save_pre_sim_checkpoint(self) -> None:
|
||||
with self._save_bundle_context() as bundle:
|
||||
bundle.save_pre_sim_checkpoint(self.game)
|
||||
|
||||
def set_loaded_from(self, bundle: SaveGameBundle) -> None:
|
||||
"""Reconfigures this save manager based on the loaded game.
|
||||
|
||||
@@ -81,6 +85,9 @@ class SaveManager:
|
||||
self.last_saved_bundle = previous_saved_bundle
|
||||
raise
|
||||
|
||||
def load_pre_sim_checkpoint(self) -> Game:
|
||||
return self.default_save_bundle.load_pre_sim_checkpoint()
|
||||
|
||||
@staticmethod
|
||||
def load_last_turn(bundle_path: Path) -> Game:
|
||||
return SaveGameBundle(bundle_path).load_last_turn()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,7 +41,8 @@ def has_save_compat_for(
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., ReturnT]) -> Callable[..., ReturnT]:
|
||||
if major != MAJOR_VERSION:
|
||||
# Allow current and previous version to ease cherry-picking.
|
||||
if major not in {MAJOR_VERSION - 1, MAJOR_VERSION}:
|
||||
raise DeprecatedSaveCompatError(func.__name__)
|
||||
return func
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ HQ_AUTOMATION_SECTION = "HQ Automation"
|
||||
|
||||
MISSION_GENERATOR_PAGE = "Mission Generator"
|
||||
|
||||
COMMANDERS_SECTION = "Battlefield Commanders"
|
||||
|
||||
GAMEPLAY_SECTION = "Gameplay"
|
||||
|
||||
# TODO: Make sections a type and add headers.
|
||||
@@ -310,6 +312,41 @@ class Settings:
|
||||
reserves_procurement_target: int = 10
|
||||
|
||||
# Mission Generator
|
||||
|
||||
# Commanders
|
||||
game_master_slots: int = bounded_int_option(
|
||||
"Game master",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=COMMANDERS_SECTION,
|
||||
default=0,
|
||||
min=0,
|
||||
max=100,
|
||||
)
|
||||
tactical_commander_slots: int = bounded_int_option(
|
||||
"Tactical commander",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=COMMANDERS_SECTION,
|
||||
default=1,
|
||||
min=0,
|
||||
max=100,
|
||||
)
|
||||
jtac_operator_slots: int = bounded_int_option(
|
||||
"JTAC/Operator",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=COMMANDERS_SECTION,
|
||||
default=0,
|
||||
min=0,
|
||||
max=100,
|
||||
)
|
||||
observer_slots: int = bounded_int_option(
|
||||
"Observer",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=COMMANDERS_SECTION,
|
||||
default=1,
|
||||
min=0,
|
||||
max=100,
|
||||
)
|
||||
|
||||
# Gameplay
|
||||
fast_forward_to_first_contact: bool = boolean_option(
|
||||
"Fast forward mission to first contact (WIP)",
|
||||
@@ -324,6 +361,17 @@ class Settings:
|
||||
"modifications."
|
||||
),
|
||||
)
|
||||
reload_pre_sim_checkpoint_on_abort: bool = boolean_option(
|
||||
"Reset mission to pre-take off conditions on abort",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=GAMEPLAY_SECTION,
|
||||
default=True,
|
||||
detail=(
|
||||
"If enabled, the game will automatically reload a pre-take off save when "
|
||||
"aborting take off. If this is disabled, you will need to manually re-load "
|
||||
"your game after aborting take off."
|
||||
),
|
||||
)
|
||||
player_mission_interrupts_sim_at: Optional[StartType] = choices_option(
|
||||
"Player missions interrupt fast forward",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
@@ -443,6 +491,16 @@ class Settings:
|
||||
section=PERFORMANCE_SECTION,
|
||||
default=True,
|
||||
)
|
||||
generate_fire_tasks_for_missile_sites: bool = boolean_option(
|
||||
"Generate fire tasks for missile sites",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
section=PERFORMANCE_SECTION,
|
||||
detail=(
|
||||
"If enabled, missile sites like V2s and Scuds will fire on random targets "
|
||||
"at the start of the mission."
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
perf_moving_units: bool = boolean_option(
|
||||
"Moving ground units",
|
||||
page=MISSION_GENERATOR_PAGE,
|
||||
@@ -523,6 +581,10 @@ class Settings:
|
||||
with settings_path.open(encoding="utf-8") as settings_file:
|
||||
data = yaml.safe_load(settings_file)
|
||||
|
||||
if data is None:
|
||||
logging.warning("Saved settings file %s is empty", settings_path)
|
||||
return
|
||||
|
||||
expected_types = get_type_hints(Settings)
|
||||
for key, value in data.items():
|
||||
if key not in self.__dict__:
|
||||
|
||||
@@ -11,12 +11,12 @@ from game.ato.flightstate import (
|
||||
Uninitialized,
|
||||
)
|
||||
from .combat import CombatInitiator, FrozenCombat
|
||||
from .gameupdateevents import GameUpdateEvents
|
||||
from .simulationresults import SimulationResults
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.ato import Flight
|
||||
from .gameupdateevents import GameUpdateEvents
|
||||
|
||||
|
||||
class AircraftSimulation:
|
||||
@@ -72,6 +72,7 @@ class AircraftSimulation:
|
||||
def reset(self) -> None:
|
||||
for flight in self.iter_flights():
|
||||
flight.set_state(Uninitialized(flight, self.game.settings))
|
||||
self.combats = []
|
||||
|
||||
def iter_flights(self) -> Iterator[Flight]:
|
||||
packages = itertools.chain(
|
||||
|
||||
@@ -39,6 +39,7 @@ class GameLoop:
|
||||
def start(self) -> None:
|
||||
if self.started:
|
||||
raise RuntimeError("Cannot start game loop because it has already started")
|
||||
self.game.save_manager.save_pre_sim_checkpoint()
|
||||
self.started = True
|
||||
self.sim.begin_simulation()
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import random
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional, Sequence, TYPE_CHECKING
|
||||
from typing import Optional, Sequence, TYPE_CHECKING, Any
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from faker import Faker
|
||||
|
||||
@@ -13,6 +14,7 @@ from game.ato import Flight, FlightType, Package
|
||||
from game.settings import AutoAtoBehavior, Settings
|
||||
from .pilot import Pilot, PilotStatus
|
||||
from ..db.database import Database
|
||||
from ..savecompat import has_save_compat_for
|
||||
from ..utils import meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -26,6 +28,8 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class Squadron:
|
||||
id: UUID = field(init=False, default_factory=uuid4)
|
||||
|
||||
name: str
|
||||
nickname: Optional[str]
|
||||
country: str
|
||||
@@ -61,21 +65,24 @@ class Squadron:
|
||||
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
|
||||
pending_deliveries: int = field(init=False, hash=False, compare=False, default=0)
|
||||
|
||||
@has_save_compat_for(7)
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
if "id" not in state:
|
||||
state["id"] = uuid4()
|
||||
self.__dict__.update(state)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.nickname is None:
|
||||
return self.name
|
||||
return f'{self.name} "{self.nickname}"'
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(
|
||||
(
|
||||
self.name,
|
||||
self.nickname,
|
||||
self.country,
|
||||
self.role,
|
||||
self.aircraft,
|
||||
)
|
||||
)
|
||||
return hash(self.id)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Squadron):
|
||||
return False
|
||||
return self.id == other.id
|
||||
|
||||
@property
|
||||
def player(self) -> bool:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -434,6 +434,14 @@ class MissileSiteGroundObject(TheaterGroundObject):
|
||||
def should_head_to_conflict(self) -> bool:
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.BAI
|
||||
for mission_type in super().mission_types(for_player):
|
||||
yield mission_type
|
||||
|
||||
|
||||
class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
@@ -466,6 +474,14 @@ class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
def should_head_to_conflict(self) -> bool:
|
||||
return True
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from game.ato import FlightType
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.BAI
|
||||
for mission_type in super().mission_types(for_player):
|
||||
yield mission_type
|
||||
|
||||
|
||||
class IadsGroundObject(TheaterGroundObject, ABC):
|
||||
def __init__(
|
||||
|
||||
@@ -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
9
game/turnstate.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class TurnState(Enum):
|
||||
WIN = 0
|
||||
LOSS = 1
|
||||
CONTINUE = 2
|
||||
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MAJOR_VERSION = 7
|
||||
MAJOR_VERSION = 8
|
||||
MINOR_VERSION = 0
|
||||
MICRO_VERSION = 0
|
||||
VERSION_NUMBER = ".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
|
||||
@@ -175,4 +175,14 @@ VERSION = _build_version_string()
|
||||
#:
|
||||
#: Version 10.7
|
||||
#: * Support for defining squadron sizes.
|
||||
CAMPAIGN_FORMAT_VERSION = (10, 7)
|
||||
#:
|
||||
#: Version 10.8
|
||||
#: * Support for Normandy 2.
|
||||
#:
|
||||
#: 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.
|
||||
#:
|
||||
#: Version 10.10
|
||||
#: * Support for Sinai.
|
||||
CAMPAIGN_FORMAT_VERSION = (10, 10)
|
||||
|
||||
@@ -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
28
qt_ui/cheatcontext.py
Normal 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)
|
||||
137
qt_ui/main.py
137
qt_ui/main.py
@@ -1,17 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
from PySide6 import QtWidgets
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QApplication, QCheckBox, QSplashScreen
|
||||
from PySide6.QtWidgets import QApplication, QCheckBox, QSplashScreen, QDialog
|
||||
from dcs.payloads import PayloadDirectories
|
||||
|
||||
from game import Game, VERSION, logging_config, persistence
|
||||
@@ -34,6 +36,7 @@ from qt_ui import (
|
||||
uiconstants,
|
||||
)
|
||||
from qt_ui.uiflags import UiFlags
|
||||
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.QLiberationWindow import QLiberationWindow
|
||||
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
||||
@@ -67,7 +70,7 @@ def on_game_load(game: Game | None) -> None:
|
||||
EventStream.put_nowait(GameUpdateEvents().game_loaded(game))
|
||||
|
||||
|
||||
def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
|
||||
def run_ui(create_game_params: CreateGameParams | None, ui_flags: UiFlags) -> None:
|
||||
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" # Potential fix for 4K screens
|
||||
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||
@@ -108,8 +111,6 @@ def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
|
||||
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() == "":
|
||||
@@ -151,6 +152,11 @@ def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
|
||||
GameUpdateSignal()
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(on_game_load)
|
||||
|
||||
game: Game | None = None
|
||||
if create_game_params is not None:
|
||||
with logged_duration("New game creation"):
|
||||
game = create_game(create_game_params)
|
||||
|
||||
# Start window
|
||||
window = QLiberationWindow(game, ui_flags)
|
||||
window.showMaximized()
|
||||
@@ -253,6 +259,12 @@ def parse_args() -> argparse.Namespace:
|
||||
"--advanced-iads", action="store_true", help="Enable advanced IADS."
|
||||
)
|
||||
|
||||
new_game.add_argument(
|
||||
"--show-air-wing-config",
|
||||
action="store_true",
|
||||
help="Show the air wing configuration dialog after generating the game.",
|
||||
)
|
||||
|
||||
lint_weapons = subparsers.add_parser("lint-weapons")
|
||||
lint_weapons.add_argument("aircraft", help="Name of the aircraft variant to lint.")
|
||||
|
||||
@@ -261,60 +273,68 @@ def parse_args() -> argparse.Namespace:
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def create_game(
|
||||
campaign_path: Path,
|
||||
blue: str,
|
||||
red: str,
|
||||
supercarrier: bool,
|
||||
auto_procurement: bool,
|
||||
inverted: bool,
|
||||
cheats: bool,
|
||||
start_date: datetime,
|
||||
restrict_weapons_by_date: bool,
|
||||
advanced_iads: bool,
|
||||
use_new_squadron_rules: bool,
|
||||
) -> Game:
|
||||
first_start = liberation_install.init()
|
||||
if first_start:
|
||||
sys.exit(
|
||||
"Cannot generate campaign without configuring DCS Liberation. Start the UI "
|
||||
"for the first run configuration."
|
||||
@dataclass(frozen=True)
|
||||
class CreateGameParams:
|
||||
campaign_path: Path
|
||||
blue: str
|
||||
red: str
|
||||
supercarrier: bool
|
||||
auto_procurement: bool
|
||||
inverted: bool
|
||||
cheats: bool
|
||||
start_date: datetime
|
||||
restrict_weapons_by_date: bool
|
||||
advanced_iads: bool
|
||||
use_new_squadron_rules: bool
|
||||
show_air_wing_config: bool
|
||||
|
||||
@staticmethod
|
||||
def from_args(args: argparse.Namespace) -> CreateGameParams | None:
|
||||
if args.subcommand != "new-game":
|
||||
return None
|
||||
return CreateGameParams(
|
||||
args.campaign,
|
||||
args.blue,
|
||||
args.red,
|
||||
args.supercarrier,
|
||||
args.auto_procurement,
|
||||
args.inverted,
|
||||
args.cheats,
|
||||
args.date,
|
||||
args.restrict_weapons_by_date,
|
||||
args.advanced_iads,
|
||||
args.use_new_squadron_rules,
|
||||
args.show_air_wing_config,
|
||||
)
|
||||
|
||||
# This needs to run before the pydcs payload cache is created, which happens
|
||||
# extremely early. It's not a problem that we inject these paths twice because we'll
|
||||
# get the same answers each time.
|
||||
#
|
||||
# Without this, it is not possible to use next turn (or anything that needs to check
|
||||
# for loadouts) without saving the generated campaign and reloading it the normal
|
||||
# way.
|
||||
inject_custom_payloads(Path(persistence.base_path()))
|
||||
campaign = Campaign.from_file(campaign_path)
|
||||
theater = campaign.load_theater(advanced_iads)
|
||||
|
||||
def create_game(params: CreateGameParams) -> Game:
|
||||
campaign = Campaign.from_file(params.campaign_path)
|
||||
theater = campaign.load_theater(params.advanced_iads)
|
||||
faction_loader = Factions.load()
|
||||
lua_plugin_manager = LuaPluginManager.load()
|
||||
lua_plugin_manager.merge_player_settings()
|
||||
generator = GameGenerator(
|
||||
faction_loader.get_by_name(blue),
|
||||
faction_loader.get_by_name(red),
|
||||
faction_loader.get_by_name(params.blue),
|
||||
faction_loader.get_by_name(params.red),
|
||||
theater,
|
||||
campaign.load_air_wing_config(theater),
|
||||
Settings(
|
||||
supercarrier=supercarrier,
|
||||
automate_runway_repair=auto_procurement,
|
||||
automate_front_line_reinforcements=auto_procurement,
|
||||
automate_aircraft_reinforcements=auto_procurement,
|
||||
enable_frontline_cheats=cheats,
|
||||
enable_base_capture_cheat=cheats,
|
||||
restrict_weapons_by_date=restrict_weapons_by_date,
|
||||
enable_squadron_aircraft_limits=use_new_squadron_rules,
|
||||
supercarrier=params.supercarrier,
|
||||
automate_runway_repair=params.auto_procurement,
|
||||
automate_front_line_reinforcements=params.auto_procurement,
|
||||
automate_aircraft_reinforcements=params.auto_procurement,
|
||||
enable_frontline_cheats=params.cheats,
|
||||
enable_base_capture_cheat=params.cheats,
|
||||
restrict_weapons_by_date=params.restrict_weapons_by_date,
|
||||
enable_squadron_aircraft_limits=params.use_new_squadron_rules,
|
||||
),
|
||||
GeneratorSettings(
|
||||
start_date=start_date,
|
||||
start_date=params.start_date,
|
||||
start_time=campaign.recommended_start_time,
|
||||
player_budget=DEFAULT_BUDGET,
|
||||
enemy_budget=DEFAULT_BUDGET,
|
||||
inverted=inverted,
|
||||
inverted=params.inverted,
|
||||
advanced_iads=theater.iads_network.advanced_iads,
|
||||
no_carrier=False,
|
||||
no_lha=False,
|
||||
@@ -334,7 +354,10 @@ def create_game(
|
||||
lua_plugin_manager,
|
||||
)
|
||||
game = generator.generate()
|
||||
game.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
|
||||
if params.show_air_wing_config:
|
||||
if AirWingConfigurationDialog(game, None).exec() == QDialog.DialogCode.Rejected:
|
||||
sys.exit("Aborted air wing configuration")
|
||||
game.begin_turn_0(squadrons_start_full=params.use_new_squadron_rules)
|
||||
return game
|
||||
|
||||
|
||||
@@ -405,8 +428,6 @@ def main():
|
||||
"Installation path contains non-ASCII characters. This is known to cause problems."
|
||||
)
|
||||
|
||||
game: Optional[Game] = None
|
||||
|
||||
args = parse_args()
|
||||
|
||||
# TODO: Flesh out data and then make unconditional.
|
||||
@@ -415,21 +436,6 @@ def main():
|
||||
|
||||
load_mods()
|
||||
|
||||
if args.subcommand == "new-game":
|
||||
with logged_duration("New game creation"):
|
||||
game = create_game(
|
||||
args.campaign,
|
||||
args.blue,
|
||||
args.red,
|
||||
args.supercarrier,
|
||||
args.auto_procurement,
|
||||
args.inverted,
|
||||
args.cheats,
|
||||
args.date,
|
||||
args.restrict_weapons_by_date,
|
||||
args.advanced_iads,
|
||||
args.use_new_squadron_rules,
|
||||
)
|
||||
if args.subcommand == "lint-weapons":
|
||||
lint_weapon_data_for_aircraft(AircraftType.named(args.aircraft))
|
||||
return
|
||||
@@ -438,7 +444,10 @@ def main():
|
||||
return
|
||||
|
||||
with Server().run_in_thread():
|
||||
run_ui(game, UiFlags(args.dev, args.show_sim_speed_controls))
|
||||
run_ui(
|
||||
CreateGameParams.from_args(args),
|
||||
UiFlags(args.dev, args.show_sim_speed_controls),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Callable
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
@@ -25,19 +25,22 @@ 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
|
||||
|
||||
|
||||
class QTopPanel(QFrame):
|
||||
def __init__(
|
||||
self, game_model: GameModel, sim_controller: SimController, ui_flags: UiFlags
|
||||
self,
|
||||
game_model: GameModel,
|
||||
sim_controller: SimController,
|
||||
ui_flags: UiFlags,
|
||||
reset_to_pre_sim_checkpoint: Callable[[], None],
|
||||
) -> None:
|
||||
super(QTopPanel, self).__init__()
|
||||
self.game_model = game_model
|
||||
self.sim_controller = sim_controller
|
||||
self.reset_to_pre_sim_checkpoint = reset_to_pre_sim_checkpoint
|
||||
self.dialog: Optional[QDialog] = None
|
||||
|
||||
self.setMaximumHeight(70)
|
||||
@@ -64,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:
|
||||
@@ -97,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)
|
||||
|
||||
@@ -116,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:
|
||||
@@ -142,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)
|
||||
@@ -293,7 +268,9 @@ class QTopPanel(QFrame):
|
||||
persistence.mission_path_for("liberation_nextturn.miz")
|
||||
)
|
||||
|
||||
waiting = QWaitingForMissionResultWindow(self.game, self.sim_controller, self)
|
||||
waiting = QWaitingForMissionResultWindow(
|
||||
self.game, self.sim_controller, self.reset_to_pre_sim_checkpoint, self
|
||||
)
|
||||
waiting.exec_()
|
||||
|
||||
def budget_update(self, game: Game):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import textwrap
|
||||
from collections import defaultdict
|
||||
from typing import Iterable, Iterator, Optional
|
||||
|
||||
from PySide6.QtCore import (
|
||||
@@ -150,6 +152,49 @@ class SquadronSizeSpinner(QSpinBox):
|
||||
# return size
|
||||
|
||||
|
||||
class AirWingConfigParkingTracker(QWidget):
|
||||
allocation_changed = Signal()
|
||||
|
||||
def __init__(self, game: Game) -> None:
|
||||
super().__init__()
|
||||
self.theater = game.theater
|
||||
self.by_cp: dict[ControlPoint, set[Squadron]] = defaultdict(set)
|
||||
for coalition in game.coalitions:
|
||||
for squadron in coalition.air_wing.iter_squadrons():
|
||||
self.add_squadron(squadron)
|
||||
|
||||
def add_squadron(self, squadron: Squadron) -> None:
|
||||
self.by_cp[squadron.location].add(squadron)
|
||||
self.signal_change()
|
||||
|
||||
def remove_squadron(self, squadron: Squadron) -> None:
|
||||
self.by_cp[squadron.location].remove(squadron)
|
||||
self.signal_change()
|
||||
|
||||
def relocate_squadron(
|
||||
self,
|
||||
squadron: Squadron,
|
||||
prior_location: ControlPoint,
|
||||
new_location: ControlPoint,
|
||||
) -> None:
|
||||
self.by_cp[prior_location].remove(squadron)
|
||||
self.by_cp[new_location].add(squadron)
|
||||
squadron.relocate_to(new_location)
|
||||
self.signal_change()
|
||||
|
||||
def used_parking_at(self, control_point: ControlPoint) -> int:
|
||||
return sum(s.max_size for s in self.by_cp[control_point])
|
||||
|
||||
def iter_overfull(self) -> Iterator[tuple[ControlPoint, int, list[Squadron]]]:
|
||||
for control_point in self.theater.controlpoints:
|
||||
used = self.used_parking_at(control_point)
|
||||
if used > control_point.total_aircraft_parking:
|
||||
yield control_point, used, list(self.by_cp[control_point])
|
||||
|
||||
def signal_change(self) -> None:
|
||||
self.allocation_changed.emit()
|
||||
|
||||
|
||||
class SquadronConfigurationBox(QGroupBox):
|
||||
remove_squadron_signal = Signal(Squadron)
|
||||
|
||||
@@ -158,11 +203,13 @@ class SquadronConfigurationBox(QGroupBox):
|
||||
game: Game,
|
||||
coalition: Coalition,
|
||||
squadron: Squadron,
|
||||
parking_tracker: AirWingConfigParkingTracker,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.game = game
|
||||
self.coalition = coalition
|
||||
self.squadron = squadron
|
||||
self.parking_tracker = parking_tracker
|
||||
|
||||
columns = QHBoxLayout()
|
||||
self.setLayout(columns)
|
||||
@@ -200,6 +247,7 @@ class SquadronConfigurationBox(QGroupBox):
|
||||
left_column.addLayout(size_column)
|
||||
size_column.addWidget(QLabel("Max size:"))
|
||||
self.max_size_selector = SquadronSizeSpinner(self.squadron.max_size, self)
|
||||
self.max_size_selector.valueChanged.connect(self.update_max_size)
|
||||
size_column.addWidget(self.max_size_selector)
|
||||
|
||||
task_column = QVBoxLayout()
|
||||
@@ -214,8 +262,14 @@ class SquadronConfigurationBox(QGroupBox):
|
||||
squadron.location,
|
||||
squadron.aircraft,
|
||||
)
|
||||
self.base_selector.currentIndexChanged.connect(self.relocate_squadron)
|
||||
left_column.addWidget(self.base_selector)
|
||||
|
||||
self.parking_label = QLabel()
|
||||
self.update_parking_label()
|
||||
self.parking_tracker.allocation_changed.connect(self.update_parking_label)
|
||||
left_column.addWidget(self.parking_label)
|
||||
|
||||
if not squadron.player and squadron.aircraft.flyable:
|
||||
player_label = QLabel("Player slots not available for opfor")
|
||||
elif not squadron.aircraft.flyable:
|
||||
@@ -266,9 +320,26 @@ class SquadronConfigurationBox(QGroupBox):
|
||||
self.player_list.setText(
|
||||
"<br />".join(p.name for p in self.claim_players_from_squadron())
|
||||
)
|
||||
self.update_parking_label()
|
||||
finally:
|
||||
self.blockSignals(old_state)
|
||||
|
||||
def update_parking_label(self) -> None:
|
||||
self.parking_label.setText(
|
||||
f"{self.parking_tracker.used_parking_at(self.squadron.location)}/"
|
||||
f"{self.squadron.location.total_aircraft_parking}"
|
||||
)
|
||||
|
||||
def update_max_size(self) -> None:
|
||||
self.squadron.max_size = self.max_size_selector.value()
|
||||
self.parking_tracker.signal_change()
|
||||
|
||||
def relocate_squadron(self) -> None:
|
||||
location = self.base_selector.currentData()
|
||||
self.parking_tracker.relocate_squadron(
|
||||
self.squadron, self.squadron.location, location
|
||||
)
|
||||
|
||||
def remove_from_squadron_config(self) -> None:
|
||||
self.remove_squadron_signal.emit(self.squadron)
|
||||
|
||||
@@ -321,6 +392,7 @@ class SquadronConfigurationBox(QGroupBox):
|
||||
self.squadron = new_squadron
|
||||
self.bind_data()
|
||||
self.mission_types.replace_squadron(self.squadron)
|
||||
self.parking_tracker.signal_change()
|
||||
|
||||
def reset_title(self) -> None:
|
||||
self.setTitle(f"{self.name_edit.text()} - {self.squadron.aircraft}")
|
||||
@@ -361,11 +433,13 @@ class SquadronConfigurationLayout(QVBoxLayout):
|
||||
game: Game,
|
||||
coalition: Coalition,
|
||||
squadrons: list[Squadron],
|
||||
parking_tracker: AirWingConfigParkingTracker,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.game = game
|
||||
self.coalition = coalition
|
||||
self.squadron_configs = []
|
||||
self.parking_tracker = parking_tracker
|
||||
for squadron in squadrons:
|
||||
self.add_squadron(squadron)
|
||||
|
||||
@@ -376,6 +450,7 @@ class SquadronConfigurationLayout(QVBoxLayout):
|
||||
return keep_squadrons
|
||||
|
||||
def remove_squadron(self, squadron: Squadron) -> None:
|
||||
self.parking_tracker.remove_squadron(squadron)
|
||||
for squadron_config in self.squadron_configs:
|
||||
if squadron_config.squadron == squadron:
|
||||
squadron_config.deleteLater()
|
||||
@@ -386,23 +461,32 @@ class SquadronConfigurationLayout(QVBoxLayout):
|
||||
return
|
||||
|
||||
def add_squadron(self, squadron: Squadron) -> None:
|
||||
squadron_config = SquadronConfigurationBox(self.game, self.coalition, squadron)
|
||||
squadron_config = SquadronConfigurationBox(
|
||||
self.game, self.coalition, squadron, self.parking_tracker
|
||||
)
|
||||
squadron_config.remove_squadron_signal.connect(self.remove_squadron)
|
||||
self.squadron_configs.append(squadron_config)
|
||||
self.addWidget(squadron_config)
|
||||
self.parking_tracker.add_squadron(squadron)
|
||||
|
||||
|
||||
class AircraftSquadronsPage(QWidget):
|
||||
remove_squadron_page = Signal(AircraftType)
|
||||
|
||||
def __init__(
|
||||
self, game: Game, coalition: Coalition, squadrons: list[Squadron]
|
||||
self,
|
||||
game: Game,
|
||||
coalition: Coalition,
|
||||
squadrons: list[Squadron],
|
||||
parking_tracker: AirWingConfigParkingTracker,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
self.squadrons_config = SquadronConfigurationLayout(game, coalition, squadrons)
|
||||
self.squadrons_config = SquadronConfigurationLayout(
|
||||
game, coalition, squadrons, parking_tracker
|
||||
)
|
||||
self.squadrons_config.config_changed.connect(self.on_squadron_config_changed)
|
||||
|
||||
scrolling_widget = QWidget()
|
||||
@@ -430,10 +514,16 @@ class AircraftSquadronsPage(QWidget):
|
||||
class AircraftSquadronsPanel(QStackedLayout):
|
||||
page_removed = Signal(AircraftType)
|
||||
|
||||
def __init__(self, game: Game, coalition: Coalition) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
coalition: Coalition,
|
||||
parking_tracker: AirWingConfigParkingTracker,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.game = game
|
||||
self.coalition = coalition
|
||||
self.parking_tracker = parking_tracker
|
||||
self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {}
|
||||
for aircraft, squadrons in self.air_wing.squadrons.items():
|
||||
self.new_page_for_type(aircraft, squadrons)
|
||||
@@ -453,7 +543,9 @@ class AircraftSquadronsPanel(QStackedLayout):
|
||||
def new_page_for_type(
|
||||
self, aircraft_type: AircraftType, squadrons: list[Squadron]
|
||||
) -> None:
|
||||
page = AircraftSquadronsPage(self.game, self.coalition, squadrons)
|
||||
page = AircraftSquadronsPage(
|
||||
self.game, self.coalition, squadrons, self.parking_tracker
|
||||
)
|
||||
page.remove_squadron_page.connect(self.remove_page_for_type)
|
||||
self.addWidget(page)
|
||||
self.squadrons_pages[aircraft_type] = page
|
||||
@@ -539,14 +631,77 @@ class AircraftTypeList(QListView):
|
||||
self.update(self.selectionModel().currentIndex())
|
||||
|
||||
|
||||
def describe_overfull_airbases(
|
||||
overfull: Iterable[tuple[ControlPoint, int, list[Squadron]]]
|
||||
) -> str:
|
||||
string_builder = []
|
||||
for (
|
||||
control_point,
|
||||
used_parking,
|
||||
squadrons,
|
||||
) in overfull:
|
||||
capacity = control_point.total_aircraft_parking
|
||||
base_description = f"{control_point.name} {used_parking}/{capacity}"
|
||||
string_builder.append(f"<p><strong>{base_description}</strong></p>")
|
||||
squadron_descriptions = []
|
||||
for squadron in squadrons:
|
||||
squadron_details = (
|
||||
f"{squadron.aircraft} {squadron.name} {squadron.max_size} aircraft"
|
||||
)
|
||||
squadron_descriptions.append(f"<li>{squadron_details}</li>")
|
||||
string_builder.append(f"<ul>{''.join(squadron_descriptions)}</ul>")
|
||||
|
||||
if not string_builder:
|
||||
string_builder.append("All airbases are within parking limits.")
|
||||
|
||||
return "".join(string_builder)
|
||||
|
||||
|
||||
class OverfullAirbasesDisplay(QGroupBox):
|
||||
def __init__(
|
||||
self,
|
||||
parking_tracker: AirWingConfigParkingTracker,
|
||||
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)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
self.label = QLabel()
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(self.label)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
layout.addWidget(scroll)
|
||||
|
||||
self.on_allocation_changed()
|
||||
|
||||
def on_allocation_changed(self) -> None:
|
||||
self.label.setText(
|
||||
describe_overfull_airbases(self.parking_tracker.iter_overfull())
|
||||
)
|
||||
|
||||
|
||||
class AirWingConfigurationTab(QWidget):
|
||||
def __init__(self, coalition: Coalition, game: Game) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
coalition: Coalition,
|
||||
game: Game,
|
||||
parking_tracker: AirWingConfigParkingTracker,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
layout = QGridLayout()
|
||||
self.setLayout(layout)
|
||||
self.game = game
|
||||
self.coalition = coalition
|
||||
self.parking_tracker = parking_tracker
|
||||
|
||||
self.type_list = AircraftTypeList(coalition.air_wing)
|
||||
|
||||
@@ -556,7 +711,9 @@ class AirWingConfigurationTab(QWidget):
|
||||
add_button.clicked.connect(lambda state: self.add_squadron())
|
||||
layout.addWidget(add_button, 2, 1, 1, 1)
|
||||
|
||||
self.squadrons_panel = AircraftSquadronsPanel(game, coalition)
|
||||
self.squadrons_panel = AircraftSquadronsPanel(
|
||||
game, coalition, self.parking_tracker
|
||||
)
|
||||
self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type)
|
||||
layout.addLayout(self.squadrons_panel, 1, 3, 2, 1)
|
||||
|
||||
@@ -630,6 +787,9 @@ class AirWingConfigurationDialog(QDialog):
|
||||
|
||||
def __init__(self, game: Game, parent) -> None:
|
||||
super().__init__(parent)
|
||||
self.game = game
|
||||
self.parking_tracker = AirWingConfigParkingTracker(game)
|
||||
|
||||
self.setMinimumSize(1024, 768)
|
||||
self.setWindowTitle(f"Air Wing Configuration")
|
||||
# TODO: self.setWindowIcon()
|
||||
@@ -651,11 +811,18 @@ class AirWingConfigurationDialog(QDialog):
|
||||
|
||||
self.tabs = []
|
||||
for coalition in game.coalitions:
|
||||
coalition_tab = AirWingConfigurationTab(coalition, game)
|
||||
coalition_tab = AirWingConfigurationTab(
|
||||
coalition, game, self.parking_tracker
|
||||
)
|
||||
name = "Blue" if coalition.player else "Red"
|
||||
self.tab_widget.addTab(coalition_tab, name)
|
||||
self.tabs.append(coalition_tab)
|
||||
|
||||
self.overfull_airbases_display = OverfullAirbasesDisplay(
|
||||
self.parking_tracker, self
|
||||
)
|
||||
layout.addWidget(self.overfull_airbases_display)
|
||||
|
||||
buttons_layout = QHBoxLayout()
|
||||
apply_button = QPushButton("Accept Changes && Start Campaign")
|
||||
apply_button.setProperty("style", "btn-accept")
|
||||
@@ -671,7 +838,29 @@ class AirWingConfigurationDialog(QDialog):
|
||||
for tab in self.tabs:
|
||||
tab.revert()
|
||||
|
||||
def can_continue(self) -> bool:
|
||||
if not self.game.settings.enable_squadron_aircraft_limits:
|
||||
return True
|
||||
|
||||
overfull = list(self.parking_tracker.iter_overfull())
|
||||
if not overfull:
|
||||
return True
|
||||
|
||||
description = (
|
||||
"<p>The following airbases are over capacity:</p>"
|
||||
f"{describe_overfull_airbases(overfull)}"
|
||||
)
|
||||
QMessageBox().critical(
|
||||
self,
|
||||
"Cannot continue with overfull bases",
|
||||
description,
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
return False
|
||||
|
||||
def accept(self) -> None:
|
||||
if not self.can_continue():
|
||||
return
|
||||
for tab in self.tabs:
|
||||
tab.apply()
|
||||
super().accept()
|
||||
@@ -679,8 +868,16 @@ class AirWingConfigurationDialog(QDialog):
|
||||
def reject(self) -> None:
|
||||
result = QMessageBox.information(
|
||||
None,
|
||||
"Discard changes?",
|
||||
"Are you sure you want to discard your changes and start the campaign?",
|
||||
"Abort new game?",
|
||||
"<br />".join(
|
||||
textwrap.wrap(
|
||||
"Are you sure you want to cancel air wing configuration and "
|
||||
"return to the new game wizard? If you instead want to revert your "
|
||||
"air wing changes and continue, use the revert and accept buttons "
|
||||
"below.",
|
||||
width=55,
|
||||
)
|
||||
),
|
||||
QMessageBox.Yes,
|
||||
QMessageBox.No,
|
||||
)
|
||||
|
||||
@@ -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__()
|
||||
|
||||
@@ -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
|
||||
@@ -133,7 +137,14 @@ class QLiberationWindow(QMainWindow):
|
||||
|
||||
vbox = QVBoxLayout()
|
||||
vbox.setContentsMargins(0, 0, 0, 0)
|
||||
vbox.addWidget(QTopPanel(self.game_model, self.sim_controller, ui_flags))
|
||||
vbox.addWidget(
|
||||
QTopPanel(
|
||||
self.game_model,
|
||||
self.sim_controller,
|
||||
ui_flags,
|
||||
self.reset_to_pre_sim_checkpoint,
|
||||
)
|
||||
)
|
||||
vbox.addWidget(hbox)
|
||||
|
||||
central_widget = QWidget()
|
||||
@@ -143,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)
|
||||
@@ -214,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)
|
||||
|
||||
@@ -246,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()
|
||||
@@ -315,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 (
|
||||
@@ -340,6 +359,23 @@ class QLiberationWindow(QMainWindow):
|
||||
except Exception:
|
||||
logging.exception("Error loading save game %s", file[0])
|
||||
|
||||
def reset_to_pre_sim_checkpoint(self) -> None:
|
||||
"""Loads the game that was saved before pressing the take-off button.
|
||||
|
||||
A checkpoint will be saved when the player presses take-off to save their state
|
||||
before the mission simulation begins. If the mission is aborted, we usually want
|
||||
to reset to the pre-simulation state to allow players to effectively "rewind",
|
||||
since they probably aborted so that they could make changes. Implementing rewind
|
||||
for real is impractical, but checkpoints are easy.
|
||||
"""
|
||||
if self.game is None:
|
||||
raise RuntimeError(
|
||||
"Cannot reset to pre-sim checkpoint when no game is loaded"
|
||||
)
|
||||
GameUpdateSignal.get_instance().game_loaded.emit(
|
||||
self.game.save_manager.load_pre_sim_checkpoint()
|
||||
)
|
||||
|
||||
def saveGame(self):
|
||||
logging.info("Saving game")
|
||||
|
||||
@@ -502,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()
|
||||
|
||||
@@ -516,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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, Callable
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
@@ -52,12 +52,14 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self,
|
||||
game: Game,
|
||||
sim_controller: SimController,
|
||||
reset_to_pre_sim_checkpoint: Callable[[], None],
|
||||
parent: Optional[QWidget] = None,
|
||||
) -> None:
|
||||
super(QWaitingForMissionResultWindow, self).__init__(parent=parent)
|
||||
self.setWindowModality(QtCore.Qt.WindowModal)
|
||||
self.game = game
|
||||
self.sim_controller = sim_controller
|
||||
self.reset_to_pre_sim_checkpoint = reset_to_pre_sim_checkpoint
|
||||
self.setWindowTitle("Waiting for mission completion.")
|
||||
self.setWindowIcon(QIcon("./resources/icon.png"))
|
||||
self.setMinimumHeight(570)
|
||||
@@ -111,7 +113,7 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self.manually_submit.clicked.connect(self.submit_manually)
|
||||
self.actions_layout.addWidget(self.manually_submit)
|
||||
self.cancel = QPushButton("Abort mission")
|
||||
self.cancel.clicked.connect(self.close)
|
||||
self.cancel.clicked.connect(self.reject)
|
||||
self.actions_layout.addWidget(self.cancel)
|
||||
self.gridLayout.addWidget(self.actions, 2, 0)
|
||||
|
||||
@@ -122,7 +124,7 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self.manually_submit2.clicked.connect(self.submit_manually)
|
||||
self.actions2_layout.addWidget(self.manually_submit2)
|
||||
self.cancel2 = QPushButton("Abort mission")
|
||||
self.cancel2.clicked.connect(self.close)
|
||||
self.cancel2.clicked.connect(self.reject)
|
||||
self.actions2_layout.addWidget(self.cancel2)
|
||||
self.proceed = QPushButton("Accept results")
|
||||
self.proceed.setProperty("style", "btn-success")
|
||||
@@ -133,6 +135,11 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self.layout.addLayout(self.gridLayout, 1, 0)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def reject(self) -> None:
|
||||
if self.game.settings.reload_pre_sim_checkpoint_on_abort:
|
||||
self.reset_to_pre_sim_checkpoint()
|
||||
super().reject()
|
||||
|
||||
@staticmethod
|
||||
def add_update_row(description: str, count: int, layout: QGridLayout) -> None:
|
||||
row = layout.rowCount()
|
||||
@@ -213,11 +220,8 @@ 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.close()
|
||||
self.accept()
|
||||
|
||||
def closeEvent(self, evt):
|
||||
super(QWaitingForMissionResultWindow, self).closeEvent(evt)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
43
qt_ui/windows/gameoverdialog.py
Normal file
43
qt_ui/windows/gameoverdialog.py
Normal 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)
|
||||
@@ -29,6 +29,7 @@ class QEditFlightDialog(QDialog):
|
||||
|
||||
self.setWindowTitle("Edit flight")
|
||||
self.setWindowIcon(EVENT_ICONS["strike"])
|
||||
self.setModal(True)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from PySide6.QtWidgets import QGroupBox, QLabel, QMessageBox, QVBoxLayout
|
||||
from PySide6.QtCore import QTime
|
||||
from PySide6.QtWidgets import (
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QVBoxLayout,
|
||||
QTimeEdit,
|
||||
QHBoxLayout,
|
||||
QCheckBox,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.ato.flight import Flight
|
||||
@@ -10,9 +20,9 @@ from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelector
|
||||
|
||||
|
||||
class FlightAirfieldDisplay(QGroupBox):
|
||||
class FlightPlanPropertiesGroup(QGroupBox):
|
||||
def __init__(self, game: Game, package_model: PackageModel, flight: Flight) -> None:
|
||||
super().__init__("Departure/Arrival")
|
||||
super().__init__("Flight plan properties")
|
||||
self.game = game
|
||||
self.package_model = package_model
|
||||
self.flight = flight
|
||||
@@ -28,6 +38,31 @@ class FlightAirfieldDisplay(QGroupBox):
|
||||
self.package_model.tot_changed.connect(self.update_departure_time)
|
||||
self.update_departure_time()
|
||||
|
||||
tot_offset_layout = QHBoxLayout()
|
||||
layout.addLayout(tot_offset_layout)
|
||||
|
||||
delay = int(self.flight.flight_plan.tot_offset.total_seconds())
|
||||
negative = delay < 0
|
||||
if negative:
|
||||
delay = -delay
|
||||
hours = delay // 3600
|
||||
minutes = delay // 60 % 60
|
||||
seconds = delay % 60
|
||||
|
||||
tot_offset_layout.addWidget(QLabel("TOT Offset (minutes:seconds)"))
|
||||
tot_offset_layout.addStretch()
|
||||
negative_offset_checkbox = QCheckBox("Ahead of package")
|
||||
negative_offset_checkbox.setChecked(negative)
|
||||
negative_offset_checkbox.toggled.connect(self.toggle_negative_offset)
|
||||
tot_offset_layout.addWidget(negative_offset_checkbox)
|
||||
|
||||
self.tot_offset_spinner = QTimeEdit(QTime(hours, minutes, seconds))
|
||||
self.tot_offset_spinner.setMaximumTime(QTime(59, 0))
|
||||
self.tot_offset_spinner.setDisplayFormat("mm:ss")
|
||||
self.tot_offset_spinner.timeChanged.connect(self.set_tot_offset)
|
||||
self.tot_offset_spinner.setToolTip("Flight TOT offset from package TOT")
|
||||
tot_offset_layout.addWidget(self.tot_offset_spinner)
|
||||
|
||||
layout.addWidget(
|
||||
QLabel(
|
||||
"Determined based on the package TOT. Edit the "
|
||||
@@ -58,7 +93,7 @@ class FlightAirfieldDisplay(QGroupBox):
|
||||
# is an invalid state for calling anything in TotEstimator.
|
||||
return
|
||||
self.departure_time.setText(
|
||||
f"At {self.flight.flight_plan.startup_time():%H:%M%S}"
|
||||
f"At {self.flight.flight_plan.startup_time():%H:%M:%S}"
|
||||
)
|
||||
|
||||
def set_divert(self, index: int) -> None:
|
||||
@@ -76,3 +111,13 @@ class FlightAirfieldDisplay(QGroupBox):
|
||||
QMessageBox.critical(
|
||||
self, "Could not update flight plan", str(ex), QMessageBox.Ok
|
||||
)
|
||||
|
||||
def set_tot_offset(self, offset: QTime) -> None:
|
||||
self.flight.flight_plan.tot_offset = timedelta(
|
||||
hours=offset.hour(), minutes=offset.minute(), seconds=offset.second()
|
||||
)
|
||||
self.update_departure_time()
|
||||
|
||||
def toggle_negative_offset(self) -> None:
|
||||
self.flight.flight_plan.tot_offset = -self.flight.flight_plan.tot_offset
|
||||
self.update_departure_time()
|
||||
@@ -4,15 +4,15 @@ from PySide6.QtWidgets import QFrame, QGridLayout, QVBoxLayout
|
||||
from game import Game
|
||||
from game.ato.flight import Flight
|
||||
from qt_ui.models import PackageModel
|
||||
from qt_ui.windows.mission.flight.settings.FlightAirfieldDisplay import (
|
||||
FlightAirfieldDisplay,
|
||||
from qt_ui.windows.mission.flight.settings.FlightPlanPropertiesGroup import (
|
||||
FlightPlanPropertiesGroup,
|
||||
)
|
||||
from qt_ui.windows.mission.flight.settings.QCustomName import QFlightCustomName
|
||||
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import QFlightSlotEditor
|
||||
from qt_ui.windows.mission.flight.settings.QFlightStartType import QFlightStartType
|
||||
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import (
|
||||
QFlightTypeTaskInfo,
|
||||
)
|
||||
from qt_ui.windows.mission.flight.settings.QCustomName import QFlightCustomName
|
||||
|
||||
|
||||
class QGeneralFlightSettingsTab(QFrame):
|
||||
@@ -23,7 +23,7 @@ class QGeneralFlightSettingsTab(QFrame):
|
||||
|
||||
layout = QGridLayout()
|
||||
layout.addWidget(QFlightTypeTaskInfo(flight), 0, 0)
|
||||
layout.addWidget(FlightAirfieldDisplay(game, package_model, flight), 1, 0)
|
||||
layout.addWidget(FlightPlanPropertiesGroup(game, package_model, flight), 1, 0)
|
||||
layout.addWidget(QFlightSlotEditor(package_model, flight, game), 2, 0)
|
||||
layout.addWidget(QFlightStartType(package_model, flight), 3, 0)
|
||||
layout.addWidget(QFlightCustomName(flight), 4, 0)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import textwrap
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
|
||||
@@ -13,6 +14,7 @@ from PySide6.QtWidgets import (
|
||||
QTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QDialog,
|
||||
)
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
@@ -26,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(
|
||||
@@ -39,7 +42,6 @@ jinja_env = Environment(
|
||||
lstrip_blocks=True,
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
Possible time periods for new games
|
||||
|
||||
@@ -86,9 +88,14 @@ TIME_PERIODS = {
|
||||
}
|
||||
|
||||
|
||||
def wrap_label_text(text: str, width: int = 100) -> str:
|
||||
return "<br />".join(textwrap.wrap(text, width=width))
|
||||
|
||||
|
||||
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
|
||||
@@ -114,7 +121,9 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
self.addPage(self.theater_page)
|
||||
self.addPage(self.faction_selection_page)
|
||||
self.addPage(GeneratorOptions(default_settings, mod_settings))
|
||||
self.difficulty_page = DifficultyAndAutomationOptions(default_settings)
|
||||
self.difficulty_page = DifficultyAndAutomationOptions(
|
||||
default_settings, self.theater_page.campaignList.selected_campaign
|
||||
)
|
||||
self.plugins_page = PluginsPage(self.lua_plugin_manager)
|
||||
|
||||
# Update difficulty page on campaign select
|
||||
@@ -132,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")
|
||||
@@ -221,11 +229,14 @@ class NewGameWizard(QtWidgets.QWizard):
|
||||
mod_settings,
|
||||
self.lua_plugin_manager,
|
||||
)
|
||||
self.generatedGame = generator.generate()
|
||||
game = generator.generate()
|
||||
|
||||
AirWingConfigurationDialog(self.generatedGame, self).exec_()
|
||||
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()
|
||||
|
||||
@@ -567,8 +578,39 @@ class BudgetInputs(QtWidgets.QGridLayout):
|
||||
self.addWidget(self.starting_money, 1, 1)
|
||||
|
||||
|
||||
class NewSquadronRulesWarning(QLabel):
|
||||
def __init__(
|
||||
self, campaign: Campaign | None, parent: QWidget | None = None
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.set_campaign(campaign)
|
||||
|
||||
def set_campaign(self, campaign: Campaign | None) -> None:
|
||||
if campaign is None:
|
||||
self.setText("No campaign selected")
|
||||
return
|
||||
if campaign.version >= (10, 9):
|
||||
text = f"{campaign.name} is compatible with the new squadron rules."
|
||||
elif campaign.version >= (10, 7):
|
||||
text = (
|
||||
f"{campaign.name} has been updated since the new squadron rules were "
|
||||
"introduced, but support for those rules was still optional. You may "
|
||||
"need to remove, resize, or relocate squadrons before beginning the "
|
||||
"game."
|
||||
)
|
||||
else:
|
||||
text = (
|
||||
f"{campaign.name} has not been updated since the new squadron rules. "
|
||||
"Were introduced. You may need to remove, resize, or relocate "
|
||||
"squadrons before beginning the game."
|
||||
)
|
||||
self.setText(wrap_label_text(text))
|
||||
|
||||
|
||||
class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
|
||||
def __init__(self, default_settings: Settings, parent=None) -> None:
|
||||
def __init__(
|
||||
self, default_settings: Settings, current_campaign: Campaign | None, parent=None
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle("Difficulty and automation options")
|
||||
@@ -609,10 +651,15 @@ class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
|
||||
new_squadron_rules.setChecked(default_settings.enable_squadron_aircraft_limits)
|
||||
self.registerField("use_new_squadron_rules", new_squadron_rules)
|
||||
economy_layout.addWidget(new_squadron_rules)
|
||||
self.new_squadron_rules_warning = NewSquadronRulesWarning(current_campaign)
|
||||
economy_layout.addWidget(self.new_squadron_rules_warning)
|
||||
economy_layout.addWidget(
|
||||
QLabel(
|
||||
"With new squadron rules enabled, squadrons will not be able to exceed a maximum number of aircraft "
|
||||
"(configurable), and the campaign will begin with all squadrons at full strength."
|
||||
wrap_label_text(
|
||||
"With new squadron rules enabled, squadrons will not be able to "
|
||||
"exceed a maximum number of aircraft (configurable), and the "
|
||||
"campaign will begin with all squadrons at full strength."
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -650,6 +697,7 @@ class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
|
||||
self.enemy_income.spinner.setValue(
|
||||
int(campaign.recommended_enemy_income_multiplier * 10)
|
||||
)
|
||||
self.new_squadron_rules_warning.set_campaign(campaign)
|
||||
|
||||
|
||||
class PluginOptionCheckbox(QCheckBox):
|
||||
|
||||
@@ -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@8fdeda106ba7e847a5d0a1ed358a1463636b513d#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
|
||||
|
||||
@@ -9,7 +9,7 @@ description:
|
||||
pushing south.</p>
|
||||
miz: battle_of_abu_dhabi.miz
|
||||
performance: 2
|
||||
version: "10.2"
|
||||
version: "10.9"
|
||||
squadrons:
|
||||
# Blue CPs:
|
||||
# The default faction is Iran, but the F-14B is given higher precedence so
|
||||
@@ -41,16 +41,20 @@ squadrons:
|
||||
- F-16CM Fighting Falcon (Block 50)
|
||||
- F-4E Phantom II
|
||||
- primary: AEW&C
|
||||
size: 2
|
||||
aircraft:
|
||||
- E-3A
|
||||
- primary: Refueling
|
||||
size: 2
|
||||
aircraft:
|
||||
- KC-135 Stratotanker
|
||||
- primary: Transport
|
||||
size: 4
|
||||
aircraft:
|
||||
- C-17A
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
size: 4
|
||||
aircraft:
|
||||
- B-1B Lancer
|
||||
- Su-24MK Fencer-D
|
||||
@@ -72,6 +76,7 @@ squadrons:
|
||||
- Su-25 Frogfoot
|
||||
- primary: BAI
|
||||
secondary: air-to-ground
|
||||
size: 8
|
||||
aircraft:
|
||||
- F-16CM Fighting Falcon (Block 50)
|
||||
- Su-24MK Fencer-D
|
||||
@@ -102,6 +107,7 @@ squadrons:
|
||||
- F/A-18C Hornet (Lot 20)
|
||||
- F-14A Tomcat (Block 135-GR Late)
|
||||
- primary: Refueling
|
||||
size: 2
|
||||
aircraft:
|
||||
- S-3B Tanker
|
||||
|
||||
@@ -111,6 +117,7 @@ squadrons:
|
||||
aircraft:
|
||||
- AV-8B Harrier II Night Attack
|
||||
- primary: CAS
|
||||
size: 8
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- UH-1H Iroquois
|
||||
|
||||
@@ -9,7 +9,7 @@ recommended_enemy_faction: Russia 2010
|
||||
recommended_start_date: 2004-01-07
|
||||
miz: black_sea.miz
|
||||
performance: 2
|
||||
version: "10.7"
|
||||
version: "10.9"
|
||||
squadrons:
|
||||
# Anapa-Vityazevo
|
||||
12:
|
||||
@@ -148,6 +148,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- UH-1H Iroquois
|
||||
size: 8
|
||||
Red CV:
|
||||
- primary: BARCAP
|
||||
secondary: air-to-air
|
||||
@@ -164,3 +165,4 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
size: 8
|
||||
|
||||
@@ -12,89 +12,61 @@ recommended_enemy_faction: Germany 1944
|
||||
recommended_start_date: 1944-07-04
|
||||
miz: caen_to_evreux.miz
|
||||
performance: 1
|
||||
version: "10.0"
|
||||
version: "10.9"
|
||||
squadrons:
|
||||
# Evreux
|
||||
26:
|
||||
- primary: BARCAP
|
||||
aircraft:
|
||||
- Bf 109 K-4 Kurfürst
|
||||
- primary: BARCAP
|
||||
aircraft:
|
||||
- Fw 190 A-8 Anton
|
||||
- primary: BARCAP
|
||||
- primary: Escort
|
||||
aircraft:
|
||||
- Fw 190 D-9 Dora
|
||||
size: 12
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Ju 88 A-4
|
||||
- primary: AEW&C
|
||||
- primary: Refueling
|
||||
- primary: Transport
|
||||
size: 12
|
||||
# Conches
|
||||
40:
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- Bf 109 K-4 Kurfürst
|
||||
- primary: BARCAP
|
||||
aircraft:
|
||||
- Fw 190 A-8 Anton
|
||||
- primary: BARCAP
|
||||
aircraft:
|
||||
- Fw 190 D-9 Dora
|
||||
- primary: SEAD
|
||||
secondary: any
|
||||
- primary: DEAD
|
||||
secondary: any
|
||||
size: 4
|
||||
# Carpiquet
|
||||
19:
|
||||
- primary: BARCAP
|
||||
- primary: CAS
|
||||
secondary: any
|
||||
aircraft:
|
||||
- Thunderbolt Mk.II (Late)
|
||||
- P-47D-40 Thunderbolt
|
||||
size: 12
|
||||
- primary: BARCAP
|
||||
aircraft:
|
||||
- Mustang Mk.IV (Late)
|
||||
- P-51D-30-NA Mustang
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- Spitfire LF Mk IX
|
||||
- primary: BARCAP
|
||||
aircraft:
|
||||
- Spitfire LF Mk IX (Clipped Wings)
|
||||
- primary: Strike
|
||||
size: 12
|
||||
- primary: BAI
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- MosquitoFBMkVI
|
||||
- primary: SEAD
|
||||
secondary: any
|
||||
- primary: DEAD
|
||||
secondary: any
|
||||
size: 12
|
||||
- primary: OCA/Runway
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Boston Mk.III
|
||||
- A-20G Havoc
|
||||
size: 10
|
||||
# Ford_AF
|
||||
31:
|
||||
- primary: BARCAP
|
||||
aircraft:
|
||||
- Thunderbolt Mk.II (Mid)
|
||||
- P-47D-30 Thunderbolt (Late)
|
||||
- primary: BARCAP
|
||||
aircraft:
|
||||
- Thunderbolt Mk.II (Early)
|
||||
- P-47D-30 Thunderbolt (Early)
|
||||
- primary: BARCAP
|
||||
- primary: Escort
|
||||
secondary: air-to-air
|
||||
aircraft:
|
||||
- Mustang Mk.IV (Early)
|
||||
- P-51D-25-NA Mustang
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Boston Mk.III
|
||||
- A-20G Havoc
|
||||
size: 10
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Fortress Mk.III
|
||||
- B-17G Flying Fortress
|
||||
- primary: AEW&C
|
||||
- primary: Refueling
|
||||
- primary: Transport
|
||||
size: 10
|
||||
|
||||
BIN
resources/campaigns/exercise_bright_star.miz
Normal file
BIN
resources/campaigns/exercise_bright_star.miz
Normal file
Binary file not shown.
133
resources/campaigns/exercise_bright_star.yaml
Normal file
133
resources/campaigns/exercise_bright_star.yaml
Normal 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
|
||||
@@ -8,7 +8,7 @@ description: <p>Welcome to Vegas Nerve, an asymmetrical Red Flag Exercise scenar
|
||||
miz: exercise_vegas_nerve.miz
|
||||
performance: 1
|
||||
recommended_start_date: 2011-02-24
|
||||
version: "10.7"
|
||||
version: "10.9"
|
||||
squadrons:
|
||||
# Tonopah Airport
|
||||
17:
|
||||
|
||||
BIN
resources/campaigns/final_countdown_2.miz
Normal file
BIN
resources/campaigns/final_countdown_2.miz
Normal file
Binary file not shown.
175
resources/campaigns/final_countdown_2.yaml
Normal file
175
resources/campaigns/final_countdown_2.yaml
Normal file
@@ -0,0 +1,175 @@
|
||||
---
|
||||
name: Normandy - The Final Countdown II
|
||||
theater: Normandy
|
||||
authors: Starfire
|
||||
recommended_player_faction:
|
||||
country: Combined Joint Task Forces Blue
|
||||
name: D-Day Allied Forces 1944 and 1990
|
||||
authors: Starfire
|
||||
description:
|
||||
<p>Faction for Final Countdown II</p>
|
||||
locales:
|
||||
- en_US
|
||||
aircrafts:
|
||||
- Boston Mk.III
|
||||
- Fortress Mk.III
|
||||
- Mustang Mk.IV (Late)
|
||||
- Spitfire LF Mk IX
|
||||
- Thunderbolt Mk.II (Late)
|
||||
- MosquitoFBMkVI
|
||||
- F-14B Tomcat
|
||||
- F/A-18C Hornet (Lot 20)
|
||||
- SH-60B Seahawk
|
||||
awacs:
|
||||
- E-2C Hawkeye
|
||||
tankers:
|
||||
- S-3B Tanker
|
||||
frontline_units:
|
||||
- A17 Light Tank Mk VII Tetrarch
|
||||
- A22 Infantry Tank MK IV Churchill VII
|
||||
- A27L Cruiser Tank MK VIII Centaur IV
|
||||
- A27M Cruiser Tank MK VIII Cromwell IV
|
||||
- Daimler Armoured Car Mk I
|
||||
- M2A1 Half-Track
|
||||
- QF 40 mm Mark III
|
||||
- Sherman Firefly VC
|
||||
- Sherman III
|
||||
artillery_units:
|
||||
- M12 Gun Motor Carriage
|
||||
logistics_units:
|
||||
- Truck Bedford
|
||||
- Truck GMC "Jimmy" 6x6 Truck
|
||||
infantry_units:
|
||||
- Infantry M1 Garand
|
||||
naval_units:
|
||||
- DDG Arleigh Burke IIa
|
||||
- CG Ticonderoga
|
||||
- CVN-74 John C. Stennis
|
||||
missiles: []
|
||||
air_defense_units:
|
||||
- Bofors 40 mm Gun
|
||||
preset_groups:
|
||||
- Ally Flak
|
||||
requirements:
|
||||
WW2 Asset Pack: https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/
|
||||
carrier_names:
|
||||
- CVN-71 Theodore Roosevelt
|
||||
has_jtac: true
|
||||
jtac_unit: MQ-9 Reaper
|
||||
unrestricted_satnav: true
|
||||
doctrine: ww2
|
||||
building_set: ww2ally
|
||||
recommended_enemy_faction: Germany 1944
|
||||
description:
|
||||
<p>While enroute to the Persian Gulf for Operation Desert Shield, the USS Theodore Roosevelt and its carrier strike group are engufled by an electrical vortex and transported through time and space to the English channel on the morning of the Normandy Landings - June 6th 1944. Seeking to reduce the cost in lives to the Allied Forces about to storm the beaches, the captain of the Roosevelt has elected to provide air support for the landings.</p><p><strong>Note:</strong> This campaign has a custom faction that combines modern US naval forces with WW2 Allied forces. To play it as intended, you should carefully ration your use of modern aircraft and not replenish them if shot down (as you cannot get new Tomcats and Hornets in 1944). You can also choose to play it as a purely WW2 campaign by switching to one of the WW2 Ally factions.</p>
|
||||
miz: final_countdown_2.miz
|
||||
performance: 2
|
||||
recommended_start_date: 1944-06-06
|
||||
version: "10.9"
|
||||
squadrons:
|
||||
#Blue CV (90)
|
||||
Blue-CV:
|
||||
- primary: TARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- F-14B Tomcat
|
||||
size: 24
|
||||
- primary: DEAD
|
||||
secondary: any
|
||||
aircraft:
|
||||
- F/A-18C Hornet (Lot 20)
|
||||
size: 24
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- E-2C Hawkeye
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- S-3B Tanker
|
||||
size: 2
|
||||
- primary: Air Assault
|
||||
secondary: any
|
||||
aircraft:
|
||||
- SH-60B Seahawk
|
||||
size: 4
|
||||
#Stoney Cross (39)
|
||||
58:
|
||||
- primary: OCA/Runway
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- A-20G Havoc
|
||||
- Boston Mk.III
|
||||
size: 20
|
||||
#Needs Oar Point (55)
|
||||
28:
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- Spitfire LF Mk IX
|
||||
size: 20
|
||||
- primary: DEAD
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- MosquitoFBMkVI
|
||||
size: 20
|
||||
#RAF Grafton Underwood (1000)
|
||||
From RAF Grafton Underwood:
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- B-17G Flying Fortress
|
||||
- Fortress Mk.III
|
||||
size: 20
|
||||
#Lymington (56)
|
||||
37:
|
||||
- primary: Escort
|
||||
secondary: any
|
||||
aircraft:
|
||||
- P-51D-30-NA Mustang
|
||||
- Mustang Mk.IV (Late)
|
||||
size: 20
|
||||
- primary: BAI
|
||||
secondary: any
|
||||
aircraft:
|
||||
- P-47D-40 Thunderbolt
|
||||
- Thunderbolt Mk.II (Late)
|
||||
size: 20
|
||||
|
||||
#Carpiquet (47)
|
||||
19:
|
||||
- primary: TARCAP
|
||||
secondary: air-to-air
|
||||
aircraft:
|
||||
- Fw 190 D-9 Dora
|
||||
size: 12
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Ju 88 A-4
|
||||
size: 8
|
||||
#Broglie (32)
|
||||
68:
|
||||
- primary: Escort
|
||||
secondary: any
|
||||
aircraft:
|
||||
- Bf 109 K-4 Kurfürst
|
||||
size: 24
|
||||
#Saint-Andre-de-lEure (30)
|
||||
70:
|
||||
- primary: BAI
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Ju 88 A-4
|
||||
size: 12
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Ju 88 A-4
|
||||
size: 12
|
||||
#Vilacoublay (76)
|
||||
42:
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- Fw 190 A-8 Anton
|
||||
size: 20
|
||||
@@ -7,7 +7,7 @@ recommended_enemy_faction: Syria 2011
|
||||
description: <p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance and helicopter friendly.</p>
|
||||
miz: golan_heights_lite.miz
|
||||
performance: 1
|
||||
version: "10.5"
|
||||
version: "10.9"
|
||||
advanced_iads: true # Campaign has connection_nodes / power_sources / command_centers
|
||||
iads_config:
|
||||
- LHA-1 Tarawa # A Naval Group without connections but still participating as EWR
|
||||
@@ -102,28 +102,31 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- E-3A
|
||||
size: 1
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- KC-135 Stratotanker
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- C-130
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
size: 1
|
||||
- primary: Escort
|
||||
secondary: air-to-air
|
||||
aircraft:
|
||||
- F-15C Eagle
|
||||
size: 10
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- F-4E Phantom II
|
||||
size: 10
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- F-15E Strike Eagle
|
||||
size: 10
|
||||
- primary: SEAD
|
||||
secondary: any
|
||||
aircraft:
|
||||
- F-16CM Fighting Falcon (Block 50)
|
||||
size: 10
|
||||
# Golan South
|
||||
Golan South:
|
||||
- primary: CAS
|
||||
@@ -132,65 +135,67 @@ squadrons:
|
||||
female_pilot_percentage: 15
|
||||
aircraft:
|
||||
- AH-1W SuperCobra
|
||||
- primary: CAS
|
||||
size: 4
|
||||
- primary: BAI
|
||||
secondary: air-to-ground
|
||||
nickname: Defenders of Golan
|
||||
female_pilot_percentage: 25
|
||||
aircraft:
|
||||
- AH-64D Apache Longbow
|
||||
- primary: Transport
|
||||
size: 6
|
||||
- primary: Air Assault
|
||||
secondary: any
|
||||
aircraft:
|
||||
- UH-1H Iroquois
|
||||
size: 2
|
||||
# Golan North
|
||||
Golan North:
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Mi-24P Hind-F
|
||||
size: 4
|
||||
- primary: CAS
|
||||
aircraft:
|
||||
- SA 342M Gazelle
|
||||
- primary: Transport
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- SA 342M Gazelle
|
||||
size: 4
|
||||
- primary: Air Assault
|
||||
secondary: any
|
||||
aircraft:
|
||||
- Mi-8MTV2 Hip
|
||||
size: 2
|
||||
# Marj Ruhayyil
|
||||
23:
|
||||
- primary: BARCAP
|
||||
- primary: BAI
|
||||
secondary: any
|
||||
aircraft:
|
||||
- MiG-21bis Fishbed-N
|
||||
- primary: BARCAP
|
||||
size: 12
|
||||
- primary: Escort
|
||||
secondary: any
|
||||
aircraft:
|
||||
- MiG-23MLD Flogger-K
|
||||
- primary: SEAD
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Su-17M4 Fitter-K
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Su-17M4 Fitter-K
|
||||
- MiG-21bis Fishbed-N
|
||||
size: 12
|
||||
# Damascus
|
||||
7:
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- MiG-29S Fulcrum-C
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- MiG-21bis Fishbed-N
|
||||
- primary: BARCAP
|
||||
- MiG-23MLD Flogger-K
|
||||
size: 12
|
||||
- primary: TARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
- MiG-25PD Foxbat-E
|
||||
size: 12
|
||||
- primary: SEAD
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Su-24M Fencer-D
|
||||
size: 12
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Su-17M4 Fitter-K
|
||||
size: 12
|
||||
@@ -16,7 +16,7 @@ description:
|
||||
miz: grabthars_hammer.miz
|
||||
performance: 2
|
||||
recommended_start_date: 1999-12-25
|
||||
version: "10.7"
|
||||
version: "10.9"
|
||||
squadrons:
|
||||
#Mount Pleasant
|
||||
2:
|
||||
|
||||
@@ -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
|
||||
@@ -38,16 +38,20 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- VAW-125
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- VS-35 (Tanker)
|
||||
size: 4
|
||||
- primary: Anti-ship
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- VS-35
|
||||
size: 8
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- HSM-40
|
||||
size: 2
|
||||
# BLUFOR LHA
|
||||
Naval-3:
|
||||
- primary: BAI
|
||||
@@ -58,6 +62,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- HMLA-169 (UH-1H)
|
||||
size: 4
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
@@ -106,9 +111,6 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- H-6J Badger
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- IL-78M
|
||||
# Rio Gallegos
|
||||
7:
|
||||
- primary: BARCAP
|
||||
|
||||
@@ -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
|
||||
@@ -198,16 +198,20 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- VAW-125
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- VS-35 (Tanker)
|
||||
size: 4
|
||||
- primary: Anti-ship
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- VS-35
|
||||
size: 8
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- HSM-40
|
||||
size: 2
|
||||
#BLUFOR LHA
|
||||
Naval-3:
|
||||
- primary: BAI
|
||||
@@ -218,6 +222,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- HMLA-269 (UH-1H)
|
||||
size: 4
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
@@ -257,6 +262,7 @@ squadrons:
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- VMGR-352
|
||||
size: 2
|
||||
- primary: Strike
|
||||
secondary: any
|
||||
aircraft:
|
||||
@@ -319,9 +325,6 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Su-24M Fencer-D
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- IL-78M
|
||||
# Bassel Al-Assad
|
||||
21:
|
||||
- primary: TARCAP
|
||||
@@ -335,12 +338,15 @@ squadrons:
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- IL-78M
|
||||
size: 1
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- IL-76MD
|
||||
size: 1
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- A-50
|
||||
size: 1
|
||||
- primary: Strike
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
@@ -389,3 +395,4 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Mi-8MTV2 Hip
|
||||
size: 2
|
||||
|
||||
@@ -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
|
||||
@@ -27,16 +27,20 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- VAW-125
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- VS-35 (Tanker)
|
||||
size: 4
|
||||
- primary: Anti-ship
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- VS-35
|
||||
size: 8
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- HSM-40
|
||||
size: 2
|
||||
# BLUFOR LHA
|
||||
Naval-2:
|
||||
- primary: BAI
|
||||
@@ -47,6 +51,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- HMLA-269 (UH-1H)
|
||||
size: 4
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
@@ -118,12 +123,15 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- A-50
|
||||
size: 1
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- IL-78M
|
||||
size: 1
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- IL-78MD
|
||||
size: 1
|
||||
# OPFOR First FOB
|
||||
FOB Gecitkale:
|
||||
- primary: CAS
|
||||
@@ -136,6 +144,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Mi-8MTV2 Hip
|
||||
size: 2
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
|
||||
@@ -8,7 +8,7 @@ description: <p>This is a semi-fictional what-if scenario for Operation Peace Sp
|
||||
miz: operation_peace_spring.miz
|
||||
performance: 1
|
||||
recommended_start_date: 2019-12-23
|
||||
version: "10.7"
|
||||
version: "10.9"
|
||||
squadrons:
|
||||
# Ramat David
|
||||
30:
|
||||
|
||||
@@ -8,7 +8,7 @@ description: <p>United Nations Observer Mission in Georgia (UNOMIG) observers st
|
||||
miz: operation_vectrons_claw.miz
|
||||
performance: 1
|
||||
recommended_start_date: 2008-08-08
|
||||
version: "10.7"
|
||||
version: "10.9"
|
||||
squadrons:
|
||||
Blue CV-1:
|
||||
- primary: BARCAP
|
||||
|
||||
@@ -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
|
||||
@@ -27,16 +27,20 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- VAW-125
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- VS-35 (Tanker)
|
||||
size: 4
|
||||
- primary: Anti-ship
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- VS-35
|
||||
size: 8
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- HSM-40
|
||||
size: 2
|
||||
# BLUFOR LHA
|
||||
Naval-2:
|
||||
- primary: BAI
|
||||
@@ -47,6 +51,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- HMLA-169 (UH-1H)
|
||||
size: 4
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
@@ -92,6 +97,7 @@ squadrons:
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- IL-76MD
|
||||
size: 2
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
@@ -105,6 +111,7 @@ squadrons:
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- IL-78M
|
||||
size: 2
|
||||
# Andersen AFB
|
||||
6:
|
||||
- primary: TARCAP
|
||||
@@ -122,6 +129,7 @@ squadrons:
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- IL-76MD
|
||||
size: 2
|
||||
# Antonio B. Won Pat Intl
|
||||
4:
|
||||
- primary: TARCAP
|
||||
|
||||
@@ -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
|
||||
@@ -37,6 +37,7 @@ squadrons:
|
||||
aircraft:
|
||||
- 101st Combat Aviation Brigade
|
||||
#US Army UH-60
|
||||
size: 6
|
||||
# Havadarya
|
||||
9:
|
||||
- primary: BARCAP
|
||||
@@ -62,9 +63,11 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- VAW-125
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- VS-35 (Tanker)
|
||||
size: 4
|
||||
# BLUFOR LHA
|
||||
BLUFOR LHA:
|
||||
- primary: BAI
|
||||
@@ -75,6 +78,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- HMLA-169 (UH-1H)
|
||||
size: 4
|
||||
# BLUFOR Start FOB
|
||||
FOB Anguran:
|
||||
- primary: CAS
|
||||
@@ -87,6 +91,7 @@ squadrons:
|
||||
aircraft:
|
||||
- Wolfpack, 1-82 ARB
|
||||
#US Army Apache AH-64D
|
||||
size: 2
|
||||
# OPFOR L1F1
|
||||
FOB Tang-e Dalan:
|
||||
- primary: CAS
|
||||
@@ -140,9 +145,11 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- A-50
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- IL-78M
|
||||
size: 2
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
@@ -159,6 +166,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- Mi-24P Hind-F
|
||||
size: 4
|
||||
- primary: SEAD
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
@@ -228,12 +236,15 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- A-50
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- IL-78M
|
||||
size: 2
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- IL-78MD
|
||||
size: 2
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
|
||||
@@ -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
|
||||
@@ -28,16 +28,20 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- VAW-125
|
||||
size: 2
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- VS-35 (Tanker)
|
||||
size: 4
|
||||
- primary: Anti-ship
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- VS-35
|
||||
size: 8
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- HSM-40
|
||||
size: 2
|
||||
# BLUFOR LHA
|
||||
Naval-2:
|
||||
- primary: BAI
|
||||
@@ -48,6 +52,7 @@ squadrons:
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
- HMLA-169 (UH-1H)
|
||||
size: 4
|
||||
- primary: CAS
|
||||
secondary: air-to-ground
|
||||
aircraft:
|
||||
@@ -72,12 +77,7 @@ squadrons:
|
||||
- primary: AEW&C
|
||||
aircraft:
|
||||
- A-50
|
||||
- primary: Refueling
|
||||
aircraft:
|
||||
- IL-78M
|
||||
- primary: Transport
|
||||
aircraft:
|
||||
- IL-78MD
|
||||
size: 1
|
||||
- primary: BARCAP
|
||||
secondary: any
|
||||
aircraft:
|
||||
|
||||
@@ -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
|
||||
@@ -24,6 +24,7 @@ squadrons:
|
||||
aircraft:
|
||||
- 960th AAC Squadron
|
||||
#USAF E-3A
|
||||
size: 2
|
||||
# King Hussein Air College, BLUFOR start
|
||||
19:
|
||||
- primary: BARCAP
|
||||
@@ -46,6 +47,7 @@ squadrons:
|
||||
aircraft:
|
||||
- 101st Combat Aviation Brigade
|
||||
#US Army UH-60
|
||||
size: 4
|
||||
# FOB Tha'lah, BLUFOR 1st FOB north
|
||||
FOB Tha'lah:
|
||||
- primary: CAS
|
||||
|
||||
@@ -9,7 +9,7 @@ local unitPayloads = {
|
||||
["num"] = 3,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{M261_M282}",
|
||||
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[3] = {
|
||||
@@ -17,7 +17,7 @@ local unitPayloads = {
|
||||
["num"] = 2,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{M261_M282}",
|
||||
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
|
||||
["num"] = 1,
|
||||
},
|
||||
},
|
||||
@@ -30,19 +30,19 @@ local unitPayloads = {
|
||||
["name"] = "Liberation BAI",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
|
||||
["CLSID"] = "{M299_4xAGM_114L}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
|
||||
["CLSID"] = "{M299_4xAGM_114L}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
|
||||
["CLSID"] = "{M299_4xAGM_114L}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
|
||||
["CLSID"] = "{M299_4xAGM_114L}",
|
||||
["num"] = 1,
|
||||
},
|
||||
},
|
||||
@@ -54,21 +54,21 @@ local unitPayloads = {
|
||||
["name"] = "Liberation OCA/Aircraft",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{M261_M229}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{M261_M229}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[4] = {
|
||||
[2] = {
|
||||
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{M261_M229}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{M261_M229}",
|
||||
["num"] = 1,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 31,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
},
|
||||
"airfield32_3": {
|
||||
"name": "Beslan",
|
||||
"callsign": "",
|
||||
"callsign": "ICH",
|
||||
"beacon_type": 14,
|
||||
"hertz": 110500000,
|
||||
"channel": null
|
||||
|
||||
@@ -167,6 +167,13 @@
|
||||
"hertz": 1000000,
|
||||
"channel": 31
|
||||
},
|
||||
"airfield20_0": {
|
||||
"name": "BIO",
|
||||
"callsign": "BIO",
|
||||
"beacon_type": 9,
|
||||
"hertz": 205000000,
|
||||
"channel": null
|
||||
},
|
||||
"airfield11_0": {
|
||||
"name": "San Julian",
|
||||
"callsign": "",
|
||||
|
||||
@@ -1,18 +1,193 @@
|
||||
{
|
||||
"airfield22_0": {
|
||||
"name": "ABUDHABI",
|
||||
"callsign": "ADV",
|
||||
"beacon_type": 1,
|
||||
"hertz": 114250000,
|
||||
"world_0": {
|
||||
"name": "Kish",
|
||||
"callsign": "KIS",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117400000,
|
||||
"channel": 121
|
||||
},
|
||||
"world_1": {
|
||||
"name": "DohaAirport",
|
||||
"callsign": "DIA",
|
||||
"beacon_type": 3,
|
||||
"hertz": 112400000,
|
||||
"channel": 71
|
||||
},
|
||||
"world_2": {
|
||||
"name": "HamadInternationalAirport",
|
||||
"callsign": "DOH",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114400000,
|
||||
"channel": 91
|
||||
},
|
||||
"world_3": {
|
||||
"name": "DezfulAirport",
|
||||
"callsign": "DZF",
|
||||
"beacon_type": 8,
|
||||
"hertz": 293000,
|
||||
"channel": null
|
||||
},
|
||||
"airfield22_1": {
|
||||
"world_4": {
|
||||
"name": "AbadanIntAirport",
|
||||
"callsign": "ABD",
|
||||
"beacon_type": 3,
|
||||
"hertz": 115100000,
|
||||
"channel": 98
|
||||
},
|
||||
"world_5": {
|
||||
"name": "AhvazIntAirport",
|
||||
"callsign": "AWZ",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114000000,
|
||||
"channel": 87
|
||||
},
|
||||
"world_6": {
|
||||
"name": "AghajariAirport",
|
||||
"callsign": "AJR",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114900000,
|
||||
"channel": 96
|
||||
},
|
||||
"world_7": {
|
||||
"name": "BirjandIntAirport",
|
||||
"callsign": "BJD",
|
||||
"beacon_type": 3,
|
||||
"hertz": 113500000,
|
||||
"channel": 82
|
||||
},
|
||||
"world_8": {
|
||||
"name": "BushehrIntAirport",
|
||||
"callsign": "BUZ",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117450000,
|
||||
"channel": 121
|
||||
},
|
||||
"world_9": {
|
||||
"name": "KonarakAirport",
|
||||
"callsign": "CBH",
|
||||
"beacon_type": 3,
|
||||
"hertz": 115600000,
|
||||
"channel": 103
|
||||
},
|
||||
"world_10": {
|
||||
"name": "IsfahanIntAirport",
|
||||
"callsign": "ISN",
|
||||
"beacon_type": 3,
|
||||
"hertz": 113200000,
|
||||
"channel": 79
|
||||
},
|
||||
"world_11": {
|
||||
"name": "KhoramabadAirport",
|
||||
"callsign": "KRD",
|
||||
"beacon_type": 3,
|
||||
"hertz": 113750000,
|
||||
"channel": 84
|
||||
},
|
||||
"world_12": {
|
||||
"name": "PersianGulfIntAirport",
|
||||
"callsign": "PRG",
|
||||
"beacon_type": 3,
|
||||
"hertz": 112100000,
|
||||
"channel": 58
|
||||
},
|
||||
"world_13": {
|
||||
"name": "YasoujAirport",
|
||||
"callsign": "YSJ",
|
||||
"beacon_type": 3,
|
||||
"hertz": 116550000,
|
||||
"channel": 112
|
||||
},
|
||||
"world_14": {
|
||||
"name": "BamAirport",
|
||||
"callsign": "BAM",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114900000,
|
||||
"channel": 96
|
||||
},
|
||||
"world_15": {
|
||||
"name": "MahshahrAirport",
|
||||
"callsign": "MAH",
|
||||
"beacon_type": 3,
|
||||
"hertz": 115800000,
|
||||
"channel": 105
|
||||
},
|
||||
"world_16": {
|
||||
"name": "IranShahrAirport",
|
||||
"callsign": "ISR",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117000000,
|
||||
"channel": 117
|
||||
},
|
||||
"world_17": {
|
||||
"name": "LamerdAirport",
|
||||
"callsign": "LAM",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117000000,
|
||||
"channel": 117
|
||||
},
|
||||
"world_18": {
|
||||
"name": "SirjanAirport",
|
||||
"callsign": "SRJ",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114600000,
|
||||
"channel": 93
|
||||
},
|
||||
"world_19": {
|
||||
"name": "YazdIntAirport",
|
||||
"callsign": "YZD",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117700000,
|
||||
"channel": 124
|
||||
},
|
||||
"world_20": {
|
||||
"name": "ZabolAirport",
|
||||
"callsign": "ZAL",
|
||||
"beacon_type": 3,
|
||||
"hertz": 113100000,
|
||||
"channel": 78
|
||||
},
|
||||
"world_21": {
|
||||
"name": "ZahedanIntAirport",
|
||||
"callsign": "ZDN",
|
||||
"beacon_type": 3,
|
||||
"hertz": 116000000,
|
||||
"channel": 107
|
||||
},
|
||||
"world_22": {
|
||||
"name": "RafsanjanAirport",
|
||||
"callsign": "RAF",
|
||||
"beacon_type": 3,
|
||||
"hertz": 112300000,
|
||||
"channel": 70
|
||||
},
|
||||
"world_23": {
|
||||
"name": "SaravanAirport",
|
||||
"callsign": "SRN",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114100000,
|
||||
"channel": 88
|
||||
},
|
||||
"world_24": {
|
||||
"name": "BuHasa",
|
||||
"callsign": "BH",
|
||||
"beacon_type": 2,
|
||||
"hertz": 309000000,
|
||||
"channel": null
|
||||
},
|
||||
"airfield22_0": {
|
||||
"name": "AbuDhabiInt",
|
||||
"callsign": "ADV",
|
||||
"beacon_type": 2,
|
||||
"hertz": 114250000,
|
||||
"channel": 119
|
||||
},
|
||||
"airfield22_1": {
|
||||
"name": "ABUDHABI",
|
||||
"callsign": "ADV",
|
||||
"beacon_type": 1,
|
||||
"hertz": 114250000,
|
||||
"channel": null
|
||||
},
|
||||
"airfield1_0": {
|
||||
"name": "Abumusa",
|
||||
"callsign": "ABM",
|
||||
@@ -530,180 +705,5 @@
|
||||
"beacon_type": 4,
|
||||
"hertz": 114200000,
|
||||
"channel": 89
|
||||
},
|
||||
"world_0": {
|
||||
"name": "Kish",
|
||||
"callsign": "KIS",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117400000,
|
||||
"channel": 121
|
||||
},
|
||||
"world_1": {
|
||||
"name": "DohaAirport",
|
||||
"callsign": "DIA",
|
||||
"beacon_type": 3,
|
||||
"hertz": 112400000,
|
||||
"channel": 71
|
||||
},
|
||||
"world_2": {
|
||||
"name": "HamadInternationalAirport",
|
||||
"callsign": "DOH",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114400000,
|
||||
"channel": 91
|
||||
},
|
||||
"world_3": {
|
||||
"name": "DezfulAirport",
|
||||
"callsign": "DZF",
|
||||
"beacon_type": 8,
|
||||
"hertz": 293000,
|
||||
"channel": null
|
||||
},
|
||||
"world_4": {
|
||||
"name": "AbadanIntAirport",
|
||||
"callsign": "ABD",
|
||||
"beacon_type": 3,
|
||||
"hertz": 115100000,
|
||||
"channel": 98
|
||||
},
|
||||
"world_5": {
|
||||
"name": "AhvazIntAirport",
|
||||
"callsign": "AWZ",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114000000,
|
||||
"channel": 87
|
||||
},
|
||||
"world_6": {
|
||||
"name": "AghajariAirport",
|
||||
"callsign": "AJR",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114900000,
|
||||
"channel": 96
|
||||
},
|
||||
"world_7": {
|
||||
"name": "BirjandIntAirport",
|
||||
"callsign": "BJD",
|
||||
"beacon_type": 3,
|
||||
"hertz": 113500000,
|
||||
"channel": 82
|
||||
},
|
||||
"world_8": {
|
||||
"name": "BushehrIntAirport",
|
||||
"callsign": "BUZ",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117450000,
|
||||
"channel": 121
|
||||
},
|
||||
"world_9": {
|
||||
"name": "KonarakAirport",
|
||||
"callsign": "CBH",
|
||||
"beacon_type": 3,
|
||||
"hertz": 115600000,
|
||||
"channel": 103
|
||||
},
|
||||
"world_10": {
|
||||
"name": "IsfahanIntAirport",
|
||||
"callsign": "ISN",
|
||||
"beacon_type": 3,
|
||||
"hertz": 113200000,
|
||||
"channel": 79
|
||||
},
|
||||
"world_11": {
|
||||
"name": "KhoramabadAirport",
|
||||
"callsign": "KRD",
|
||||
"beacon_type": 3,
|
||||
"hertz": 113750000,
|
||||
"channel": 84
|
||||
},
|
||||
"world_12": {
|
||||
"name": "PersianGulfIntAirport",
|
||||
"callsign": "PRG",
|
||||
"beacon_type": 3,
|
||||
"hertz": 112100000,
|
||||
"channel": 58
|
||||
},
|
||||
"world_13": {
|
||||
"name": "YasoujAirport",
|
||||
"callsign": "YSJ",
|
||||
"beacon_type": 3,
|
||||
"hertz": 116550000,
|
||||
"channel": 112
|
||||
},
|
||||
"world_14": {
|
||||
"name": "BamAirport",
|
||||
"callsign": "BAM",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114900000,
|
||||
"channel": 96
|
||||
},
|
||||
"world_15": {
|
||||
"name": "MahshahrAirport",
|
||||
"callsign": "MAH",
|
||||
"beacon_type": 3,
|
||||
"hertz": 115800000,
|
||||
"channel": 105
|
||||
},
|
||||
"world_16": {
|
||||
"name": "IranShahrAirport",
|
||||
"callsign": "ISR",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117000000,
|
||||
"channel": 117
|
||||
},
|
||||
"world_17": {
|
||||
"name": "LamerdAirport",
|
||||
"callsign": "LAM",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117000000,
|
||||
"channel": 117
|
||||
},
|
||||
"world_18": {
|
||||
"name": "SirjanAirport",
|
||||
"callsign": "SRJ",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114600000,
|
||||
"channel": 93
|
||||
},
|
||||
"world_19": {
|
||||
"name": "YazdIntAirport",
|
||||
"callsign": "YZD",
|
||||
"beacon_type": 3,
|
||||
"hertz": 117700000,
|
||||
"channel": 124
|
||||
},
|
||||
"world_20": {
|
||||
"name": "ZabolAirport",
|
||||
"callsign": "ZAL",
|
||||
"beacon_type": 3,
|
||||
"hertz": 113100000,
|
||||
"channel": 78
|
||||
},
|
||||
"world_21": {
|
||||
"name": "ZahedanIntAirport",
|
||||
"callsign": "ZDN",
|
||||
"beacon_type": 3,
|
||||
"hertz": 116000000,
|
||||
"channel": 107
|
||||
},
|
||||
"world_22": {
|
||||
"name": "RafsanjanAirport",
|
||||
"callsign": "RAF",
|
||||
"beacon_type": 3,
|
||||
"hertz": 112300000,
|
||||
"channel": 70
|
||||
},
|
||||
"world_23": {
|
||||
"name": "SaravanAirport",
|
||||
"callsign": "SRN",
|
||||
"beacon_type": 3,
|
||||
"hertz": 114100000,
|
||||
"channel": 88
|
||||
},
|
||||
"world_24": {
|
||||
"name": "BuHasa",
|
||||
"callsign": "BH",
|
||||
"beacon_type": 2,
|
||||
"hertz": 309000000,
|
||||
"channel": null
|
||||
}
|
||||
}
|
||||
1045
resources/dcs/beacons/sinai.json
Normal file
1045
resources/dcs/beacons/sinai.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,7 @@ naval_units:
|
||||
- CVN-74 John C. Stennis
|
||||
missiles: []
|
||||
air_defense_units:
|
||||
- SAM Patriot STR
|
||||
- EWR AN/FPS-117 Radar
|
||||
- M163 Vulcan Air Defense System
|
||||
- M1097 Heavy HMMWV Avenger
|
||||
- M48 Chaparral
|
||||
|
||||
@@ -75,7 +75,7 @@ naval_units:
|
||||
- CVN-74 John C. Stennis
|
||||
missiles: []
|
||||
air_defense_units:
|
||||
- SAM Patriot STR
|
||||
- EWR AN/FPS-117 Radar
|
||||
- M163 Vulcan Air Defense System
|
||||
- M1097 Heavy HMMWV Avenger
|
||||
- M48 Chaparral
|
||||
|
||||
@@ -30,6 +30,7 @@ frontline_units:
|
||||
- QF 3.7-inch AA Gun
|
||||
artillery_units:
|
||||
- M12 Gun Motor Carriage
|
||||
- FH M2A1 105mm
|
||||
logistics_units:
|
||||
- Truck Bedford
|
||||
- Truck GMC "Jimmy" 6x6 Truck
|
||||
|
||||
@@ -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
|
||||
@@ -86,7 +95,7 @@ naval_units:
|
||||
- CVN-74 John C. Stennis
|
||||
missiles: []
|
||||
air_defense_units:
|
||||
- SAM Patriot STR
|
||||
- EWR AN/FPS-117 Radar
|
||||
- M1097 Heavy HMMWV Avenger
|
||||
requirements: {}
|
||||
carrier_names:
|
||||
@@ -94,6 +103,7 @@ carrier_names:
|
||||
- CVN-72 Abraham Lincoln
|
||||
- CVN-73 George Washington
|
||||
- CVN-74 John C. Stennis
|
||||
- CVN-75 Harry S. Truman
|
||||
helicopter_carrier_names:
|
||||
- LHA-1 Tarawa
|
||||
- LHA-2 Saipan
|
||||
@@ -107,7 +117,7 @@ liveries_overrides:
|
||||
J-11A Flanker-L:
|
||||
- USN Aggressor VFC-13 'Ferris' (Fictional)
|
||||
JF-17 Thunder:
|
||||
- 'Chips' Camo for Blue Side (Fictional)
|
||||
- "'Chips' Camo for Blue Side (Fictional)"
|
||||
Ka-50 Hokum:
|
||||
- georgia camo
|
||||
Ka-50 Hokum (Blackshark 3):
|
||||
|
||||
@@ -22,7 +22,7 @@ tankers:
|
||||
frontline_units:
|
||||
- BMP-1
|
||||
- HQ-7 Launcher
|
||||
- T-55A
|
||||
- MT Type 59
|
||||
- Type 04A (ZBD-04A)
|
||||
- Type 96B (ZTZ-96B)
|
||||
artillery_units:
|
||||
@@ -52,6 +52,7 @@ naval_units:
|
||||
- Type 052B Destroyer
|
||||
- Type 052C Destroyer
|
||||
- Type 054A Frigate
|
||||
- Type 093 Attack Submarine
|
||||
- CV 1143.5 Admiral Kuznetsov
|
||||
- Type 071 Amphibious Transport Dock
|
||||
air_defense_units:
|
||||
|
||||
79
resources/factions/egypt_2000.yaml
Normal file
79
resources/factions/egypt_2000.yaml
Normal 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
|
||||
@@ -42,7 +42,7 @@ naval_units:
|
||||
- CG Ticonderoga
|
||||
missiles: []
|
||||
air_defense_units:
|
||||
- SAM Patriot STR
|
||||
- EWR AN/FPS-117 Radar
|
||||
- Flakpanzer Gepard
|
||||
requirements: {}
|
||||
carrier_names: []
|
||||
|
||||
@@ -74,3 +74,6 @@ missiles:
|
||||
- SSM SS-1C Scud-B
|
||||
has_jtac: true
|
||||
jtac_unit: MQ-9 Reaper
|
||||
liveries_overrides:
|
||||
F-14A Tomcat (Block 135-GR Late):
|
||||
- Rogue Nation(Top Gun - Maverick)
|
||||
|
||||
@@ -48,7 +48,7 @@ naval_units:
|
||||
- LHA-1 Tarawa
|
||||
missiles: []
|
||||
air_defense_units:
|
||||
- SAM Patriot STR
|
||||
- EWR AN/FPS-117 Radar
|
||||
- Flakpanzer Gepard
|
||||
requirements: {}
|
||||
carrier_names: []
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user