Compare commits

..

9 Commits

Author SHA1 Message Date
Dan Albert
7614017828 Bump version to 7.0.1. 2023-05-23 01:41:13 -07:00
Dan Albert
61879aeafa Fix line endings.
These get broken whenever someone uses the GitHub file upload editor,
since that doesn't understand .gitattributes.

(cherry picked from commit 6f4ac1dc39)
2023-05-23 00:50:38 -07:00
Starfire13
f5b9052257 Update Golan Heights and Caen to Evreux campaigns.
I had asked Khopa for permission to edit two of his campaigns to fix
some issues. Only the YAMLs have been edited, .miz files did not need
changes. I have tested both YAMLs to make sure campaigns will generate.
Also tested generating turn 1 .miz and ran it in DCS.

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

Caen to Evreux:
1. Re-arranged squadrons for better force distribution and revised
primary and secondary mission types for better default play experience.
2. Implemented squadron limits.

(cherry picked from commit f831c8efdd)
2023-05-23 00:50:38 -07:00
Starfire13
b27d2be0d1 Update Apache loadouts.
BAI loadout updated to use the new radar guided hellfires. Aux tanks
removed in favour of extra cannon ammo.

(cherry picked from commit e3c6b03603)
2023-05-20 02:34:02 -07:00
Dan Albert
e1a1eca5da Fix syntax error in bluefor_modern.yaml.
(cherry picked from commit 7a2e8279cd)
2023-05-19 18:00:32 -07:00
Dan Albert
c695db0f98 Checkpoint game before sim, auto-revert on abort.
An alternative to
https://github.com/dcs-liberation/dcs_liberation/pull/2891 that I ended
up liking much better (I had assumed some part of the UI would fail or
at least look terrible with this approach, but it seems to work quite
well).

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

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2735.

(cherry picked from commit 24e72475b4)
2023-05-19 17:53:34 -07:00
Starfire13
ff20f16109 Update scenic_inland.yaml
A formatting fix for scenic route 2 that was preventing new campaign start. Fixing at Fuzzle's request as he doesn't have the time for it right now.

(cherry picked from commit f10350dac4)
2023-05-19 17:43:59 -07:00
Dan Albert
8af3dc6965 Fuzzle campaign updates.
https://github.com/dcs-liberation/dcs_liberation/issues/2889
(cherry picked from commit f068976749)
2023-05-19 01:28:08 -07:00
Dan Albert
e6cf253e45 Attempt to reset the simulation on abort.
This is optional because I really don't know if I trust it. I don't see
much wrong with it (aside from the warning about not using it with auto-
resolve, because it won't restore lost aircraft), but it's really not
something I'd built for since it's not going to be possible as the RTS
features grow.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2735.

(cherry picked from commit 4b4c45e90f)
2023-05-19 01:19:49 -07:00
350 changed files with 633 additions and 5341 deletions

View File

@@ -31,7 +31,7 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 7.1.0
- 6.1.1
- 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.zip` file found in `%USERPROFILE%/Saved
Games/DCS/Liberation/Saves`), so most bugs filed without saved games
will be closed without investigation.
`.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.
Other useful files to include are:
@@ -73,7 +73,10 @@ 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.
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).
You can attach files to the bug by dragging and dropping the file into

View File

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

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

@@ -1,56 +1,3 @@
# 8.1.0
Saves from 8.0.0 are compatible with 8.1.0
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41363, including F-15E support.
* **[UI]** Flight loadout/properties tab is now scrollable.
## Fixes
* **[Campaign]** Fixed liveries for premade squadrons all being off-by-one.
* **[UI]** Fixed numbering of waypoints in the map and flight dialog (first waypoint is now 0 rather than 1).
# 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.

View File

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

View File

@@ -3,48 +3,36 @@ 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,
PreloadedState,
ThunkAction,
combineReducers,
configureStore,
} from "@reduxjs/toolkit";
import unculledZonesReducer from "../api/unculledZonesSlice";
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
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 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),
});
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 AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,10 +30,18 @@ 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">
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
{cez}
</LayersControl.Overlay>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
import SplitLines from "./SplitLines";
import { screen } from "@testing-library/dom";
import { render } from "@testing-library/react";
describe("SplitLines", () => {
it("joins items with line break tags", () => {
render(
<div data-testid={"container"}>
<SplitLines items={["foo", "bar", "baz"]} />
</div>
);
const container = screen.getByTestId("container");
expect(container).toContainHTML("foo<br />bar<br />baz<br />");
});
});

View File

@@ -1,159 +0,0 @@
import { renderWithProviders } from "../../testutils";
import SupplyRoute, { RouteColor } from "./SupplyRoute";
import { screen } from "@testing-library/react";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: PropsWithChildren<any>) => {
mockPolyline(props);
return <>{props.children}</>;
},
Tooltip: (props: PropsWithChildren<any>) => {
return <p data-testid="tooltip">{props.children}</p>;
},
}));
describe("SupplyRoute", () => {
it("is red when inactive and owned by opfor", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: false,
active_transports: [],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
color: RouteColor.Red,
})
);
});
it("is blue when inactive and owned by bluefor", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: true,
active_transports: [],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
color: RouteColor.Blue,
})
);
});
it("is orange when contested", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: true,
is_sea: false,
blue: false,
active_transports: [],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
color: RouteColor.Contested,
})
);
});
it("is highlighted when the route has active transports", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: false,
active_transports: ["foo"],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
color: RouteColor.Highlight,
})
);
});
it("is drawn in the right place", () => {
const points = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 1 },
];
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: points,
front_active: false,
is_sea: false,
blue: false,
active_transports: ["foo"],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: points,
})
);
});
it("has a tooltip describing an inactive supply route", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: false,
active_transports: [],
}}
/>
);
const tooltip = screen.getByTestId("tooltip");
expect(tooltip).toHaveTextContent("This supply route is inactive.");
});
it("has a tooltip describing active supply routes", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: false,
active_transports: ["foo", "bar"],
}}
/>
);
const tooltip = screen.getByTestId("tooltip");
expect(tooltip).toContainHTML("foo<br />bar");
});
});

View File

@@ -4,13 +4,6 @@ import { Polyline as LPolyline } from "leaflet";
import { useEffect, useRef } from "react";
import { Polyline, Tooltip } from "react-leaflet";
export enum RouteColor {
Blue = "#2d3e50",
Contested = "#c85050",
Highlight = "#ffffff",
Red = "#8c1414",
}
interface SupplyRouteProps {
route: SupplyRouteModel;
}
@@ -33,22 +26,18 @@ function ActiveSupplyRouteHighlight(props: SupplyRouteProps) {
}
return (
<Polyline
positions={props.route.points}
color={RouteColor.Highlight}
weight={2}
/>
<Polyline positions={props.route.points} color={"#ffffff"} weight={2} />
);
}
function colorFor(route: SupplyRouteModel) {
if (route.front_active) {
return RouteColor.Contested;
return "#c85050";
}
if (route.blue) {
return RouteColor.Blue;
return "#2d3e50";
}
return RouteColor.Red;
return "#8c1414";
}
export default function SupplyRoute(props: SupplyRouteProps) {

View File

@@ -1,5 +1,5 @@
import App from "./App";
import { setupStore } from "./app/store";
import { store } 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={setupStore()}>
<Provider store={store}>
<SocketProvider>
<App />
</SocketProvider>

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,6 @@ 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
@@ -63,13 +62,6 @@ 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:
@@ -203,7 +195,8 @@ class FlightPlan(ABC, Generic[LayoutT]):
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
)
def default_tot_offset(self) -> timedelta:
@property
def 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

View File

@@ -25,6 +25,10 @@ if TYPE_CHECKING:
class FormationAttackFlightPlan(FormationFlightPlan, ABC):
@property
def lead_time(self) -> timedelta:
return timedelta()
@property
def package_speed_waypoints(self) -> set[FlightWaypoint]:
return {
@@ -46,6 +50,13 @@ 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(

View File

@@ -16,8 +16,9 @@ class SeadFlightPlan(FormationAttackFlightPlan):
def builder_type() -> Type[Builder]:
return Builder
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=1)
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=1)
class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):

View File

@@ -38,6 +38,10 @@ class SweepLayout(LoiterLayout):
class SweepFlightPlan(LoiterFlightPlan):
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=5)
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@@ -50,8 +54,9 @@ class SweepFlightPlan(LoiterFlightPlan):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.sweep_end
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=5)
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
@property
def sweep_start_time(self) -> datetime:

View File

@@ -34,6 +34,10 @@ 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
@@ -60,8 +64,9 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
return {self.layout.patrol_start, self.layout.patrol_end}
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=2)
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.patrol_end:

View File

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

View File

@@ -5,6 +5,7 @@ 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
@@ -36,7 +37,6 @@ 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,6 +81,12 @@ AWACS_BUDGET_COST = 4
PLAYER_BUDGET_IMPORTANCE_LOG = 2
class TurnState(Enum):
WIN = 0
LOSS = 1
CONTINUE = 2
class Game:
def __init__(
self,

View File

@@ -353,14 +353,6 @@ class MissionGenerator:
gen.generate()
def setup_combined_arms(self) -> None:
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
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

View File

@@ -315,10 +315,6 @@ 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 ?

View File

@@ -1,7 +1,6 @@
"""Runway information and selection."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Iterator, Optional, TYPE_CHECKING
@@ -52,20 +51,7 @@ class RunwayData:
atc = atc_radio.uhf
for beacon_data in airport.beacons:
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
beacon = Beacons.with_id(beacon_data.id, theater)
if beacon.is_tacan:
tacan = beacon.tacan_channel
tacan_callsign = beacon.callsign

View File

@@ -41,8 +41,7 @@ def has_save_compat_for(
"""
def decorator(func: Callable[..., ReturnT]) -> Callable[..., ReturnT]:
# Allow current and previous version to ease cherry-picking.
if major not in {MAJOR_VERSION - 1, MAJOR_VERSION}:
if major != MAJOR_VERSION:
raise DeprecatedSaveCompatError(func.__name__)
return func

View File

@@ -6,16 +6,30 @@ from starlette.responses import Response
from game import Game
from game.ato import Flight
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.server import GameContext
from game.server.leaflet import LeafletPoint
from game.server.waypoints.models import FlightWaypointJs
from game.sim import GameUpdateEvents
from game.utils import meters
router: APIRouter = APIRouter(prefix="/waypoints")
def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]:
return [
departure = FlightWaypointJs.for_waypoint(
FlightWaypoint(
"TAKEOFF",
FlightWaypointType.TAKEOFF,
flight.departure.position,
meters(0),
"RADIO",
),
flight,
0,
)
return [departure] + [
FlightWaypointJs.for_waypoint(w, flight, i)
for i, w in enumerate(flight.flight_plan.waypoints, 1)
]
@@ -50,7 +64,7 @@ def set_position(
if waypoint_idx == 0:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
waypoint = flight.flight_plan.waypoints[waypoint_idx]
waypoint = flight.flight_plan.waypoints[waypoint_idx - 1]
waypoint.position = Point.from_latlng(
LatLng(position.lat, position.lng), game.theater.terrain
)

View File

@@ -42,8 +42,6 @@ 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.
@@ -312,41 +310,6 @@ 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)",
@@ -491,16 +454,6 @@ 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,
@@ -581,10 +534,6 @@ 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__:

View File

@@ -5,8 +5,7 @@ import random
from collections.abc import Iterable
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Sequence, TYPE_CHECKING, Any
from uuid import uuid4, UUID
from typing import Optional, Sequence, TYPE_CHECKING
from faker import Faker
@@ -14,7 +13,6 @@ 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:
@@ -28,8 +26,6 @@ if TYPE_CHECKING:
@dataclass
class Squadron:
id: UUID = field(init=False, default_factory=uuid4)
name: str
nickname: Optional[str]
country: str
@@ -65,24 +61,21 @@ 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.id)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Squadron):
return False
return self.id == other.id
return hash(
(
self.name,
self.nickname,
self.country,
self.role,
self.aircraft,
)
)
@property
def player(self) -> bool:

View File

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

View File

@@ -434,14 +434,6 @@ 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__(
@@ -474,14 +466,6 @@ 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__(

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
from pathlib import Path
MAJOR_VERSION = 8
MINOR_VERSION = 1
MICRO_VERSION = 0
MAJOR_VERSION = 7
MINOR_VERSION = 0
MICRO_VERSION = 1
VERSION_NUMBER = ".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
@@ -175,14 +175,4 @@ VERSION = _build_version_string()
#:
#: Version 10.7
#: * Support for defining squadron sizes.
#:
#: 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)
CAMPAIGN_FORMAT_VERSION = (10, 7)

View File

@@ -9,14 +9,8 @@ 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}
OV10_Paratrooper = {
"clsid": "OV10_Paratrooper",
"name": "OV10_Paratrooper",
"weight": 400,
}
ParaTrooper = {"clsid": "{PARA}", "name": "ParaTrooper", "weight": 80}
Fuel_Tank_150_gallons_ = {
"clsid": "{150gal}",
"name": "Fuel Tank 150 gallons",
@@ -53,11 +47,6 @@ 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}
@@ -72,7 +61,6 @@ 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,
@@ -81,62 +69,6 @@ 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}
@@ -151,7 +83,6 @@ 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,
@@ -160,62 +91,6 @@ 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_)
@@ -224,7 +99,6 @@ 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}
@@ -239,7 +113,6 @@ 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,
@@ -248,62 +121,6 @@ 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}
@@ -318,7 +135,6 @@ 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,
@@ -327,76 +143,15 @@ 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:
OV10_Paratrooper = (8, Weapons.OV10_Paratrooper)
ParaTrooper = (8, Weapons.ParaTrooper)
class Pylon9:
OV10_SMOKE = (9, Weapons.OV10_SMOKE)

View File

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

View File

@@ -1,19 +1,17 @@
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, QDialog
from PySide6.QtWidgets import QApplication, QCheckBox, QSplashScreen
from dcs.payloads import PayloadDirectories
from game import Game, VERSION, logging_config, persistence
@@ -36,7 +34,6 @@ 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 (
@@ -70,7 +67,7 @@ def on_game_load(game: Game | None) -> None:
EventStream.put_nowait(GameUpdateEvents().game_loaded(game))
def run_ui(create_game_params: CreateGameParams | None, ui_flags: UiFlags) -> None:
def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" # Potential fix for 4K screens
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
@@ -111,6 +108,8 @@ def run_ui(create_game_params: CreateGameParams | None, ui_flags: UiFlags) -> No
uiconstants.load_event_icons()
uiconstants.load_aircraft_icons()
uiconstants.load_vehicle_icons()
uiconstants.load_aircraft_banners()
uiconstants.load_vehicle_banners()
# Show warning if no DCS Installation directory was set
if liberation_install.get_dcs_install_directory() == "":
@@ -152,11 +151,6 @@ def run_ui(create_game_params: CreateGameParams | None, ui_flags: UiFlags) -> No
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()
@@ -259,12 +253,6 @@ 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.")
@@ -273,68 +261,60 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args()
@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,
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."
)
def create_game(params: CreateGameParams) -> Game:
campaign = Campaign.from_file(params.campaign_path)
theater = campaign.load_theater(params.advanced_iads)
# 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)
faction_loader = Factions.load()
lua_plugin_manager = LuaPluginManager.load()
lua_plugin_manager.merge_player_settings()
generator = GameGenerator(
faction_loader.get_by_name(params.blue),
faction_loader.get_by_name(params.red),
faction_loader.get_by_name(blue),
faction_loader.get_by_name(red),
theater,
campaign.load_air_wing_config(theater),
Settings(
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,
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,
),
GeneratorSettings(
start_date=params.start_date,
start_date=start_date,
start_time=campaign.recommended_start_time,
player_budget=DEFAULT_BUDGET,
enemy_budget=DEFAULT_BUDGET,
inverted=params.inverted,
inverted=inverted,
advanced_iads=theater.iads_network.advanced_iads,
no_carrier=False,
no_lha=False,
@@ -354,10 +334,7 @@ def create_game(params: CreateGameParams) -> Game:
lua_plugin_manager,
)
game = generator.generate()
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)
game.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
return game
@@ -428,6 +405,8 @@ 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.
@@ -436,6 +415,21 @@ 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
@@ -444,10 +438,7 @@ def main():
return
with Server().run_in_thread():
run_ui(
CreateGameParams.from_args(args),
UiFlags(args.dev, args.show_sim_speed_controls),
)
run_ui(game, UiFlags(args.dev, args.show_sim_speed_controls))
if __name__ == "__main__":

View File

@@ -8,12 +8,15 @@ 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")
@@ -202,7 +205,6 @@ def load_aircraft_icons():
AIRCRAFT_ICONS[f1] = AIRCRAFT_ICONS["Mirage-F1C-200"]
AIRCRAFT_ICONS["Mirage-F1M-CE"] = AIRCRAFT_ICONS["Mirage-F1CE"]
AIRCRAFT_ICONS["MB-339A"] = AIRCRAFT_ICONS["MB-339A PAN"]
AIRCRAFT_ICONS["F-15ESE"] = AIRCRAFT_ICONS["F-15E"]
def load_vehicle_icons():
@@ -211,3 +213,25 @@ def load_vehicle_icons():
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(
os.path.join("./resources/ui/units/vehicles/icons/", vehicle)
)
def load_aircraft_banners():
for aircraft in os.listdir("./resources/ui/units/aircrafts/banners/"):
if aircraft.endswith(".jpg"):
AIRCRAFT_BANNERS[aircraft[:-7]] = QPixmap(
os.path.join("./resources/ui/units/aircrafts/banners/", aircraft)
)
variants = ["Mirage-F1CT", "Mirage-F1EE", "Mirage-F1M-EE", "Mirage-F1EQ"]
for f1 in variants:
AIRCRAFT_BANNERS[f1] = AIRCRAFT_BANNERS["Mirage-F1C-200"]
variants = ["Mirage-F1CE", "Mirage-F1M-CE"]
for f1 in variants:
AIRCRAFT_BANNERS[f1] = AIRCRAFT_BANNERS["Mirage-F1C"]
def load_vehicle_banners():
for aircraft in os.listdir("./resources/ui/units/vehicles/banners/"):
if aircraft.endswith(".jpg"):
VEHICLE_BANNERS[aircraft[:-7]] = QPixmap(
os.path.join("./resources/ui/units/vehicles/banners/", aircraft)
)

View File

@@ -25,7 +25,9 @@ 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
@@ -67,8 +69,24 @@ 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:
@@ -84,6 +102,7 @@ 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)
@@ -102,6 +121,9 @@ 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:
@@ -125,6 +147,14 @@ class QTopPanel(QFrame):
else:
self.proceedButton.setEnabled(True)
def open_air_wing(self):
self.dialog = AirWingDialog(self.game_model, self.window())
self.dialog.show()
def open_transfers(self):
self.dialog = PendingTransfersDialog(self.game_model)
self.dialog.show()
def passTurn(self):
with logged_duration("Skipping turn"):
self.game.pass_turn(no_action=True)

View File

@@ -24,8 +24,7 @@ class LiverySelector(QComboBox):
for idx, livery in enumerate(
squadron.aircraft.dcs_unit_type.iter_liveries_for_country(
dcs.countries.get_by_name(squadron.country)
),
1, # First entry is "Default".
)
):
self.addItem(livery.name, livery)
if squadron.livery == livery.id:

View File

@@ -1,5 +1,3 @@
import textwrap
from collections import defaultdict
from typing import Iterable, Iterator, Optional
from PySide6.QtCore import (
@@ -152,49 +150,6 @@ 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)
@@ -203,13 +158,11 @@ 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)
@@ -247,7 +200,6 @@ 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()
@@ -262,14 +214,8 @@ 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:
@@ -320,26 +266,9 @@ 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)
@@ -392,7 +321,6 @@ 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}")
@@ -433,13 +361,11 @@ 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)
@@ -450,7 +376,6 @@ 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()
@@ -461,32 +386,23 @@ class SquadronConfigurationLayout(QVBoxLayout):
return
def add_squadron(self, squadron: Squadron) -> None:
squadron_config = SquadronConfigurationBox(
self.game, self.coalition, squadron, self.parking_tracker
)
squadron_config = SquadronConfigurationBox(self.game, self.coalition, squadron)
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],
parking_tracker: AirWingConfigParkingTracker,
self, game: Game, coalition: Coalition, squadrons: list[Squadron]
) -> None:
super().__init__()
layout = QVBoxLayout()
self.setLayout(layout)
self.squadrons_config = SquadronConfigurationLayout(
game, coalition, squadrons, parking_tracker
)
self.squadrons_config = SquadronConfigurationLayout(game, coalition, squadrons)
self.squadrons_config.config_changed.connect(self.on_squadron_config_changed)
scrolling_widget = QWidget()
@@ -514,16 +430,10 @@ class AircraftSquadronsPage(QWidget):
class AircraftSquadronsPanel(QStackedLayout):
page_removed = Signal(AircraftType)
def __init__(
self,
game: Game,
coalition: Coalition,
parking_tracker: AirWingConfigParkingTracker,
) -> None:
def __init__(self, game: Game, coalition: Coalition) -> 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)
@@ -543,9 +453,7 @@ class AircraftSquadronsPanel(QStackedLayout):
def new_page_for_type(
self, aircraft_type: AircraftType, squadrons: list[Squadron]
) -> None:
page = AircraftSquadronsPage(
self.game, self.coalition, squadrons, self.parking_tracker
)
page = AircraftSquadronsPage(self.game, self.coalition, squadrons)
page.remove_squadron_page.connect(self.remove_page_for_type)
self.addWidget(page)
self.squadrons_pages[aircraft_type] = page
@@ -631,77 +539,14 @@ 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,
parking_tracker: AirWingConfigParkingTracker,
) -> None:
def __init__(self, coalition: Coalition, game: Game) -> 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)
@@ -711,9 +556,7 @@ 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.parking_tracker
)
self.squadrons_panel = AircraftSquadronsPanel(game, coalition)
self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type)
layout.addLayout(self.squadrons_panel, 1, 3, 2, 1)
@@ -787,9 +630,6 @@ 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()
@@ -811,18 +651,11 @@ class AirWingConfigurationDialog(QDialog):
self.tabs = []
for coalition in game.coalitions:
coalition_tab = AirWingConfigurationTab(
coalition, game, self.parking_tracker
)
coalition_tab = AirWingConfigurationTab(coalition, game)
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")
@@ -838,29 +671,7 @@ 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()
@@ -868,16 +679,8 @@ class AirWingConfigurationDialog(QDialog):
def reject(self) -> None:
result = QMessageBox.information(
None,
"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,
)
),
"Discard changes?",
"Are you sure you want to discard your changes and start the campaign?",
QMessageBox.Yes,
QMessageBox.No,
)

View File

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

View File

@@ -24,7 +24,6 @@ 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
@@ -34,13 +33,10 @@ 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
@@ -154,7 +150,6 @@ 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)
@@ -226,12 +221,6 @@ 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)
@@ -264,8 +253,6 @@ 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()
@@ -335,6 +322,7 @@ class QLiberationWindow(QMainWindow):
def newGame(self):
wizard = NewGameWizard(self)
wizard.show()
wizard.accepted.connect(lambda: self.onGameGenerated(wizard.generatedGame))
def openFile(self):
if (
@@ -538,14 +526,6 @@ 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()
@@ -560,14 +540,7 @@ class QLiberationWindow(QMainWindow):
def onDebriefing(self, debrief: Debriefing):
logging.info("On Debriefing")
self.debriefing = QDebriefingWindow(debrief)
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)
self.debriefing.show()
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show()

View File

@@ -1,48 +1,14 @@
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtGui import QIcon
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
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"
elif unit_type.dcs_id == "F-15ESE":
name = "F-15E"
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__}")
from qt_ui.uiconstants import AIRCRAFT_BANNERS, VEHICLE_BANNERS
class QUnitInfoWindow(QDialog):
@@ -63,10 +29,14 @@ class QUnitInfoWindow(QDialog):
header = QLabel(self)
header.setGeometry(0, 0, 720, 360)
banner_path = banner_path_for(unit_type)
if not banner_path.exists():
banner_path = MISSING_BANNER_PATH
pixmap = QPixmap(banner_path)
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")
header.setPixmap(pixmap.scaled(header.width(), header.height()))
self.layout.addWidget(header, 0, 0)

View File

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

View File

@@ -9,17 +9,19 @@ 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
@@ -117,11 +119,13 @@ class QBaseMenu2(QDialog):
return self.game_model.game.settings.enable_base_capture_cheat
def cheat_capture(self) -> None:
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()
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)
@property
def has_transfer_destinations(self) -> bool:

View File

@@ -3,8 +3,10 @@ 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.cheatcontext import game_state_modifying_cheat_context
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import (
QGroundForcesStrategySelector,
)
@@ -50,12 +52,15 @@ class QGroundForcesStrategy(QGroupBox):
self.setLayout(layout)
def cheat_alter_front_line(self, enemy_point: ControlPoint, advance: bool) -> None:
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)
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)

View File

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

View File

@@ -29,7 +29,6 @@ class QEditFlightDialog(QDialog):
self.setWindowTitle("Edit flight")
self.setWindowIcon(EVENT_ICONS["strike"])
self.setModal(True)
layout = QVBoxLayout()

View File

@@ -4,8 +4,6 @@ from PySide6.QtWidgets import (
QFrame,
QLabel,
QVBoxLayout,
QScrollArea,
QWidget,
)
from game import Game
@@ -37,16 +35,6 @@ class QFlightPayloadTab(QFrame):
layout = QVBoxLayout()
scroll_content = QWidget()
scrolling_layout = QVBoxLayout()
scroll_content.setLayout(scrolling_layout)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(scroll_content)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
layout.addWidget(scroll)
# Docs Link
docsText = QLabel(
'<a href="https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Loadouts"><span style="color:#FFFFFF;">How to create your own default loadout</span></a>'
@@ -54,12 +42,12 @@ class QFlightPayloadTab(QFrame):
docsText.setAlignment(Qt.AlignCenter)
docsText.setOpenExternalLinks(True)
scrolling_layout.addLayout(PropertyEditor(self.flight))
layout.addLayout(PropertyEditor(self.flight))
self.loadout_selector = DcsLoadoutSelector(flight)
self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout)
scrolling_layout.addWidget(self.loadout_selector)
scrolling_layout.addWidget(self.payload_editor)
scrolling_layout.addWidget(docsText)
layout.addWidget(self.loadout_selector)
layout.addWidget(self.payload_editor)
layout.addWidget(docsText)
self.setLayout(layout)

View File

@@ -1,16 +1,6 @@
import logging
from datetime import timedelta
from PySide6.QtCore import QTime
from PySide6.QtWidgets import (
QGroupBox,
QLabel,
QMessageBox,
QVBoxLayout,
QTimeEdit,
QHBoxLayout,
QCheckBox,
)
from PySide6.QtWidgets import QGroupBox, QLabel, QMessageBox, QVBoxLayout
from game import Game
from game.ato.flight import Flight
@@ -20,9 +10,9 @@ from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelector
class FlightPlanPropertiesGroup(QGroupBox):
class FlightAirfieldDisplay(QGroupBox):
def __init__(self, game: Game, package_model: PackageModel, flight: Flight) -> None:
super().__init__("Flight plan properties")
super().__init__("Departure/Arrival")
self.game = game
self.package_model = package_model
self.flight = flight
@@ -38,31 +28,6 @@ class FlightPlanPropertiesGroup(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 "
@@ -93,7 +58,7 @@ class FlightPlanPropertiesGroup(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:
@@ -111,13 +76,3 @@ class FlightPlanPropertiesGroup(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()

View File

@@ -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.FlightPlanPropertiesGroup import (
FlightPlanPropertiesGroup,
from qt_ui.windows.mission.flight.settings.FlightAirfieldDisplay import (
FlightAirfieldDisplay,
)
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(FlightPlanPropertiesGroup(game, package_model, flight), 1, 0)
layout.addWidget(FlightAirfieldDisplay(game, package_model, flight), 1, 0)
layout.addWidget(QFlightSlotEditor(package_model, flight, game), 2, 0)
layout.addWidget(QFlightStartType(package_model, flight), 3, 0)
layout.addWidget(QFlightCustomName(flight), 4, 0)

View File

@@ -1,35 +1,14 @@
from PySide6.QtCore import QItemSelectionModel, QPoint, QModelIndex
from PySide6.QtCore import QItemSelectionModel, QPoint
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QHeaderView,
QTableView,
QStyledItemDelegate,
QDoubleSpinBox,
QWidget,
QStyleOptionViewItem,
)
from PySide6.QtWidgets import QHeaderView, QTableView
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__()
@@ -37,9 +16,8 @@ class QFlightWaypointList(QTableView):
self.flight = flight
self.model = QStandardItemModel(self)
self.model.itemChanged.connect(self.on_changed)
self.setModel(self.model)
self.model.setHorizontalHeaderLabels(HEADER_LABELS)
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
header = self.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
@@ -49,52 +27,27 @@ class QFlightWaypointList(QTableView):
self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select
)
self.altitude_editor_delegate = AltitudeEditorDelegate(self)
self.setItemDelegateForColumn(1, self.altitude_editor_delegate)
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()
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()
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
self.model.setHorizontalHeaderLabels(HEADER_LABELS)
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)
waypoints = self.flight.flight_plan.waypoints
# Why [1:]? Qt starts indexing at 1 rather than 0, whereas DCS numbers
# waypoints starting with 0, and for whatever reason Qt crashes whenever I
# set the vertical labels manually.
#
# Starting with the second waypoint is a bit of a hack, but it's also the
# historical behavior anyway. This view used to have waypoints starting at 1
# and just didn't show the departure waypoint because the departure waypoint
# wasn't actually part of the flight plan tracked by Liberation. That
# changed at some point, so now we need to skip it manually to preserve that
# behavior.
#
# It really ought to show the departure waypoint and start indexing at 0,
# but since this all pending a move to React anyway, it's not worth fighting
# the Qt crashes for now.
#
# https://github.com/dcs-liberation/dcs_liberation/issues/3037
for row, waypoint in enumerate(waypoints[1:]):
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(
def add_waypoint_row(
self, row: int, flight: Flight, waypoint: FlightWaypoint
) -> None:
self.model.insertRow(self.model.rowCount())
@@ -102,25 +55,15 @@ class QFlightWaypointList(QTableView):
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
altitude = int(waypoint.alt.feet)
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)
altitude_item = QStandardItem(f"{altitude} ft {altitude_type}")
altitude_item.setEditable(False)
self.model.setItem(row, 1, altitude_item)
tot = self.tot_text(flight, waypoint)
tot_item = QStandardItem(tot)
tot_item.setEditable(False)
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)
self.model.setItem(row, 2, tot_item)
def tot_text(self, flight: Flight, waypoint: FlightWaypoint) -> str:
if waypoint.waypoint_type == FlightWaypointType.TAKEOFF:

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
import logging
import textwrap
from datetime import datetime, timedelta
from typing import List
@@ -14,7 +13,6 @@ from PySide6.QtWidgets import (
QTextEdit,
QVBoxLayout,
QWidget,
QDialog,
)
from jinja2 import Environment, FileSystemLoader, select_autoescape
@@ -28,7 +26,6 @@ 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(
@@ -42,6 +39,7 @@ jinja_env = Environment(
lstrip_blocks=True,
)
"""
Possible time periods for new games
@@ -88,14 +86,9 @@ 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
@@ -121,9 +114,7 @@ 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.theater_page.campaignList.selected_campaign
)
self.difficulty_page = DifficultyAndAutomationOptions(default_settings)
self.plugins_page = PluginsPage(self.lua_plugin_manager)
# Update difficulty page on campaign select
@@ -141,6 +132,7 @@ 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")
@@ -229,14 +221,11 @@ class NewGameWizard(QtWidgets.QWizard):
mod_settings,
self.lua_plugin_manager,
)
game = generator.generate()
self.generatedGame = generator.generate()
if AirWingConfigurationDialog(game, self).exec() == QDialog.DialogCode.Rejected:
logging.info("Aborted air wing configuration")
return
AirWingConfigurationDialog(self.generatedGame, self).exec_()
game.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
GameUpdateSignal.get_instance().game_generated.emit(game)
self.generatedGame.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
super(NewGameWizard, self).accept()
@@ -578,39 +567,8 @@ 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, current_campaign: Campaign | None, parent=None
) -> None:
def __init__(self, default_settings: Settings, parent=None) -> None:
super().__init__(parent)
self.setTitle("Difficulty and automation options")
@@ -651,15 +609,10 @@ 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(
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."
)
"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."
)
)
@@ -697,7 +650,6 @@ 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):

View File

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

View File

@@ -9,7 +9,7 @@ description:
pushing south.</p>
miz: battle_of_abu_dhabi.miz
performance: 2
version: "10.9"
version: "10.2"
squadrons:
# Blue CPs:
# The default faction is Iran, but the F-14B is given higher precedence so
@@ -41,20 +41,16 @@ 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
@@ -76,7 +72,6 @@ squadrons:
- Su-25 Frogfoot
- primary: BAI
secondary: air-to-ground
size: 8
aircraft:
- F-16CM Fighting Falcon (Block 50)
- Su-24MK Fencer-D
@@ -107,7 +102,6 @@ squadrons:
- F/A-18C Hornet (Lot 20)
- F-14A Tomcat (Block 135-GR Late)
- primary: Refueling
size: 2
aircraft:
- S-3B Tanker
@@ -117,7 +111,6 @@ squadrons:
aircraft:
- AV-8B Harrier II Night Attack
- primary: CAS
size: 8
secondary: air-to-ground
aircraft:
- UH-1H Iroquois

View File

@@ -9,7 +9,7 @@ recommended_enemy_faction: Russia 2010
recommended_start_date: 2004-01-07
miz: black_sea.miz
performance: 2
version: "10.9"
version: "10.7"
squadrons:
# Anapa-Vityazevo
12:
@@ -148,7 +148,6 @@ squadrons:
secondary: air-to-ground
aircraft:
- UH-1H Iroquois
size: 8
Red CV:
- primary: BARCAP
secondary: air-to-air
@@ -165,4 +164,3 @@ squadrons:
secondary: air-to-ground
- primary: CAS
secondary: air-to-ground
size: 8

View File

@@ -12,7 +12,7 @@ recommended_enemy_faction: Germany 1944
recommended_start_date: 1944-07-04
miz: caen_to_evreux.miz
performance: 1
version: "10.9"
version: "10.7"
squadrons:
# Evreux
26:

View File

@@ -1,133 +0,0 @@
---
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: 24
- primary: TARCAP
secondary: any
aircraft:
- F-15E Strike Eagle (Suite 4+)
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: 12
# Kedem
12:
- primary: Transport
aircraft:
- CH-47D
size: 20
- primary: Air Assault
secondary: air-to-ground
aircraft:
- UH-1H Iroquois
size: 4
# Nevatim (106)
8:
- primary: AEW&C
aircraft:
- E-3A
size: 2
- primary: Refueling
aircraft:
- KC-135 Stratotanker
size: 1
- primary: Refueling
aircraft:
- KC-135 Stratotanker MPRS
size: 1
- primary: CAS
secondary: air-to-ground
aircraft:
- A-10C Thunderbolt II (Suite 7)
size: 8
# Melez (30)
5:
- primary: CAS
secondary: air-to-ground
aircraft:
- Ka-50 Hokum (Blackshark 3)
size: 4
- primary: TARCAP
secondary: air-to-air
aircraft:
- Mirage 2000C
size: 12
- primary: Strike
secondary: air-to-ground
aircraft:
- Mirage 2000C
size: 12
# Wadi al Jandali (72)
13:
- primary: AEW&C
aircraft:
- E-2C Hawkeye
size: 2
- primary: SEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: DEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: Air Assault
secondary: air-to-ground
aircraft:
- Mi-24P Hind-F
size: 4
- primary: OCA/Aircraft
secondary: air-to-ground
aircraft:
- SA 342L Gazelle
size: 4
# Cairo West (95)
18:
- primary: Transport
aircraft:
- C-130
size: 8
- primary: Escort
secondary: air-to-air
aircraft:
- MiG-29S Fulcrum-C
size: 20
- primary: BARCAP
secondary: any
aircraft:
- J-7B
size: 20

View File

@@ -8,14 +8,14 @@ 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.9"
version: "10.7"
squadrons:
# Tonopah Airport
17:
- primary: TARCAP
secondary: any
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15E Strike Eagle (Suite 4+)
- F-15C Eagle
size: 12
- primary: Strike
secondary: air-to-ground

View File

@@ -1,175 +0,0 @@
---
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

View File

@@ -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.9"
version: "10.7"
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

View File

@@ -16,7 +16,7 @@ description:
miz: grabthars_hammer.miz
performance: 2
recommended_start_date: 1999-12-25
version: "10.9"
version: "10.7"
squadrons:
#Mount Pleasant
2:
@@ -42,9 +42,9 @@ squadrons:
#San Julian
11:
- primary: BAI
secondary: any
secondary: air-to-ground
aircraft:
- F-15E Strike Eagle (Suite 4+)
- F-15E Strike Eagle
size: 8
- primary: Air Assault
secondary: any

View File

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

View File

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

View File

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

View File

@@ -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.9"
version: "10.7"
squadrons:
# Ramat David
30:
@@ -43,10 +43,10 @@ squadrons:
aircraft:
- AV-8B Harrier II Night Attack
size: 8
- primary: TARCAP
secondary: any
- primary: BARCAP
secondary: air-to-air
aircraft:
- F-15E Strike Eagle (Suite 4+)
- F-15C Eagle
size: 12
- primary: CAS
secondary: air-to-ground

View File

@@ -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.9"
version: "10.7"
squadrons:
Blue CV-1:
- primary: BARCAP
@@ -66,11 +66,6 @@ squadrons:
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 16
- primary: BAI
secondary: any
aircraft:
- F-15E Strike Eagle (Suite 4+)
size: 12
- primary: BAI
secondary: air-to-ground
aircraft:

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,560 +0,0 @@
local unitPayloads = {
["name"] = "F-15ESE",
["payloads"] = {
[1] = {
["displayName"] = "Liberation Strike",
["name"] = "Liberation Strike",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{CFT_R_MK84LD_x_2}",
["num"] = 12,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 8,
["settings"] = {
["GUI_fuze_type"] = 1,
["arm_delay_ctrl_FMU139CB_LD"] = 1,
["function_delay_ctrl_FMU139CB_LD"] = 0,
},
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{CFT_L_MK84LD_x_2}",
["num"] = 4,
},
[10] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[11] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
},
["tasks"] = {
[1] = 32,
},
},
[2] = {
["name"] = "Liberation BAI",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[2] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[3] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[4] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[5] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[6] = {
["CLSID"] = "{CFT_L_CBU_97_x_6}",
["num"] = 4,
},
[7] = {
["CLSID"] = "{CFT_R_CBU_97_x_6}",
["num"] = 12,
},
[8] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
},
["tasks"] = {
[1] = 32,
},
},
[3] = {
["name"] = "Liberation BARCAP",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 11,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 10,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 5,
},
[11] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[12] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
[13] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 32,
},
},
[4] = {
["displayName"] = "Liberation Escort",
["name"] = "Liberation Escort",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 11,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 10,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 5,
},
[11] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[12] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
[13] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 32,
},
},
[5] = {
["displayName"] = "Liberation OCA/Runway",
["name"] = "Liberation OCA/Runway",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{CFT_R_MK84LD_x_2}",
["num"] = 12,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 8,
["settings"] = {
["GUI_fuze_type"] = 1,
["arm_delay_ctrl_FMU139CB_LD"] = 1,
["function_delay_ctrl_FMU139CB_LD"] = 0,
},
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{CFT_L_MK84LD_x_2}",
["num"] = 4,
},
[10] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[11] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
},
["tasks"] = {
[1] = 32,
},
},
[6] = {
["displayName"] = "Liberation OCA/Aircraft",
["name"] = "Liberation OCA/Aircraft",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{CFT_R_MK82LD_x_6}",
["num"] = 12,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["num"] = 8,
["settings"] = {
["GUI_fuze_type"] = 1,
["arm_delay_ctrl_FMU139CB_LD"] = 1,
["function_delay_ctrl_FMU139CB_LD"] = 0,
},
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{CFT_L_MK82LD_x_6}",
["num"] = 4,
},
[10] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[11] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
},
["tasks"] = {
[1] = 32,
},
},
[7] = {
["displayName"] = "Liberation Fighter Sweep",
["name"] = "Liberation Fighter Sweep",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 11,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 10,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 5,
},
[11] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[12] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
[13] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 32,
},
},
[8] = {
["displayName"] = "Liberation DEAD",
["name"] = "Liberation DEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[6] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[7] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[8] = {
["CLSID"] = "{CFT_L_CBU_97_x_6}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{CFT_R_CBU_97_x_6}",
["num"] = 12,
},
},
["tasks"] = {
[1] = 32,
},
},
[9] = {
["displayName"] = "Liberation TARCAP",
["name"] = "Liberation TARCAP",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 11,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 10,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 5,
},
[11] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[12] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
[13] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 32,
},
},
[10] = {
["displayName"] = "Liberation CAS",
["name"] = "Liberation CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{CFT_R_CBU_97_x_6}",
["num"] = 12,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{CFT_L_CBU_97_x_6}",
["num"] = 4,
},
},
["tasks"] = {
[1] = 32,
},
},
},
["tasks"] = {
},
["unitType"] = "F-15ESE",
}
return unitPayloads

View File

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

View File

@@ -78,7 +78,7 @@
},
"airfield32_3": {
"name": "Beslan",
"callsign": "ICH",
"callsign": "",
"beacon_type": 14,
"hertz": 110500000,
"channel": null

View File

@@ -167,13 +167,6 @@
"hertz": 1000000,
"channel": 31
},
"airfield20_0": {
"name": "BIO",
"callsign": "BIO",
"beacon_type": 9,
"hertz": 205000000,
"channel": null
},
"airfield11_0": {
"name": "San Julian",
"callsign": "",

View File

@@ -1,193 +1,18 @@
{
"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
},
"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
},
"airfield22_1": {
"name": "AbuDhabiInt",
"callsign": "ADV",
"beacon_type": 2,
"hertz": 114250000,
"channel": 119
},
"airfield1_0": {
"name": "Abumusa",
"callsign": "ABM",
@@ -705,5 +530,180 @@
"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
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@ country: Combined Joint Task Forces Blue
name: NATO Desert Storm
authors: Hawkmoon
description:
<p>A faction to recreate the actual unit lineup during Desert Storm as closely
as possible</p>
<p>A faction to recreate the actual unit lineup during Desert Storm as
closely as possible</p>
aircrafts:
- A-10A Thunderbolt II
- AH-64A Apache
@@ -18,7 +18,6 @@ aircrafts:
- F-14B Tomcat
- F-15C Eagle
- F-15E Strike Eagle
- F-15E Strike Eagle (Suite 4+)
- F-16CM Fighting Falcon (Block 50)
- F-4E Phantom II
- F/A-18C Hornet (Lot 20)
@@ -73,7 +72,7 @@ naval_units:
- CVN-74 John C. Stennis
missiles: []
air_defense_units:
- EWR AN/FPS-117 Radar
- SAM Patriot STR
- M163 Vulcan Air Defense System
- M1097 Heavy HMMWV Avenger
- M48 Chaparral

View File

@@ -3,8 +3,8 @@ country: Combined Joint Task Forces Blue
name: NATO OIF
authors: Fuzzle
description:
<p>A more modern NATO mixed faction reflecting the units involved in Operation
Iraqi Freedom.</p>
<p>A more modern NATO mixed faction reflecting the units involved in
Operation Iraqi Freedom.</p>
aircrafts:
- A-10C Thunderbolt II (Suite 3)
- AH-64D Apache Longbow
@@ -19,7 +19,6 @@ aircrafts:
- F-14B Tomcat
- F-15C Eagle
- F-15E Strike Eagle
- F-15E Strike Eagle (Suite 4+)
- F-16CM Fighting Falcon (Block 50)
- F-22A Raptor
- F/A-18C Hornet (Lot 20)
@@ -76,7 +75,7 @@ naval_units:
- CVN-74 John C. Stennis
missiles: []
air_defense_units:
- EWR AN/FPS-117 Radar
- SAM Patriot STR
- M163 Vulcan Air Defense System
- M1097 Heavy HMMWV Avenger
- M48 Chaparral

View File

@@ -30,7 +30,6 @@ 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

View File

@@ -17,14 +17,9 @@ 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
- F-15E Strike Eagle (Suite 4+)
- F-16CM Fighting Falcon (Block 50)
- F-22A Raptor
- F-5E Tiger II
@@ -49,11 +44,7 @@ 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
@@ -84,7 +75,6 @@ infantry_units:
- Infantry M249
- Infantry M4
- MANPADS Stinger
- Mortar 2B11 120mm
preset_groups:
- Hawk
- Patriot
@@ -96,7 +86,7 @@ naval_units:
- CVN-74 John C. Stennis
missiles: []
air_defense_units:
- EWR AN/FPS-117 Radar
- SAM Patriot STR
- M1097 Heavy HMMWV Avenger
requirements: {}
carrier_names:
@@ -104,7 +94,6 @@ 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

View File

@@ -22,7 +22,7 @@ tankers:
frontline_units:
- BMP-1
- HQ-7 Launcher
- MT Type 59
- T-55A
- Type 04A (ZBD-04A)
- Type 96B (ZTZ-96B)
artillery_units:
@@ -52,7 +52,6 @@ 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:

View File

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

View File

@@ -42,7 +42,7 @@ naval_units:
- CG Ticonderoga
missiles: []
air_defense_units:
- EWR AN/FPS-117 Radar
- SAM Patriot STR
- Flakpanzer Gepard
requirements: {}
carrier_names: []

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