mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
82 Commits
develop-9.
...
11.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfe0c0b315 | ||
|
|
bd5087b3c7 | ||
|
|
88ebb8b612 | ||
|
|
63702f859d | ||
|
|
cc5e2ba26c | ||
|
|
b0a8d53fa6 | ||
|
|
77b7f777f6 | ||
|
|
e59da610e9 | ||
|
|
b61310d229 | ||
|
|
6550400604 | ||
|
|
1ee1113e48 | ||
|
|
01f22d6da7 | ||
|
|
8e6893d550 | ||
|
|
fa9d5525c0 | ||
|
|
5127022910 | ||
|
|
e42a7b9a59 | ||
|
|
24c9ca5d12 | ||
|
|
678dd58e7d | ||
|
|
d6879040ad | ||
|
|
d0be4b6a29 | ||
|
|
0d71372377 | ||
|
|
86de5df21e | ||
|
|
d1daa0521c | ||
|
|
ffa88688dc | ||
|
|
5ecaeca910 | ||
|
|
e1d443b697 | ||
|
|
5af4e56f30 | ||
|
|
5b858886c0 | ||
|
|
c695e7724a | ||
|
|
2fb22e4e17 | ||
|
|
ce073c24bc | ||
|
|
8de053cc7d | ||
|
|
fe6e49b22b | ||
|
|
3653dc8cbd | ||
|
|
d2b5eea0de | ||
|
|
211ec86e2e | ||
|
|
03caddc1b4 | ||
|
|
3f7618d75d | ||
|
|
dcf23c655d | ||
|
|
ef69275f34 | ||
|
|
167cea08f6 | ||
|
|
48ae55bdc2 | ||
|
|
ff2bd3f815 | ||
|
|
ba5d0bed4d | ||
|
|
4a07b8a2d8 | ||
|
|
1efce862fb | ||
|
|
80cb440e7d | ||
|
|
e970c281e8 | ||
|
|
b863e2fb83 | ||
|
|
3007a96343 | ||
|
|
463981f4bf | ||
|
|
816d1cd787 | ||
|
|
4631ee0d74 | ||
|
|
a213215c3f | ||
|
|
b014f2e543 | ||
|
|
f3d3c5f43a | ||
|
|
5ee3afeddb | ||
|
|
88591fd18c | ||
|
|
f5573cfc19 | ||
|
|
f7141a9882 | ||
|
|
a599b503f8 | ||
|
|
6c4b8c81ee | ||
|
|
2447cc156d | ||
|
|
28954d05eb | ||
|
|
65eb10639b | ||
|
|
7bc35ef7f4 | ||
|
|
46766ecbd4 | ||
|
|
3469d08461 | ||
|
|
28d959bba0 | ||
|
|
b99eb49dcf | ||
|
|
c6f812238c | ||
|
|
cc5b5fa3bb | ||
|
|
5271b3d32c | ||
|
|
8f4192edc3 | ||
|
|
183d6df8bf | ||
|
|
a825651330 | ||
|
|
f3c02816fc | ||
|
|
c4e2e45650 | ||
|
|
6613642517 | ||
|
|
b73ca2c62e | ||
|
|
8abd3c7cf9 | ||
|
|
f8a72d8f22 |
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -31,7 +31,7 @@ body:
|
|||||||
If the bug was found in a development build, select "Development build"
|
If the bug was found in a development build, select "Development build"
|
||||||
and provide a link to the build in the field below.
|
and provide a link to the build in the field below.
|
||||||
options:
|
options:
|
||||||
- 8.1.0
|
- 10.0.0
|
||||||
- Development build
|
- Development build
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
2
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -39,7 +39,7 @@ body:
|
|||||||
If the bug was found in a development build, select "Development build"
|
If the bug was found in a development build, select "Development build"
|
||||||
and provide a link to the build in the field below.
|
and provide a link to the build in the field below.
|
||||||
options:
|
options:
|
||||||
- 8.1.0
|
- 10.0.0
|
||||||
- Development build
|
- Development build
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
|||||||
- uses: actions/setup-python@v2
|
- uses: actions/setup-python@v2
|
||||||
- uses: psf/black@stable
|
- uses: psf/black@stable
|
||||||
with:
|
with:
|
||||||
version: ~=22.12
|
version: ~=23.11
|
||||||
src: "."
|
src: "."
|
||||||
options: "--check"
|
options: "--check"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.12.0
|
rev: 23.11.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
language_version: python3
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
|
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
|
||||||
|
|
||||||
[](https://patreon.com/khopa)
|
[](https://patreon.com/dcsliberation)
|
||||||
|
|
||||||
[](https://github.com/dcs-liberation/dcs_liberation/releases)
|
[](https://github.com/dcs-liberation/dcs_liberation/releases)
|
||||||
|
|
||||||
|
|||||||
33
changelog.md
33
changelog.md
@@ -1,3 +1,34 @@
|
|||||||
|
# 11.0.0
|
||||||
|
|
||||||
|
Saves from 10.x are not compatible with 11.0.0.
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
|
||||||
|
* **[Engine]** Support for DCS 2.9.3.51704.
|
||||||
|
* **[Campaign]** Improved tracking of parked aircraft deaths. Parked aircraft are now considered dead once sufficient damage is done, meaning guns, rockets and AGMs are viable weapons for OCA/Aircraft missions. Previously Liberation relied on DCS death tracking which required parked aircraft to be hit with more powerful weapons e.g. 2000lb bombs as they were uncontrolled.
|
||||||
|
* **[Campaign]** Track damage to theater ground objects across turns. Damage can accumulate across turns leading to death of the unit. This behavior only applies to SAMs, ships and other units that appear on the Liberation map. Frontline units and buildings are not tracked (yet).
|
||||||
|
* **[Mods]** F/A-18 E/F/G Super Hornet mod (v2.2.5) support added.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
* **[Mission Generation]** When planning anti-ship missions against carriers or LHAs, target escorts (if present) if the carrier/LHA is sunk.
|
||||||
|
* **[UI]** Identify that a carrier or LHA is sunk instead of "damaged".
|
||||||
|
|
||||||
|
# 10.0.0
|
||||||
|
|
||||||
|
Saves from 9.x are not compatible with 10.0.0.
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
|
||||||
|
* **[Engine]** Support for DCS 2.9.2.49629 Open Beta. (F-15E JDAM and JSOW, F-16 AIM-9P, updated Falklands and Normandy airfields).
|
||||||
|
* **[UI]** Improved the description of "runway" state for FARPs, FOBs, carriers, and off-map spawns.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
* **[Flight Planning]** Aircraft from even numbered flights will no longer become inaccessible when canceling a draft package.
|
||||||
|
* **[UI]** Flight members in the loadout menu are now numbered starting from 1 instead of 0.
|
||||||
|
* **[UI]** Flight plan paths are now drawn behind all other map elements, fixing rare cases where they could prevent other UI elements from being clickable.
|
||||||
|
|
||||||
# 9.0.0
|
# 9.0.0
|
||||||
|
|
||||||
Saves from 8.x are not compatible with 9.0.0.
|
Saves from 8.x are not compatible with 9.0.0.
|
||||||
@@ -46,7 +77,7 @@ Saves from 8.x are not compatible with 9.0.0.
|
|||||||
* **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint.
|
* **[UI]** Fixed deleting waypoints in custom flight plans deleting the wrong waypoint.
|
||||||
* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes.
|
* **[UI]** Fixed flight properties UI to support F-15E S4+ laser codes.
|
||||||
* **[UI]** In unit transfer dialog, only list control points that are reachable from the control point units are being transferred from.
|
* **[UI]** In unit transfer dialog, only list control points that are reachable from the control point units are being transferred from.
|
||||||
* **[UI]** Fixed UI bug where altering an "ahead of package" TOT offset would change the offset back to a "behind pacakge" offset.
|
* **[UI]** Fixed UI bug where altering an "ahead of package" TOT offset would change the offset back to a "behind package" offset.
|
||||||
* **[UI]** Fixed bug where changing TOT offsets could result in flight startup times that are in the past.
|
* **[UI]** Fixed bug where changing TOT offsets could result in flight startup times that are in the past.
|
||||||
* **[UI]** Fixed odd spacing of the finance window when there were not enough items to fill the page.
|
* **[UI]** Fixed odd spacing of the finance window when there were not enough items to fill the page.
|
||||||
* **[UI]** Fixed regression where waypoint altitude changes in the waypoint list screen are applied to the wrong waypoint.
|
* **[UI]** Fixed regression where waypoint altitude changes in the waypoint list screen are applied to the wrong waypoint.
|
||||||
|
|||||||
36
client/package-lock.json
generated
36
client/package-lock.json
generated
@@ -50,9 +50,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz",
|
||||||
"integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg=="
|
"integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw=="
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@@ -9883,9 +9883,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.2",
|
"version": "1.15.6",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -11161,9 +11161,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ip": {
|
"node_modules/ip": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz",
|
||||||
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
|
"integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
@@ -21339,9 +21339,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adobe/css-tools": {
|
"@adobe/css-tools": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.2.tgz",
|
||||||
"integrity": "sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg=="
|
"integrity": "sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw=="
|
||||||
},
|
},
|
||||||
"@ampproject/remapping": {
|
"@ampproject/remapping": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@@ -28743,9 +28743,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"follow-redirects": {
|
"follow-redirects": {
|
||||||
"version": "1.15.2",
|
"version": "1.15.6",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
|
||||||
},
|
},
|
||||||
"for-each": {
|
"for-each": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
@@ -29680,9 +29680,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ip": {
|
"ip": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz",
|
||||||
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=",
|
"integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ipaddr.js": {
|
"ipaddr.js": {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Flight } from "../../api/liberationApi";
|
import { Flight } from "../../api/liberationApi";
|
||||||
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
|
import { useGetCommitBoundaryForFlightQuery } from "../../api/liberationApi";
|
||||||
import WaypointMarker from "../waypointmarker";
|
import WaypointMarker from "../waypointmarker";
|
||||||
import { ReactElement } from "react";
|
import { Polyline as LPolyline } from "leaflet";
|
||||||
|
import { ReactElement, useEffect, useRef } from "react";
|
||||||
import { Polyline } from "react-leaflet";
|
import { Polyline } from "react-leaflet";
|
||||||
|
|
||||||
const BLUE_PATH = "#0084ff";
|
const BLUE_PATH = "#0084ff";
|
||||||
@@ -27,16 +28,41 @@ const pathColor = (props: FlightPlanProps) => {
|
|||||||
function FlightPlanPath(props: FlightPlanProps) {
|
function FlightPlanPath(props: FlightPlanProps) {
|
||||||
const color = pathColor(props);
|
const color = pathColor(props);
|
||||||
const waypoints = props.flight.waypoints;
|
const waypoints = props.flight.waypoints;
|
||||||
|
|
||||||
|
const polylineRef = useRef<LPolyline | null>(null);
|
||||||
|
|
||||||
|
// Flight paths should be drawn under everything else. There seems to be an
|
||||||
|
// issue where `interactive: false` doesn't do as its told (there's nuance,
|
||||||
|
// see the bug for details). It looks better if we draw the other elements on
|
||||||
|
// top of the flight plans anyway, so just push the flight plan to the back.
|
||||||
|
//
|
||||||
|
// https://github.com/dcs-liberation/dcs_liberation/issues/3295
|
||||||
|
//
|
||||||
|
// It's not possible to z-index a polyline (and leaflet says it never will be,
|
||||||
|
// because this is a limitation of SVG, not leaflet:
|
||||||
|
// https://github.com/Leaflet/Leaflet/issues/185), so we need to use
|
||||||
|
// bringToBack() to push the flight paths to the back of the drawing once
|
||||||
|
// they've been added to the map. They'll still draw on top of the map, but
|
||||||
|
// behind everything than was added before them. Anything added after always
|
||||||
|
// goes on top.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.selected) {
|
||||||
|
polylineRef.current?.bringToBack();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (waypoints == null) {
|
if (waypoints == null) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
const points = waypoints
|
const points = waypoints
|
||||||
.filter((waypoint) => waypoint.include_in_path)
|
.filter((waypoint) => waypoint.include_in_path)
|
||||||
.map((waypoint) => waypoint.position);
|
.map((waypoint) => waypoint.position);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Polyline
|
<Polyline
|
||||||
positions={points}
|
positions={points}
|
||||||
pathOptions={{ color: color, interactive: false }}
|
pathOptions={{ color: color, interactive: false }}
|
||||||
|
ref={polylineRef}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,8 +95,12 @@ describe("FlightPlansLayer", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
|
||||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
// For some reason passing ref to PolyLine causes it and its group to be
|
||||||
|
// redrawn, so these numbers don't match what you'd expect from the test.
|
||||||
|
// It probably needs to be rewritten without mocks.
|
||||||
|
expect(mockPolyline).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mockLayerGroup).toBeCalledTimes(2);
|
||||||
});
|
});
|
||||||
it("are not drawn if wrong coalition", () => {
|
it("are not drawn if wrong coalition", () => {
|
||||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
project = "DCS Liberation"
|
project = "DCS Liberation"
|
||||||
copyright = "2023, DCS Liberation Team"
|
copyright = "2023, DCS Liberation Team"
|
||||||
author = "DCS Liberation Team"
|
author = "DCS Liberation Team"
|
||||||
release = "9.0.0"
|
release = "11.0.0"
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
|
|||||||
raise PlanningError("Air assault is only usable by helicopters")
|
raise PlanningError("Air assault is only usable by helicopters")
|
||||||
assert self.package.waypoints is not None
|
assert self.package.waypoints is not None
|
||||||
|
|
||||||
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
|
altitude = self.doctrine.helicopter.air_assault_nav_altitude
|
||||||
altitude_is_agl = self.flight.is_helo
|
altitude_is_agl = self.flight.is_helo
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_duration(self) -> timedelta:
|
def patrol_duration(self) -> timedelta:
|
||||||
return self.flight.coalition.doctrine.cap_duration
|
return self.flight.coalition.doctrine.cap.duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
@@ -29,7 +29,7 @@ class BarCapFlightPlan(PatrollingFlightPlan[PatrollingLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def engagement_distance(self) -> Distance:
|
def engagement_distance(self) -> Distance:
|
||||||
return self.flight.coalition.doctrine.cap_engagement_range
|
return self.flight.coalition.doctrine.cap.engagement_range
|
||||||
|
|
||||||
|
|
||||||
class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
||||||
@@ -44,8 +44,8 @@ class Builder(CapBuilder[BarCapFlightPlan, PatrollingLayout]):
|
|||||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||||
patrol_alt = max(
|
patrol_alt = max(
|
||||||
self.doctrine.min_patrol_altitude,
|
self.doctrine.cap.min_patrol_altitude,
|
||||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
min(self.doctrine.cap.max_patrol_altitude, randomized_alt),
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
# buffer.
|
# buffer.
|
||||||
distance_to_no_fly = (
|
distance_to_no_fly = (
|
||||||
meters(position.distance(self.threat_zones.all))
|
meters(position.distance(self.threat_zones.all))
|
||||||
- self.doctrine.cap_engagement_range
|
- self.doctrine.cap.engagement_range
|
||||||
- nautical_miles(5)
|
- nautical_miles(5)
|
||||||
)
|
)
|
||||||
max_track_length = self.doctrine.cap_max_track_length
|
max_track_length = self.doctrine.cap.max_track_length
|
||||||
else:
|
else:
|
||||||
# Other race tracks (TARCAPs, currently) just try to keep some
|
# Other race tracks (TARCAPs, currently) just try to keep some
|
||||||
# distance from the nearest enemy airbase, but since they are by
|
# distance from the nearest enemy airbase, but since they are by
|
||||||
@@ -108,15 +108,15 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
distance_to_no_fly = distance_to_airfield - min_distance_from_enemy
|
||||||
|
|
||||||
# TARCAPs fly short racetracks because they need to react faster.
|
# TARCAPs fly short racetracks because they need to react faster.
|
||||||
max_track_length = self.doctrine.cap_min_track_length + 0.3 * (
|
max_track_length = self.doctrine.cap.min_track_length + 0.3 * (
|
||||||
self.doctrine.cap_max_track_length - self.doctrine.cap_min_track_length
|
self.doctrine.cap.max_track_length - self.doctrine.cap.min_track_length
|
||||||
)
|
)
|
||||||
|
|
||||||
min_cap_distance = min(
|
min_cap_distance = min(
|
||||||
self.doctrine.cap_min_distance_from_cp, distance_to_no_fly
|
self.doctrine.cap.min_distance_from_cp, distance_to_no_fly
|
||||||
)
|
)
|
||||||
max_cap_distance = min(
|
max_cap_distance = min(
|
||||||
self.doctrine.cap_max_distance_from_cp, distance_to_no_fly
|
self.doctrine.cap.max_distance_from_cp, distance_to_no_fly
|
||||||
)
|
)
|
||||||
|
|
||||||
end = location.position.point_from_heading(
|
end = location.position.point_from_heading(
|
||||||
@@ -125,7 +125,7 @@ class CapBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
track_length = random.randint(
|
track_length = random.randint(
|
||||||
int(self.doctrine.cap_min_track_length.meters),
|
int(self.doctrine.cap.min_track_length.meters),
|
||||||
int(max_track_length.meters),
|
int(max_track_length.meters),
|
||||||
)
|
)
|
||||||
start = end.point_from_heading(heading.opposite.degrees, track_length)
|
start = end.point_from_heading(heading.opposite.degrees, track_length)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_duration(self) -> timedelta:
|
def patrol_duration(self) -> timedelta:
|
||||||
return self.flight.coalition.doctrine.cas_duration
|
return self.flight.coalition.doctrine.cas.duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
@@ -96,7 +96,7 @@ class Builder(IBuilder[CasFlightPlan, CasLayout]):
|
|||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
|
||||||
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
|
is_helo = self.flight.unit_type.dcs_unit_type.helicopter
|
||||||
patrol_altitude = self.doctrine.ingress_altitude if not is_helo else meters(50)
|
patrol_altitude = self.doctrine.resolve_combat_altitude(is_helo)
|
||||||
use_agl_patrol_altitude = is_helo
|
use_agl_patrol_altitude = is_helo
|
||||||
|
|
||||||
ip_solver = IpSolver(
|
ip_solver = IpSolver(
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
|||||||
departure=builder.takeoff(self.flight.departure),
|
departure=builder.takeoff(self.flight.departure),
|
||||||
hold=hold,
|
hold=hold,
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
hold.position, join.position, self.doctrine.ingress_altitude
|
hold.position, join.position, self.doctrine.combat_altitude
|
||||||
),
|
),
|
||||||
join=join,
|
join=join,
|
||||||
ingress=ingress,
|
ingress=ingress,
|
||||||
@@ -43,7 +43,7 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
|
|||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
refuel.position,
|
refuel.position,
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
self.doctrine.ingress_altitude,
|
self.doctrine.combat_altitude,
|
||||||
),
|
),
|
||||||
arrival=builder.land(self.flight.arrival),
|
arrival=builder.land(self.flight.arrival),
|
||||||
divert=builder.divert(self.flight.divert),
|
divert=builder.divert(self.flight.divert),
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
departure=builder.takeoff(self.flight.departure),
|
departure=builder.takeoff(self.flight.departure),
|
||||||
hold=hold,
|
hold=hold,
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
hold.position, join.position, self.doctrine.ingress_altitude
|
hold.position, join.position, self.doctrine.combat_altitude
|
||||||
),
|
),
|
||||||
join=join,
|
join=join,
|
||||||
ingress=ingress,
|
ingress=ingress,
|
||||||
@@ -173,7 +173,7 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
|
|||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
refuel.position,
|
refuel.position,
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
self.doctrine.ingress_altitude,
|
self.doctrine.combat_altitude,
|
||||||
),
|
),
|
||||||
arrival=builder.land(self.flight.arrival),
|
arrival=builder.land(self.flight.arrival),
|
||||||
divert=builder.divert(self.flight.divert),
|
divert=builder.divert(self.flight.divert),
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
|
|||||||
|
|
||||||
class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]):
|
class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]):
|
||||||
def layout(self) -> RecoveryTankerLayout:
|
def layout(self) -> RecoveryTankerLayout:
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
|
||||||
# TODO: Propagate the ship position to the Tanker's TOT,
|
# TODO: Propagate the ship position to the Tanker's TOT,
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
|||||||
self.package.waypoints.join.heading_between_point(target)
|
self.package.waypoints.join.heading_between_point(target)
|
||||||
)
|
)
|
||||||
start_pos = target.point_from_heading(
|
start_pos = target.point_from_heading(
|
||||||
heading.degrees, -self.doctrine.sweep_distance.meters
|
heading.degrees, -self.doctrine.sweep.distance.meters
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
start, end = builder.sweep(start_pos, target, self.doctrine.combat_altitude)
|
||||||
|
|
||||||
hold = builder.hold(self._hold_point())
|
hold = builder.hold(self._hold_point())
|
||||||
|
|
||||||
@@ -126,12 +126,12 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
|
|||||||
departure=builder.takeoff(self.flight.departure),
|
departure=builder.takeoff(self.flight.departure),
|
||||||
hold=hold,
|
hold=hold,
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
hold.position, start.position, self.doctrine.ingress_altitude
|
hold.position, start.position, self.doctrine.combat_altitude
|
||||||
),
|
),
|
||||||
nav_from=builder.nav_path(
|
nav_from=builder.nav_path(
|
||||||
end.position,
|
end.position,
|
||||||
self.flight.arrival.position,
|
self.flight.arrival.position,
|
||||||
self.doctrine.ingress_altitude,
|
self.doctrine.combat_altitude,
|
||||||
),
|
),
|
||||||
sweep_start=start,
|
sweep_start=start,
|
||||||
sweep_end=end,
|
sweep_end=end,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
|||||||
# flights in the package that have requested escort. If the package
|
# flights in the package that have requested escort. If the package
|
||||||
# requests an escort the CAP self.flight will remain on station for the
|
# requests an escort the CAP self.flight will remain on station for the
|
||||||
# duration of the escorted mission, or until it is winchester/bingo.
|
# duration of the escorted mission, or until it is winchester/bingo.
|
||||||
return self.flight.coalition.doctrine.cap_duration
|
return self.flight.coalition.doctrine.cap.duration
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_speed(self) -> Speed:
|
def patrol_speed(self) -> Speed:
|
||||||
@@ -50,7 +50,7 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def engagement_distance(self) -> Distance:
|
def engagement_distance(self) -> Distance:
|
||||||
return self.flight.coalition.doctrine.cap_engagement_range
|
return self.flight.coalition.doctrine.cap.engagement_range
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def builder_type() -> Type[Builder]:
|
def builder_type() -> Type[Builder]:
|
||||||
@@ -90,8 +90,8 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
|
|||||||
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
preferred_alt = self.flight.unit_type.preferred_patrol_altitude
|
||||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||||
patrol_alt = max(
|
patrol_alt = max(
|
||||||
self.doctrine.min_patrol_altitude,
|
self.doctrine.cap.min_patrol_altitude,
|
||||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
min(self.doctrine.cap.max_patrol_altitude, randomized_alt),
|
||||||
)
|
)
|
||||||
|
|
||||||
builder = WaypointBuilder(self.flight, self.coalition)
|
builder = WaypointBuilder(self.flight, self.coalition)
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class WaypointBuilder:
|
|||||||
"NAV",
|
"NAV",
|
||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position,
|
position,
|
||||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
|
||||||
description="Enter theater",
|
description="Enter theater",
|
||||||
pretty_name="Enter theater",
|
pretty_name="Enter theater",
|
||||||
)
|
)
|
||||||
@@ -99,7 +99,7 @@ class WaypointBuilder:
|
|||||||
"NAV",
|
"NAV",
|
||||||
FlightWaypointType.NAV,
|
FlightWaypointType.NAV,
|
||||||
position,
|
position,
|
||||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
|
||||||
description="Exit theater",
|
description="Exit theater",
|
||||||
pretty_name="Exit theater",
|
pretty_name="Exit theater",
|
||||||
)
|
)
|
||||||
@@ -127,10 +127,7 @@ class WaypointBuilder:
|
|||||||
position = divert.position
|
position = divert.position
|
||||||
altitude_type: AltitudeReference
|
altitude_type: AltitudeReference
|
||||||
if isinstance(divert, OffMapSpawn):
|
if isinstance(divert, OffMapSpawn):
|
||||||
if self.is_helo:
|
altitude = self.doctrine.resolve_rendezvous_altitude(self.is_helo)
|
||||||
altitude = meters(500)
|
|
||||||
else:
|
|
||||||
altitude = self.doctrine.rendezvous_altitude
|
|
||||||
altitude_type = "BARO"
|
altitude_type = "BARO"
|
||||||
else:
|
else:
|
||||||
altitude = meters(0)
|
altitude = meters(0)
|
||||||
@@ -168,10 +165,7 @@ class WaypointBuilder:
|
|||||||
"HOLD",
|
"HOLD",
|
||||||
FlightWaypointType.LOITER,
|
FlightWaypointType.LOITER,
|
||||||
position,
|
position,
|
||||||
# Bug: DCS only accepts MSL altitudes for the orbit task and 500 meters is
|
self.doctrine.resolve_rendezvous_altitude(self.is_helo),
|
||||||
# below the ground for most if not all of NTTR (and lots of places in other
|
|
||||||
# maps).
|
|
||||||
meters(500) if self.is_helo else self.doctrine.rendezvous_altitude,
|
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Wait until push time",
|
description="Wait until push time",
|
||||||
pretty_name="Hold",
|
pretty_name="Hold",
|
||||||
@@ -186,7 +180,7 @@ class WaypointBuilder:
|
|||||||
"JOIN",
|
"JOIN",
|
||||||
FlightWaypointType.JOIN,
|
FlightWaypointType.JOIN,
|
||||||
position,
|
position,
|
||||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
self.doctrine.resolve_combat_altitude(self.is_helo),
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Rendezvous with package",
|
description="Rendezvous with package",
|
||||||
pretty_name="Join",
|
pretty_name="Join",
|
||||||
@@ -201,7 +195,7 @@ class WaypointBuilder:
|
|||||||
"REFUEL",
|
"REFUEL",
|
||||||
FlightWaypointType.REFUEL,
|
FlightWaypointType.REFUEL,
|
||||||
position,
|
position,
|
||||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
self.doctrine.resolve_combat_altitude(self.is_helo),
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Refuel from tanker",
|
description="Refuel from tanker",
|
||||||
pretty_name="Refuel",
|
pretty_name="Refuel",
|
||||||
@@ -229,7 +223,7 @@ class WaypointBuilder:
|
|||||||
"SPLIT",
|
"SPLIT",
|
||||||
FlightWaypointType.SPLIT,
|
FlightWaypointType.SPLIT,
|
||||||
position,
|
position,
|
||||||
meters(80) if self.is_helo else self.doctrine.ingress_altitude,
|
self.doctrine.resolve_combat_altitude(self.is_helo),
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Depart from package",
|
description="Depart from package",
|
||||||
pretty_name="Split",
|
pretty_name="Split",
|
||||||
@@ -249,7 +243,7 @@ class WaypointBuilder:
|
|||||||
"INGRESS",
|
"INGRESS",
|
||||||
ingress_type,
|
ingress_type,
|
||||||
position,
|
position,
|
||||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
self.doctrine.resolve_combat_altitude(self.is_helo),
|
||||||
alt_type,
|
alt_type,
|
||||||
description=f"INGRESS on {objective.name}",
|
description=f"INGRESS on {objective.name}",
|
||||||
pretty_name=f"INGRESS on {objective.name}",
|
pretty_name=f"INGRESS on {objective.name}",
|
||||||
@@ -294,7 +288,7 @@ class WaypointBuilder:
|
|||||||
f"SEAD on {target.name}",
|
f"SEAD on {target.name}",
|
||||||
target,
|
target,
|
||||||
flyover=True,
|
flyover=True,
|
||||||
altitude=self.doctrine.ingress_altitude,
|
altitude=self.doctrine.combat_altitude,
|
||||||
alt_type="BARO",
|
alt_type="BARO",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -484,7 +478,7 @@ class WaypointBuilder:
|
|||||||
"TARGET",
|
"TARGET",
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
target.position,
|
target.position,
|
||||||
meters(60) if self.is_helo else self.doctrine.ingress_altitude,
|
self.doctrine.resolve_combat_altitude(self.is_helo),
|
||||||
alt_type,
|
alt_type,
|
||||||
description="Escort the package",
|
description="Escort the package",
|
||||||
pretty_name="Target area",
|
pretty_name="Target area",
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class GroundSpeed:
|
|||||||
# on fuel, but mission speed will be fast enough to keep the flight
|
# on fuel, but mission speed will be fast enough to keep the flight
|
||||||
# safer.
|
# safer.
|
||||||
|
|
||||||
|
if flight.squadron.aircraft.cruise_speed is not None:
|
||||||
|
return mach(flight.squadron.aircraft.cruise_speed.mach(), altitude)
|
||||||
|
|
||||||
# DCS's max speed is in kph at 0 MSL.
|
# DCS's max speed is in kph at 0 MSL.
|
||||||
max_speed = flight.unit_type.max_speed
|
max_speed = flight.unit_type.max_speed
|
||||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class DefaultSquadronAssigner:
|
|||||||
self.coalition.player
|
self.coalition.player
|
||||||
):
|
):
|
||||||
for squadron_config in self.config.by_location[control_point]:
|
for squadron_config in self.config.by_location[control_point]:
|
||||||
|
|
||||||
squadron_def = self.override_squadron_defaults(
|
squadron_def = self.override_squadron_defaults(
|
||||||
self.find_squadron_for(squadron_config, control_point),
|
self.find_squadron_for(squadron_config, control_point),
|
||||||
squadron_config,
|
squadron_config,
|
||||||
@@ -162,7 +161,6 @@ class DefaultSquadronAssigner:
|
|||||||
def override_squadron_defaults(
|
def override_squadron_defaults(
|
||||||
squadron_def: Optional[SquadronDef], config: SquadronConfig
|
squadron_def: Optional[SquadronDef], config: SquadronConfig
|
||||||
) -> Optional[SquadronDef]:
|
) -> Optional[SquadronDef]:
|
||||||
|
|
||||||
if squadron_def is None:
|
if squadron_def is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from game.utils import meters, nautical_miles
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.transfers import CargoShip, Convoy
|
from game.transfers import CargoShip, Convoy
|
||||||
|
from game.threatzones import ThreatZones
|
||||||
|
|
||||||
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
||||||
|
|
||||||
@@ -193,17 +194,36 @@ class ObjectiveFinder:
|
|||||||
|
|
||||||
def farthest_friendly_control_point(self) -> ControlPoint:
|
def farthest_friendly_control_point(self) -> ControlPoint:
|
||||||
"""Finds the friendly control point that is farthest from any threats."""
|
"""Finds the friendly control point that is farthest from any threats."""
|
||||||
|
|
||||||
|
def find_farthest(
|
||||||
|
control_points: Iterator[ControlPoint],
|
||||||
|
threat_zones: ThreatZones,
|
||||||
|
consider_off_map_spawn: bool,
|
||||||
|
) -> ControlPoint | None:
|
||||||
|
farthest = None
|
||||||
|
max_distance = meters(0)
|
||||||
|
for cp in control_points:
|
||||||
|
if isinstance(cp, OffMapSpawn) and not consider_off_map_spawn:
|
||||||
|
continue
|
||||||
|
distance = threat_zones.distance_to_threat(cp.position)
|
||||||
|
if distance > max_distance:
|
||||||
|
farthest = cp
|
||||||
|
max_distance = distance
|
||||||
|
return farthest
|
||||||
|
|
||||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
farthest = None
|
farthest = find_farthest(
|
||||||
max_distance = meters(0)
|
self.friendly_control_points(), threat_zones, consider_off_map_spawn=False
|
||||||
for cp in self.friendly_control_points():
|
)
|
||||||
if isinstance(cp, OffMapSpawn):
|
|
||||||
continue
|
# If there are only off-map spawn control points, fall back to the farthest amongst off map spawn points
|
||||||
distance = threat_zones.distance_to_threat(cp.position)
|
if farthest is None:
|
||||||
if distance > max_distance:
|
farthest = find_farthest(
|
||||||
farthest = cp
|
self.friendly_control_points(),
|
||||||
max_distance = distance
|
threat_zones,
|
||||||
|
consider_off_map_spawn=True,
|
||||||
|
)
|
||||||
|
|
||||||
if farthest is None:
|
if farthest is None:
|
||||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class TheaterState(WorldState["TheaterState"]):
|
|||||||
# Plan enough rounds of CAP that the target has coverage over the expected
|
# Plan enough rounds of CAP that the target has coverage over the expected
|
||||||
# mission duration.
|
# mission duration.
|
||||||
mission_duration = game.settings.desired_player_mission_duration.total_seconds()
|
mission_duration = game.settings.desired_player_mission_duration.total_seconds()
|
||||||
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
|
barcap_duration = coalition.doctrine.cap.duration.total_seconds()
|
||||||
barcap_rounds = math.ceil(mission_duration / barcap_duration)
|
barcap_rounds = math.ceil(mission_duration / barcap_duration)
|
||||||
|
|
||||||
refueling_targets: list[MissionTarget] = []
|
refueling_targets: list[MissionTarget] = []
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
from typing import Any, ClassVar
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
@@ -15,19 +21,105 @@ class GroundUnitProcurementRatios:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data: dict[str, float]) -> GroundUnitProcurementRatios:
|
||||||
|
unit_class_enum_from_name = {unit.value: unit for unit in UnitClass}
|
||||||
|
r = {}
|
||||||
|
for unit_class in data:
|
||||||
|
if unit_class not in unit_class_enum_from_name:
|
||||||
|
raise ValueError(f"Could not find unit type {unit_class}")
|
||||||
|
r[unit_class_enum_from_name[unit_class]] = float(data[unit_class])
|
||||||
|
return GroundUnitProcurementRatios(r)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Helicopter:
|
||||||
|
#: The altitude used for combat section of a flight, overrides the base combat_altitude parameter for helos
|
||||||
|
combat_altitude: Distance
|
||||||
|
|
||||||
|
#: The altitude used for forming up a pacakge. Overrides the base rendezvous_altitude parameter for helos
|
||||||
|
rendezvous_altitude: Distance
|
||||||
|
|
||||||
|
#: Altitude of the nav points (cruise section) of air assault missions.
|
||||||
|
air_assault_nav_altitude: Distance
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data: dict[str, Any]) -> Helicopter:
|
||||||
|
return Helicopter(
|
||||||
|
combat_altitude=feet(data["combat_altitude_ft_agl"]),
|
||||||
|
rendezvous_altitude=feet(data["rendezvous_altitude_ft_agl"]),
|
||||||
|
air_assault_nav_altitude=feet(data["air_assault_nav_altitude_ft_agl"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cas:
|
||||||
|
#: The duration that CAP flights will remain on-station.
|
||||||
|
duration: timedelta
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data: dict[str, Any]) -> Cas:
|
||||||
|
return Cas(duration=timedelta(minutes=data["duration_minutes"]))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Sweep:
|
||||||
|
#: Length of the sweep / patrol leg
|
||||||
|
distance: Distance
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data: dict[str, Any]) -> Sweep:
|
||||||
|
return Sweep(
|
||||||
|
distance=nautical_miles(data["distance_nm"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cap:
|
||||||
|
#: The duration that CAP flights will remain on-station.
|
||||||
|
duration: timedelta
|
||||||
|
|
||||||
|
#: The minimum length of the CAP race track.
|
||||||
|
min_track_length: Distance
|
||||||
|
|
||||||
|
#: The maximum length of the CAP race track.
|
||||||
|
max_track_length: Distance
|
||||||
|
|
||||||
|
#: The minimum distance between the defended position and the *end* of the
|
||||||
|
#: CAP race track.
|
||||||
|
min_distance_from_cp: Distance
|
||||||
|
|
||||||
|
#: The maximum distance between the defended position and the *end* of the
|
||||||
|
#: CAP race track.
|
||||||
|
max_distance_from_cp: Distance
|
||||||
|
|
||||||
|
#: The engagement range of CAP flights. Any enemy aircraft within this range
|
||||||
|
#: of the CAP's current position will be engaged by the CAP.
|
||||||
|
engagement_range: Distance
|
||||||
|
|
||||||
|
#: Defines the range of altitudes CAP racetracks are planned at.
|
||||||
|
min_patrol_altitude: Distance
|
||||||
|
max_patrol_altitude: Distance
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(data: dict[str, Any]) -> Cap:
|
||||||
|
return Cap(
|
||||||
|
duration=timedelta(minutes=data["duration_minutes"]),
|
||||||
|
min_track_length=nautical_miles(data["min_track_length_nm"]),
|
||||||
|
max_track_length=nautical_miles(data["max_track_length_nm"]),
|
||||||
|
min_distance_from_cp=nautical_miles(data["min_distance_from_cp_nm"]),
|
||||||
|
max_distance_from_cp=nautical_miles(data["max_distance_from_cp_nm"]),
|
||||||
|
engagement_range=nautical_miles(data["engagement_range_nm"]),
|
||||||
|
min_patrol_altitude=feet(data["min_patrol_altitude_ft_msl"]),
|
||||||
|
max_patrol_altitude=feet(data["max_patrol_altitude_ft_msl"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Doctrine:
|
class Doctrine:
|
||||||
|
#: Name of the doctrine, used to assign a doctrine in a faction.
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
cas: bool
|
|
||||||
cap: bool
|
|
||||||
sead: bool
|
|
||||||
strike: bool
|
|
||||||
antiship: bool
|
|
||||||
|
|
||||||
rendezvous_altitude: Distance
|
|
||||||
|
|
||||||
#: The minimum distance between the departure airfield and the hold point.
|
#: The minimum distance between the departure airfield and the hold point.
|
||||||
hold_distance: Distance
|
hold_distance: Distance
|
||||||
|
|
||||||
@@ -46,155 +138,87 @@ class Doctrine:
|
|||||||
#: target.
|
#: target.
|
||||||
min_ingress_distance: Distance
|
min_ingress_distance: Distance
|
||||||
|
|
||||||
ingress_altitude: Distance
|
#: The altitude used for combat section of a flight.
|
||||||
|
combat_altitude: Distance
|
||||||
|
|
||||||
min_patrol_altitude: Distance
|
#: The altitude used for forming up a pacakge.
|
||||||
max_patrol_altitude: Distance
|
rendezvous_altitude: Distance
|
||||||
pattern_altitude: Distance
|
|
||||||
|
|
||||||
#: The duration that CAP flights will remain on-station.
|
|
||||||
cap_duration: timedelta
|
|
||||||
|
|
||||||
#: The minimum length of the CAP race track.
|
|
||||||
cap_min_track_length: Distance
|
|
||||||
|
|
||||||
#: The maximum length of the CAP race track.
|
|
||||||
cap_max_track_length: Distance
|
|
||||||
|
|
||||||
#: The minimum distance between the defended position and the *end* of the
|
|
||||||
#: CAP race track.
|
|
||||||
cap_min_distance_from_cp: Distance
|
|
||||||
|
|
||||||
#: The maximum distance between the defended position and the *end* of the
|
|
||||||
#: CAP race track.
|
|
||||||
cap_max_distance_from_cp: Distance
|
|
||||||
|
|
||||||
#: The engagement range of CAP flights. Any enemy aircraft within this range
|
|
||||||
#: of the CAP's current position will be engaged by the CAP.
|
|
||||||
cap_engagement_range: Distance
|
|
||||||
|
|
||||||
cas_duration: timedelta
|
|
||||||
|
|
||||||
sweep_distance: Distance
|
|
||||||
|
|
||||||
|
#: Defines prioritization of ground unit purchases.
|
||||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||||
|
|
||||||
|
#: Helicopter specific doctrines.
|
||||||
|
helicopter: Helicopter
|
||||||
|
|
||||||
MODERN_DOCTRINE = Doctrine(
|
#: Doctrine for CAS missions.
|
||||||
"modern",
|
cas: Cas
|
||||||
cap=True,
|
|
||||||
cas=True,
|
|
||||||
sead=True,
|
|
||||||
strike=True,
|
|
||||||
antiship=True,
|
|
||||||
rendezvous_altitude=feet(25000),
|
|
||||||
hold_distance=nautical_miles(25),
|
|
||||||
push_distance=nautical_miles(20),
|
|
||||||
join_distance=nautical_miles(20),
|
|
||||||
max_ingress_distance=nautical_miles(45),
|
|
||||||
min_ingress_distance=nautical_miles(10),
|
|
||||||
ingress_altitude=feet(20000),
|
|
||||||
min_patrol_altitude=feet(15000),
|
|
||||||
max_patrol_altitude=feet(33000),
|
|
||||||
pattern_altitude=feet(5000),
|
|
||||||
cap_duration=timedelta(minutes=30),
|
|
||||||
cap_min_track_length=nautical_miles(15),
|
|
||||||
cap_max_track_length=nautical_miles(40),
|
|
||||||
cap_min_distance_from_cp=nautical_miles(10),
|
|
||||||
cap_max_distance_from_cp=nautical_miles(40),
|
|
||||||
cap_engagement_range=nautical_miles(50),
|
|
||||||
cas_duration=timedelta(minutes=30),
|
|
||||||
sweep_distance=nautical_miles(60),
|
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
|
||||||
{
|
|
||||||
UnitClass.TANK: 3,
|
|
||||||
UnitClass.ATGM: 2,
|
|
||||||
UnitClass.APC: 2,
|
|
||||||
UnitClass.IFV: 3,
|
|
||||||
UnitClass.ARTILLERY: 1,
|
|
||||||
UnitClass.SHORAD: 2,
|
|
||||||
UnitClass.RECON: 1,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
COLDWAR_DOCTRINE = Doctrine(
|
#: Doctrine for CAP missions.
|
||||||
name="coldwar",
|
cap: Cap
|
||||||
cap=True,
|
|
||||||
cas=True,
|
|
||||||
sead=True,
|
|
||||||
strike=True,
|
|
||||||
antiship=True,
|
|
||||||
rendezvous_altitude=feet(22000),
|
|
||||||
hold_distance=nautical_miles(15),
|
|
||||||
push_distance=nautical_miles(10),
|
|
||||||
join_distance=nautical_miles(10),
|
|
||||||
max_ingress_distance=nautical_miles(30),
|
|
||||||
min_ingress_distance=nautical_miles(10),
|
|
||||||
ingress_altitude=feet(18000),
|
|
||||||
min_patrol_altitude=feet(10000),
|
|
||||||
max_patrol_altitude=feet(24000),
|
|
||||||
pattern_altitude=feet(5000),
|
|
||||||
cap_duration=timedelta(minutes=30),
|
|
||||||
cap_min_track_length=nautical_miles(12),
|
|
||||||
cap_max_track_length=nautical_miles(24),
|
|
||||||
cap_min_distance_from_cp=nautical_miles(8),
|
|
||||||
cap_max_distance_from_cp=nautical_miles(25),
|
|
||||||
cap_engagement_range=nautical_miles(35),
|
|
||||||
cas_duration=timedelta(minutes=30),
|
|
||||||
sweep_distance=nautical_miles(40),
|
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
|
||||||
{
|
|
||||||
UnitClass.TANK: 4,
|
|
||||||
UnitClass.ATGM: 2,
|
|
||||||
UnitClass.APC: 3,
|
|
||||||
UnitClass.IFV: 2,
|
|
||||||
UnitClass.ARTILLERY: 1,
|
|
||||||
UnitClass.SHORAD: 2,
|
|
||||||
UnitClass.RECON: 1,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
WWII_DOCTRINE = Doctrine(
|
#: Doctrine for Fighter Sweep missions.
|
||||||
name="ww2",
|
sweep: Sweep
|
||||||
cap=True,
|
|
||||||
cas=True,
|
|
||||||
sead=False,
|
|
||||||
strike=True,
|
|
||||||
antiship=True,
|
|
||||||
hold_distance=nautical_miles(10),
|
|
||||||
push_distance=nautical_miles(5),
|
|
||||||
join_distance=nautical_miles(5),
|
|
||||||
rendezvous_altitude=feet(10000),
|
|
||||||
max_ingress_distance=nautical_miles(7),
|
|
||||||
min_ingress_distance=nautical_miles(5),
|
|
||||||
ingress_altitude=feet(8000),
|
|
||||||
min_patrol_altitude=feet(4000),
|
|
||||||
max_patrol_altitude=feet(15000),
|
|
||||||
pattern_altitude=feet(5000),
|
|
||||||
cap_duration=timedelta(minutes=30),
|
|
||||||
cap_min_track_length=nautical_miles(8),
|
|
||||||
cap_max_track_length=nautical_miles(18),
|
|
||||||
cap_min_distance_from_cp=nautical_miles(0),
|
|
||||||
cap_max_distance_from_cp=nautical_miles(5),
|
|
||||||
cap_engagement_range=nautical_miles(20),
|
|
||||||
cas_duration=timedelta(minutes=30),
|
|
||||||
sweep_distance=nautical_miles(10),
|
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
|
||||||
{
|
|
||||||
UnitClass.TANK: 3,
|
|
||||||
UnitClass.ATGM: 3,
|
|
||||||
UnitClass.APC: 3,
|
|
||||||
UnitClass.ARTILLERY: 1,
|
|
||||||
UnitClass.SHORAD: 3,
|
|
||||||
UnitClass.RECON: 1,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
ALL_DOCTRINES = [
|
_by_name: ClassVar[dict[str, Doctrine]] = {}
|
||||||
COLDWAR_DOCTRINE,
|
_loaded: ClassVar[bool] = False
|
||||||
MODERN_DOCTRINE,
|
|
||||||
WWII_DOCTRINE,
|
def resolve_combat_altitude(self, is_helo: bool = False) -> Distance:
|
||||||
]
|
if is_helo:
|
||||||
|
return self.helicopter.combat_altitude
|
||||||
|
return self.combat_altitude
|
||||||
|
|
||||||
|
def resolve_rendezvous_altitude(self, is_helo: bool = False) -> Distance:
|
||||||
|
if is_helo:
|
||||||
|
return self.helicopter.rendezvous_altitude
|
||||||
|
return self.rendezvous_altitude
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, doctrine: Doctrine) -> None:
|
||||||
|
if doctrine.name in cls._by_name:
|
||||||
|
duplicate = cls._by_name[doctrine.name]
|
||||||
|
raise ValueError(f"Doctrine {doctrine.name} is already loaded")
|
||||||
|
cls._by_name[doctrine.name] = doctrine
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def named(cls, name: str) -> Doctrine:
|
||||||
|
if not cls._loaded:
|
||||||
|
cls.load_all()
|
||||||
|
return cls._by_name[name]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all_doctrines(cls) -> list[Doctrine]:
|
||||||
|
if not cls._loaded:
|
||||||
|
cls.load_all()
|
||||||
|
return list(cls._by_name.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_all(cls) -> None:
|
||||||
|
if cls._loaded:
|
||||||
|
return
|
||||||
|
for doctrine_file_path in Path("resources/doctrines").glob("**/*.yaml"):
|
||||||
|
with doctrine_file_path.open(encoding="utf8") as doctrine_file:
|
||||||
|
data = yaml.safe_load(doctrine_file)
|
||||||
|
cls.register(
|
||||||
|
Doctrine(
|
||||||
|
name=data["name"],
|
||||||
|
rendezvous_altitude=feet(data["rendezvous_altitude_ft_msl"]),
|
||||||
|
hold_distance=nautical_miles(data["hold_distance_nm"]),
|
||||||
|
push_distance=nautical_miles(data["push_distance_nm"]),
|
||||||
|
join_distance=nautical_miles(data["join_distance_nm"]),
|
||||||
|
max_ingress_distance=nautical_miles(
|
||||||
|
data["max_ingress_distance_nm"]
|
||||||
|
),
|
||||||
|
min_ingress_distance=nautical_miles(
|
||||||
|
data["min_ingress_distance_nm"]
|
||||||
|
),
|
||||||
|
combat_altitude=feet(data["combat_altitude_ft_msl"]),
|
||||||
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios.from_dict(
|
||||||
|
data["ground_unit_procurement_ratios"]
|
||||||
|
),
|
||||||
|
helicopter=Helicopter.from_dict(data["helicopter"]),
|
||||||
|
cas=Cas.from_dict(data["cas"]),
|
||||||
|
cap=Cap.from_dict(data["cap"]),
|
||||||
|
sweep=Sweep.from_dict(data["sweep"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cls._loaded = True
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace as dataclasses_replace
|
||||||
from functools import cache, cached_property
|
from functools import cache, cached_property
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type
|
from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type
|
||||||
@@ -182,6 +182,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
#: planner will consider this aircraft usable for a mission.
|
#: planner will consider this aircraft usable for a mission.
|
||||||
max_mission_range: Distance
|
max_mission_range: Distance
|
||||||
|
|
||||||
|
#: Speed used for TOT calculations
|
||||||
|
cruise_speed: Optional[Speed]
|
||||||
|
|
||||||
fuel_consumption: Optional[FuelConsumption]
|
fuel_consumption: Optional[FuelConsumption]
|
||||||
|
|
||||||
default_livery: Optional[str]
|
default_livery: Optional[str]
|
||||||
@@ -400,6 +403,12 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
for k in config:
|
for k in config:
|
||||||
if k in aircraft.property_defaults:
|
if k in aircraft.property_defaults:
|
||||||
aircraft.property_defaults[k] = config[k]
|
aircraft.property_defaults[k] = config[k]
|
||||||
|
# In addition to setting the property_defaults, we have to set the "default" property in the
|
||||||
|
# value of aircraft.properties for the key, as this is used in parts of the codebase to get
|
||||||
|
# the default value.
|
||||||
|
aircraft.properties[k] = dataclasses_replace(
|
||||||
|
aircraft.properties[k], default=config[k]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"'{aircraft.id}' attempted to set default prop '{k}' that does not exist"
|
f"'{aircraft.id}' attempted to set default prop '{k}' that does not exist"
|
||||||
@@ -489,6 +498,9 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
patrol_altitude=patrol_config.altitude,
|
patrol_altitude=patrol_config.altitude,
|
||||||
patrol_speed=patrol_config.speed,
|
patrol_speed=patrol_config.speed,
|
||||||
max_mission_range=mission_range,
|
max_mission_range=mission_range,
|
||||||
|
cruise_speed=knots(data["cruise_speed_kt_indicated"])
|
||||||
|
if "cruise_speed_kt_indicated" in data
|
||||||
|
else None,
|
||||||
fuel_consumption=fuel_consumption,
|
fuel_consumption=fuel_consumption,
|
||||||
default_livery=data.get("default_livery"),
|
default_livery=data.get("default_livery"),
|
||||||
intra_flight_radio=radio_config.intra_flight,
|
intra_flight_radio=radio_config.intra_flight,
|
||||||
@@ -505,6 +517,7 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", [])
|
LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", [])
|
||||||
],
|
],
|
||||||
use_f15e_waypoint_names=data.get("use_f15e_waypoint_names", False),
|
use_f15e_waypoint_names=data.get("use_f15e_waypoint_names", False),
|
||||||
|
hit_points=data.get("hit_points", 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
|
|||||||
@@ -133,4 +133,5 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
|
|||||||
data.get("skynet_properties", {})
|
data.get("skynet_properties", {})
|
||||||
),
|
),
|
||||||
reversed_heading=data.get("reversed_heading", False),
|
reversed_heading=data.get("reversed_heading", False),
|
||||||
|
hit_points=data.get("hit_points", 1),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -79,4 +79,5 @@ class ShipUnitType(UnitType[Type[ShipType]]):
|
|||||||
manufacturer=data.get("manufacturer", "No data."),
|
manufacturer=data.get("manufacturer", "No data."),
|
||||||
role=data.get("role", "No data."),
|
role=data.get("role", "No data."),
|
||||||
price=data["price"],
|
price=data["price"],
|
||||||
|
hit_points=data.get("hit_points", 1),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
|
|||||||
role: str
|
role: str
|
||||||
price: int
|
price: int
|
||||||
unit_class: UnitClass
|
unit_class: UnitClass
|
||||||
|
hit_points: int
|
||||||
|
|
||||||
_loaded: ClassVar[bool] = False
|
_loaded: ClassVar[bool] = False
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from abc import ABC
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -9,7 +9,9 @@ from typing import (
|
|||||||
Dict,
|
Dict,
|
||||||
Iterator,
|
Iterator,
|
||||||
List,
|
List,
|
||||||
|
Optional,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
@@ -21,8 +23,10 @@ from game.theater import Airfield, ControlPoint
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
|
from game.dcs.unittype import UnitType
|
||||||
from game.sim.simulationresults import SimulationResults
|
from game.sim.simulationresults import SimulationResults
|
||||||
from game.transfers import CargoShip
|
from game.transfers import CargoShip
|
||||||
|
from game.theater import TheaterUnit
|
||||||
from game.unitmap import (
|
from game.unitmap import (
|
||||||
AirliftUnits,
|
AirliftUnits,
|
||||||
ConvoyUnit,
|
ConvoyUnit,
|
||||||
@@ -90,6 +94,103 @@ class BaseCaptureEvent:
|
|||||||
captured_by_player: bool
|
captured_by_player: bool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UnitHitpointUpdate(ABC):
|
||||||
|
unit: Any
|
||||||
|
hit_points: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(
|
||||||
|
cls, data: dict[str, Any], unit_map: UnitMap
|
||||||
|
) -> Optional[UnitHitpointUpdate]:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def is_dead(self) -> bool:
|
||||||
|
# Use hit_points > 1 to indicate unit is alive, rather than >=1 (DCS logic) to account for uncontrolled units which often have a
|
||||||
|
# health floor of 1
|
||||||
|
if self.hit_points > 1:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FlyingUnitHitPointUpdate(UnitHitpointUpdate):
|
||||||
|
unit: FlyingUnit
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(
|
||||||
|
cls, data: dict[str, Any], unit_map: UnitMap
|
||||||
|
) -> Optional[FlyingUnitHitPointUpdate]:
|
||||||
|
unit = unit_map.flight(data["name"])
|
||||||
|
if unit is None:
|
||||||
|
return None
|
||||||
|
return cls(unit, int(float(data["hit_points"])))
|
||||||
|
|
||||||
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
|
if to_player:
|
||||||
|
return self.unit.flight.departure.captured
|
||||||
|
return not self.unit.flight.departure.captured
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TheaterUnitHitPointUpdate(UnitHitpointUpdate):
|
||||||
|
unit: TheaterUnitMapping
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(
|
||||||
|
cls, data: dict[str, Any], unit_map: UnitMap
|
||||||
|
) -> Optional[TheaterUnitHitPointUpdate]:
|
||||||
|
unit = unit_map.theater_units(data["name"])
|
||||||
|
if unit is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if unit.theater_unit.unit_type is None:
|
||||||
|
logging.debug(
|
||||||
|
f"Ground unit {data['name']} does not have a valid unit type."
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if unit.theater_unit.hit_points is None:
|
||||||
|
logging.debug(f"Ground unit {data['name']} does not have hit_points set.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
sim_hit_points = int(
|
||||||
|
float(data["hit_points"])
|
||||||
|
) # Hit points out of the sim i.e. new unit hit points - damage in this turn
|
||||||
|
previous_turn_hit_points = (
|
||||||
|
unit.theater_unit.hit_points
|
||||||
|
) # Hit points at the end of the previous turn
|
||||||
|
full_health_hit_points = (
|
||||||
|
unit.theater_unit.unit_type.hit_points
|
||||||
|
) # Hit points of a new unit
|
||||||
|
|
||||||
|
# Hit points left after damage this turn is subtracted from hit points at the end of the previous turn
|
||||||
|
new_hit_points = previous_turn_hit_points - (
|
||||||
|
full_health_hit_points - sim_hit_points
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(unit, new_hit_points)
|
||||||
|
|
||||||
|
def is_dead(self) -> bool:
|
||||||
|
# Some TheaterUnits can start with low health of around 1, make sure we don't always kill them off.
|
||||||
|
if (
|
||||||
|
self.unit.theater_unit.unit_type is not None
|
||||||
|
and self.unit.theater_unit.unit_type.hit_points is not None
|
||||||
|
and self.unit.theater_unit.unit_type.hit_points <= 1
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
return super().is_dead()
|
||||||
|
|
||||||
|
def is_friendly(self, to_player: bool) -> bool:
|
||||||
|
return self.unit.theater_unit.ground_object.is_friendly(to_player)
|
||||||
|
|
||||||
|
def commit(self) -> None:
|
||||||
|
self.unit.theater_unit.hit_points = self.hit_points
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StateData:
|
class StateData:
|
||||||
#: True if the mission ended. If False, the mission exited abnormally.
|
#: True if the mission ended. If False, the mission exited abnormally.
|
||||||
@@ -108,6 +209,10 @@ class StateData:
|
|||||||
#: Mangled names of bases that were captured during the mission.
|
#: Mangled names of bases that were captured during the mission.
|
||||||
base_capture_events: List[str]
|
base_capture_events: List[str]
|
||||||
|
|
||||||
|
# List of descriptions of damage done to units. Each list element is a dict like the following
|
||||||
|
# {"name": "<damaged unit name>", "hit_points": <hit points as float>}
|
||||||
|
unit_hit_point_updates: List[dict[str, Any]]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls, data: Dict[str, Any], unit_map: UnitMap) -> StateData:
|
def from_json(cls, data: Dict[str, Any], unit_map: UnitMap) -> StateData:
|
||||||
def clean_unit_list(unit_list: List[Any]) -> List[str]:
|
def clean_unit_list(unit_list: List[Any]) -> List[str]:
|
||||||
@@ -147,6 +252,7 @@ class StateData:
|
|||||||
killed_ground_units=killed_ground_units,
|
killed_ground_units=killed_ground_units,
|
||||||
destroyed_statics=data["destroyed_objects_positions"],
|
destroyed_statics=data["destroyed_objects_positions"],
|
||||||
base_capture_events=data["base_capture_events"],
|
base_capture_events=data["base_capture_events"],
|
||||||
|
unit_hit_point_updates=data["unit_hit_point_updates"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -284,6 +390,19 @@ class Debriefing:
|
|||||||
player_losses.append(aircraft)
|
player_losses.append(aircraft)
|
||||||
else:
|
else:
|
||||||
enemy_losses.append(aircraft)
|
enemy_losses.append(aircraft)
|
||||||
|
|
||||||
|
for unit_data in self.state_data.unit_hit_point_updates:
|
||||||
|
damaged_unit = FlyingUnitHitPointUpdate.from_json(unit_data, self.unit_map)
|
||||||
|
if damaged_unit is None:
|
||||||
|
continue
|
||||||
|
if damaged_unit.is_dead():
|
||||||
|
# If unit already killed, nothing to do.
|
||||||
|
if unit_data["name"] in self.state_data.killed_aircraft:
|
||||||
|
continue
|
||||||
|
if damaged_unit.is_friendly(to_player=True):
|
||||||
|
player_losses.append(damaged_unit.unit)
|
||||||
|
else:
|
||||||
|
enemy_losses.append(damaged_unit.unit)
|
||||||
return AirLosses(player_losses, enemy_losses)
|
return AirLosses(player_losses, enemy_losses)
|
||||||
|
|
||||||
def dead_ground_units(self) -> GroundLosses:
|
def dead_ground_units(self) -> GroundLosses:
|
||||||
@@ -356,8 +475,29 @@ class Debriefing:
|
|||||||
losses.enemy_airlifts.append(airlift_unit)
|
losses.enemy_airlifts.append(airlift_unit)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
for unit_data in self.state_data.unit_hit_point_updates:
|
||||||
|
damaged_unit = TheaterUnitHitPointUpdate.from_json(unit_data, self.unit_map)
|
||||||
|
if damaged_unit is None:
|
||||||
|
continue
|
||||||
|
if damaged_unit.is_dead():
|
||||||
|
if unit_data["name"] in self.state_data.killed_ground_units:
|
||||||
|
continue
|
||||||
|
if damaged_unit.is_friendly(to_player=True):
|
||||||
|
losses.player_ground_objects.append(damaged_unit.unit)
|
||||||
|
else:
|
||||||
|
losses.enemy_ground_objects.append(damaged_unit.unit)
|
||||||
|
|
||||||
return losses
|
return losses
|
||||||
|
|
||||||
|
def unit_hit_point_update_events(self) -> List[TheaterUnitHitPointUpdate]:
|
||||||
|
damaged_units = []
|
||||||
|
for unit_data in self.state_data.unit_hit_point_updates:
|
||||||
|
unit = TheaterUnitHitPointUpdate.from_json(unit_data, self.unit_map)
|
||||||
|
if unit is None:
|
||||||
|
continue
|
||||||
|
damaged_units.append(unit)
|
||||||
|
return damaged_units
|
||||||
|
|
||||||
def base_capture_events(self) -> List[BaseCaptureEvent]:
|
def base_capture_events(self) -> List[BaseCaptureEvent]:
|
||||||
"""Keeps only the last instance of a base capture event for each base ID."""
|
"""Keeps only the last instance of a base capture event for each base ID."""
|
||||||
blue_coalition_id = 2
|
blue_coalition_id = 2
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ from game.data.building_data import (
|
|||||||
WW2_FREE,
|
WW2_FREE,
|
||||||
WW2_GERMANY_BUILDINGS,
|
WW2_GERMANY_BUILDINGS,
|
||||||
)
|
)
|
||||||
from game.data.doctrine import (
|
from game.data.doctrine import Doctrine
|
||||||
COLDWAR_DOCTRINE,
|
|
||||||
Doctrine,
|
|
||||||
MODERN_DOCTRINE,
|
|
||||||
WWII_DOCTRINE,
|
|
||||||
)
|
|
||||||
from game.data.groups import GroupRole
|
from game.data.groups import GroupRole
|
||||||
from game.data.units import UnitClass
|
from game.data.units import UnitClass
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
@@ -106,7 +101,7 @@ class Faction:
|
|||||||
jtac_unit: Optional[AircraftType] = field(default=None)
|
jtac_unit: Optional[AircraftType] = field(default=None)
|
||||||
|
|
||||||
# doctrine
|
# doctrine
|
||||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
doctrine: Doctrine = field(default=Doctrine.named("modern"))
|
||||||
|
|
||||||
# List of available building layouts for this faction
|
# List of available building layouts for this faction
|
||||||
building_set: List[str] = field(default_factory=list)
|
building_set: List[str] = field(default_factory=list)
|
||||||
@@ -238,14 +233,7 @@ class Faction:
|
|||||||
|
|
||||||
# Load doctrine
|
# Load doctrine
|
||||||
doctrine = json.get("doctrine", "modern")
|
doctrine = json.get("doctrine", "modern")
|
||||||
if doctrine == "modern":
|
faction.doctrine = Doctrine.named(doctrine)
|
||||||
faction.doctrine = MODERN_DOCTRINE
|
|
||||||
elif doctrine == "coldwar":
|
|
||||||
faction.doctrine = COLDWAR_DOCTRINE
|
|
||||||
elif doctrine == "ww2":
|
|
||||||
faction.doctrine = WWII_DOCTRINE
|
|
||||||
else:
|
|
||||||
faction.doctrine = MODERN_DOCTRINE
|
|
||||||
|
|
||||||
# Load the building set
|
# Load the building set
|
||||||
faction.building_set = []
|
faction.building_set = []
|
||||||
@@ -312,6 +300,11 @@ class Faction:
|
|||||||
self.remove_aircraft("Su-57")
|
self.remove_aircraft("Su-57")
|
||||||
if not mod_settings.ov10a_bronco:
|
if not mod_settings.ov10a_bronco:
|
||||||
self.remove_aircraft("Bronco-OV-10A")
|
self.remove_aircraft("Bronco-OV-10A")
|
||||||
|
if not mod_settings.fa18efg:
|
||||||
|
self.remove_aircraft("FA_18E")
|
||||||
|
self.remove_aircraft("FA_18F")
|
||||||
|
self.remove_aircraft("EA_18G")
|
||||||
|
|
||||||
# frenchpack
|
# frenchpack
|
||||||
if not mod_settings.frenchpack:
|
if not mod_settings.frenchpack:
|
||||||
self.remove_vehicle("AMX10RCR")
|
self.remove_vehicle("AMX10RCR")
|
||||||
|
|||||||
@@ -11,17 +11,14 @@ from shapely import transform
|
|||||||
from shapely.geometry import shape
|
from shapely.geometry import shape
|
||||||
from shapely.geometry.base import BaseGeometry
|
from shapely.geometry.base import BaseGeometry
|
||||||
|
|
||||||
from game.data.doctrine import Doctrine, ALL_DOCTRINES
|
from game.data.doctrine import Doctrine
|
||||||
from .ipsolver import IpSolver
|
from .ipsolver import IpSolver
|
||||||
from .waypointsolver import WaypointSolver
|
from .waypointsolver import WaypointSolver
|
||||||
from ..theater.theaterloader import TERRAINS_BY_NAME
|
from ..theater.theaterloader import TERRAINS_BY_NAME
|
||||||
|
|
||||||
|
|
||||||
def doctrine_from_name(name: str) -> Doctrine:
|
def doctrine_from_name(name: str) -> Doctrine:
|
||||||
for doctrine in ALL_DOCTRINES:
|
return Doctrine.named(name)
|
||||||
if doctrine.name == name:
|
|
||||||
return doctrine
|
|
||||||
raise KeyError
|
|
||||||
|
|
||||||
|
|
||||||
def geometry_ll_to_xy(geometry: BaseGeometry, terrain: Terrain) -> BaseGeometry:
|
def geometry_ll_to_xy(geometry: BaseGeometry, terrain: Terrain) -> BaseGeometry:
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ class RequirementBuilder:
|
|||||||
def maximum_turn_to(
|
def maximum_turn_to(
|
||||||
self, turn_point: Point, next_point: Point, turn_limit: Heading
|
self, turn_point: Point, next_point: Point, turn_limit: Heading
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
large_distance = nautical_miles(400)
|
large_distance = nautical_miles(400)
|
||||||
next_heading = Heading.from_degrees(
|
next_heading = Heading.from_degrees(
|
||||||
angle_between_points(next_point, turn_point)
|
angle_between_points(next_point, turn_point)
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class GroundPlanner:
|
|||||||
self.reserve: List[CombatGroup] = []
|
self.reserve: List[CombatGroup] = []
|
||||||
|
|
||||||
def plan_groundwar(self) -> None:
|
def plan_groundwar(self) -> None:
|
||||||
|
|
||||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||||
|
|
||||||
remaining_available_frontline_units = ground_unit_limit
|
remaining_available_frontline_units = ground_unit_limit
|
||||||
@@ -139,7 +138,6 @@ class GroundPlanner:
|
|||||||
remaining_available_frontline_units -= available
|
remaining_available_frontline_units -= available
|
||||||
|
|
||||||
while available > 0:
|
while available > 0:
|
||||||
|
|
||||||
if role == CombatGroupRole.SHORAD:
|
if role == CombatGroupRole.SHORAD:
|
||||||
count = 1
|
count = 1
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -241,7 +241,6 @@ class AntiAirLayout(TgoLayout):
|
|||||||
location: PresetLocation,
|
location: PresetLocation,
|
||||||
control_point: ControlPoint,
|
control_point: ControlPoint,
|
||||||
) -> IadsGroundObject:
|
) -> IadsGroundObject:
|
||||||
|
|
||||||
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
|
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
|
||||||
return EwrGroundObject(name, location, control_point)
|
return EwrGroundObject(name, location, control_point)
|
||||||
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
|
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ class LayoutLoader:
|
|||||||
temp_mis.country(country.name).ship_group,
|
temp_mis.country(country.name).ship_group,
|
||||||
temp_mis.country(country.name).static_group,
|
temp_mis.country(country.name).static_group,
|
||||||
):
|
):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
g_id, u_id, group_name, group_mapping = mapping.group_for_name(
|
g_id, u_id, group_name, group_mapping = mapping.group_for_name(
|
||||||
dcs_group.name
|
dcs_group.name
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ class AntiShipIngressBuilder(PydcsWaypointBuilder):
|
|||||||
group_names.append(target.name)
|
group_names.append(target.name)
|
||||||
elif isinstance(target, NavalControlPoint):
|
elif isinstance(target, NavalControlPoint):
|
||||||
carrier_name = target.get_carrier_group_name()
|
carrier_name = target.get_carrier_group_name()
|
||||||
if carrier_name:
|
if carrier_name and self.mission.find_group(
|
||||||
|
carrier_name
|
||||||
|
): # Found a carrier, target it.
|
||||||
group_names.append(carrier_name)
|
group_names.append(carrier_name)
|
||||||
|
else: # Could not find carrier/LHA, indicating it was sunk. Target other groups if present e.g. escorts.
|
||||||
|
for ground_object in target.ground_objects:
|
||||||
|
for group in ground_object.groups:
|
||||||
|
group_names.append(group.group_name)
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Unexpected target type for anti-ship mission: %s",
|
"Unexpected target type for anti-ship mission: %s",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
|||||||
|
|
||||||
class RecoveryTankerBuilder(PydcsWaypointBuilder):
|
class RecoveryTankerBuilder(PydcsWaypointBuilder):
|
||||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||||
|
|
||||||
assert self.flight.flight_type == FlightType.REFUELING
|
assert self.flight.flight_type == FlightType.REFUELING
|
||||||
|
|
||||||
# Tanker task required in conjunction with RecoveryTanker task.
|
# Tanker task required in conjunction with RecoveryTanker task.
|
||||||
@@ -48,7 +47,6 @@ class RecoveryTankerBuilder(PydcsWaypointBuilder):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def configure_tanker_tacan(self, waypoint: MovingPoint) -> None:
|
def configure_tanker_tacan(self, waypoint: MovingPoint) -> None:
|
||||||
|
|
||||||
if self.flight.unit_type.dcs_unit_type.tacan:
|
if self.flight.unit_type.dcs_unit_type.tacan:
|
||||||
tanker_info = self.mission_data.tankers[-1]
|
tanker_info = self.mission_data.tankers[-1]
|
||||||
tacan = tanker_info.tacan
|
tacan = tanker_info.tacan
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from .pydcswaypointbuilder import PydcsWaypointBuilder
|
|||||||
|
|
||||||
class SplitPointBuilder(PydcsWaypointBuilder):
|
class SplitPointBuilder(PydcsWaypointBuilder):
|
||||||
def add_tasks(self, waypoint: MovingPoint) -> None:
|
def add_tasks(self, waypoint: MovingPoint) -> None:
|
||||||
|
|
||||||
if not self.flight.flight_type.is_air_to_air:
|
if not self.flight.flight_type.is_air_to_air:
|
||||||
# Capture any non A/A type to avoid issues with SPJs that use the primary radar such as the F/A-18C.
|
# Capture any non A/A type to avoid issues with SPJs that use the primary radar such as the F/A-18C.
|
||||||
# You can bully them with STT to not be able to fire radar guided missiles at you,
|
# You can bully them with STT to not be able to fire radar guided missiles at you,
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ class DrawingsGenerator:
|
|||||||
if destination in seen:
|
if destination in seen:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# Determine path color
|
# Determine path color
|
||||||
if cp.captured and destination.captured:
|
if cp.captured and destination.captured:
|
||||||
color = BLUE_PATH_COLOR
|
color = BLUE_PATH_COLOR
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ class FlotGenerator:
|
|||||||
side: Country,
|
side: Country,
|
||||||
forward_heading: Heading,
|
forward_heading: Heading,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
infantry_position = self.conflict.find_ground_position(
|
infantry_position = self.conflict.find_ground_position(
|
||||||
group.points[0].position.random_point_within(250, 50),
|
group.points[0].position.random_point_within(250, 50),
|
||||||
500,
|
500,
|
||||||
@@ -304,7 +303,6 @@ class FlotGenerator:
|
|||||||
|
|
||||||
# Artillery will fall back when under attack
|
# Artillery will fall back when under attack
|
||||||
if stance != CombatStance.RETREAT:
|
if stance != CombatStance.RETREAT:
|
||||||
|
|
||||||
# Hold position
|
# Hold position
|
||||||
dcs_group.points[1].tasks.append(Hold())
|
dcs_group.points[1].tasks.append(Hold())
|
||||||
retreat = self.find_retreat_point(
|
retreat = self.find_retreat_point(
|
||||||
@@ -476,7 +474,6 @@ class FlotGenerator:
|
|||||||
from_cp: ControlPoint,
|
from_cp: ControlPoint,
|
||||||
to_cp: ControlPoint,
|
to_cp: ControlPoint,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
if not self.game.settings.perf_moving_units:
|
if not self.game.settings.perf_moving_units:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ class NumberedWaypoint:
|
|||||||
|
|
||||||
|
|
||||||
class FlightPlanBuilder:
|
class FlightPlanBuilder:
|
||||||
|
|
||||||
WAYPOINT_DESC_MAX_LEN = 25
|
WAYPOINT_DESC_MAX_LEN = 25
|
||||||
|
|
||||||
def __init__(self, start_time: datetime.datetime, units: UnitSystem) -> None:
|
def __init__(self, start_time: datetime.datetime, units: UnitSystem) -> None:
|
||||||
@@ -503,7 +502,6 @@ class SupportPage(KneeboardPage):
|
|||||||
aewc_ladder = []
|
aewc_ladder = []
|
||||||
|
|
||||||
for single_aewc in self.awacs:
|
for single_aewc in self.awacs:
|
||||||
|
|
||||||
if single_aewc.depature_location is None:
|
if single_aewc.depature_location is None:
|
||||||
dep = "-"
|
dep = "-"
|
||||||
arr = "-"
|
arr = "-"
|
||||||
|
|||||||
@@ -402,7 +402,6 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
|||||||
self.mission_data = mission_data
|
self.mission_data = mission_data
|
||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
|
|
||||||
# This can also be refactored as the general generation was updated
|
# This can also be refactored as the general generation was updated
|
||||||
atc = self.radio_registry.alloc_uhf()
|
atc = self.radio_registry.alloc_uhf()
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class ProcurementAi:
|
|||||||
manage_front_line: bool,
|
manage_front_line: bool,
|
||||||
manage_aircraft: bool,
|
manage_aircraft: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.is_player = for_player
|
self.is_player = for_player
|
||||||
self.air_wing = game.air_wing_for(for_player)
|
self.air_wing = game.air_wing_for(for_player)
|
||||||
|
|||||||
@@ -33,15 +33,19 @@ class FrozenCombatJs(BaseModel):
|
|||||||
if isinstance(combat, AtIp):
|
if isinstance(combat, AtIp):
|
||||||
return FrozenCombatJs(
|
return FrozenCombatJs(
|
||||||
id=combat.id,
|
id=combat.id,
|
||||||
flight_position=combat.flight.position().latlng(),
|
flight_position=LeafletPoint.from_pydcs(combat.flight.position()),
|
||||||
target_positions=[combat.flight.package.target.position.latlng()],
|
target_positions=[
|
||||||
|
LeafletPoint.from_pydcs(combat.flight.package.target.position)
|
||||||
|
],
|
||||||
footprint=None,
|
footprint=None,
|
||||||
)
|
)
|
||||||
if isinstance(combat, DefendingSam):
|
if isinstance(combat, DefendingSam):
|
||||||
return FrozenCombatJs(
|
return FrozenCombatJs(
|
||||||
id=combat.id,
|
id=combat.id,
|
||||||
flight_position=combat.flight.position().latlng(),
|
flight_position=LeafletPoint.from_pydcs(combat.flight.position()),
|
||||||
target_positions=[sam.position.latlng() for sam in combat.air_defenses],
|
target_positions=[
|
||||||
|
LeafletPoint.from_pydcs(sam.position) for sam in combat.air_defenses
|
||||||
|
],
|
||||||
footprint=None,
|
footprint=None,
|
||||||
)
|
)
|
||||||
raise NotImplementedError(f"Unhandled FrozenCombat type: {combat.__class__}")
|
raise NotImplementedError(f"Unhandled FrozenCombat type: {combat.__class__}")
|
||||||
|
|||||||
@@ -28,12 +28,12 @@ class ControlPointJs(BaseModel):
|
|||||||
def for_control_point(control_point: ControlPoint) -> ControlPointJs:
|
def for_control_point(control_point: ControlPoint) -> ControlPointJs:
|
||||||
destination = None
|
destination = None
|
||||||
if control_point.target_position is not None:
|
if control_point.target_position is not None:
|
||||||
destination = control_point.target_position.latlng()
|
destination = LeafletPoint.from_pydcs(control_point.target_position)
|
||||||
return ControlPointJs(
|
return ControlPointJs(
|
||||||
id=control_point.id,
|
id=control_point.id,
|
||||||
name=control_point.name,
|
name=control_point.name,
|
||||||
blue=control_point.captured,
|
blue=control_point.captured,
|
||||||
position=control_point.position.latlng(),
|
position=LeafletPoint.from_pydcs(control_point.position),
|
||||||
mobile=control_point.moveable and control_point.captured,
|
mobile=control_point.moveable and control_point.captured,
|
||||||
destination=destination,
|
destination=destination,
|
||||||
sidc=str(control_point.sidc()),
|
sidc=str(control_point.sidc()),
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class GameUpdateEventsJs(BaseModel):
|
|||||||
def from_events(
|
def from_events(
|
||||||
cls, events: GameUpdateEvents, game: Game | None
|
cls, events: GameUpdateEvents, game: Game | None
|
||||||
) -> GameUpdateEventsJs:
|
) -> GameUpdateEventsJs:
|
||||||
|
|
||||||
# We still need to be able to send update events when there is no game loaded
|
# We still need to be able to send update events when there is no game loaded
|
||||||
# because we need to send the unload event.
|
# because we need to send the unload event.
|
||||||
new_combats = []
|
new_combats = []
|
||||||
@@ -81,9 +80,13 @@ class GameUpdateEventsJs(BaseModel):
|
|||||||
for f in events.updated_front_lines
|
for f in events.updated_front_lines
|
||||||
]
|
]
|
||||||
|
|
||||||
|
reset_on_map_center: LeafletPoint | None = None
|
||||||
|
if events.reset_on_map_center is not None:
|
||||||
|
reset_on_map_center = LeafletPoint.from_pydcs(events.reset_on_map_center)
|
||||||
return GameUpdateEventsJs(
|
return GameUpdateEventsJs(
|
||||||
updated_flight_positions={
|
updated_flight_positions={
|
||||||
f[0].id: f[1].latlng() for f in events.updated_flight_positions
|
f[0].id: LeafletPoint.from_pydcs(f[1])
|
||||||
|
for f in events.updated_flight_positions
|
||||||
},
|
},
|
||||||
new_combats=new_combats,
|
new_combats=new_combats,
|
||||||
updated_combats=updated_combats,
|
updated_combats=updated_combats,
|
||||||
@@ -110,7 +113,7 @@ class GameUpdateEventsJs(BaseModel):
|
|||||||
],
|
],
|
||||||
updated_iads=updated_iads,
|
updated_iads=updated_iads,
|
||||||
deleted_iads=events.deleted_iads_connections,
|
deleted_iads=events.deleted_iads_connections,
|
||||||
reset_on_map_center=events.reset_on_map_center,
|
reset_on_map_center=reset_on_map_center,
|
||||||
game_unloaded=events.game_unloaded,
|
game_unloaded=events.game_unloaded,
|
||||||
new_turn=events.new_turn,
|
new_turn=events.new_turn,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from asyncio import wait
|
from asyncio import wait, Future
|
||||||
|
|
||||||
from fastapi import APIRouter, WebSocket
|
from fastapi import APIRouter, WebSocket
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
@@ -16,9 +16,9 @@ class ConnectionManager:
|
|||||||
self.active_connections: list[WebSocket] = []
|
self.active_connections: list[WebSocket] = []
|
||||||
|
|
||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
futures = []
|
futures: list[Future[None]] = []
|
||||||
for connection in self.active_connections:
|
for connection in self.active_connections:
|
||||||
futures.append(connection.close())
|
futures.append(asyncio.create_task(connection.close()))
|
||||||
await wait(futures)
|
await wait(futures)
|
||||||
|
|
||||||
async def connect(self, websocket: WebSocket) -> None:
|
async def connect(self, websocket: WebSocket) -> None:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class FlightJs(BaseModel):
|
|||||||
# lost.
|
# lost.
|
||||||
position = None
|
position = None
|
||||||
if isinstance(flight.state, InFlight) or isinstance(flight.state, Killed):
|
if isinstance(flight.state, InFlight) or isinstance(flight.state, Killed):
|
||||||
position = flight.position().latlng()
|
position = LeafletPoint.from_pydcs(flight.position())
|
||||||
waypoints = None
|
waypoints = None
|
||||||
if with_waypoints:
|
if with_waypoints:
|
||||||
waypoints = waypoints_for_flight(flight)
|
waypoints = waypoints_for_flight(flight)
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ class FrontLineJs(BaseModel):
|
|||||||
bounds = FrontLineConflictDescription.frontline_bounds(front_line, theater)
|
bounds = FrontLineConflictDescription.frontline_bounds(front_line, theater)
|
||||||
return FrontLineJs(
|
return FrontLineJs(
|
||||||
id=front_line.id,
|
id=front_line.id,
|
||||||
extents=[bounds.left_position.latlng(), bounds.right_position.latlng()],
|
extents=[
|
||||||
|
LeafletPoint.from_pydcs(bounds.left_position),
|
||||||
|
LeafletPoint.from_pydcs(bounds.right_position),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ from pydantic import BaseModel
|
|||||||
from game.server.controlpoints.models import ControlPointJs
|
from game.server.controlpoints.models import ControlPointJs
|
||||||
from game.server.flights.models import FlightJs
|
from game.server.flights.models import FlightJs
|
||||||
from game.server.frontlines.models import FrontLineJs
|
from game.server.frontlines.models import FrontLineJs
|
||||||
|
from game.server.iadsnetwork.models import IadsNetworkJs
|
||||||
from game.server.leaflet import LeafletPoint
|
from game.server.leaflet import LeafletPoint
|
||||||
from game.server.mapzones.models import ThreatZoneContainerJs, UnculledZoneJs
|
from game.server.mapzones.models import ThreatZoneContainerJs, UnculledZoneJs
|
||||||
from game.server.navmesh.models import NavMeshesJs
|
from game.server.navmesh.models import NavMeshesJs
|
||||||
from game.server.supplyroutes.models import SupplyRouteJs
|
from game.server.supplyroutes.models import SupplyRouteJs
|
||||||
from game.server.tgos.models import TgoJs
|
from game.server.tgos.models import TgoJs
|
||||||
from game.server.iadsnetwork.models import IadsConnectionJs, IadsNetworkJs
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@@ -44,6 +44,8 @@ class GameJs(BaseModel):
|
|||||||
iads_network=IadsNetworkJs.from_network(game.theater.iads_network),
|
iads_network=IadsNetworkJs.from_network(game.theater.iads_network),
|
||||||
threat_zones=ThreatZoneContainerJs.for_game(game),
|
threat_zones=ThreatZoneContainerJs.for_game(game),
|
||||||
navmeshes=NavMeshesJs.from_game(game),
|
navmeshes=NavMeshesJs.from_game(game),
|
||||||
map_center=game.theater.terrain.map_view_default.position.latlng(),
|
map_center=LeafletPoint.from_pydcs(
|
||||||
|
game.theater.terrain.map_view_default.position
|
||||||
|
),
|
||||||
unculled_zones=UnculledZoneJs.from_game(game),
|
unculled_zones=UnculledZoneJs.from_game(game),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from game.server.leaflet import LeafletPoint
|
from game.server.leaflet import LeafletPoint
|
||||||
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork
|
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork
|
||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
|
||||||
|
|
||||||
|
|
||||||
class IadsConnectionJs(BaseModel):
|
class IadsConnectionJs(BaseModel):
|
||||||
@@ -45,8 +45,8 @@ class IadsConnectionJs(BaseModel):
|
|||||||
IadsConnectionJs(
|
IadsConnectionJs(
|
||||||
id=id,
|
id=id,
|
||||||
points=[
|
points=[
|
||||||
tgo.position.latlng(),
|
LeafletPoint.from_pydcs(tgo.position),
|
||||||
connection.ground_object.position.latlng(),
|
LeafletPoint.from_pydcs(connection.ground_object.position),
|
||||||
],
|
],
|
||||||
node=tgo.id,
|
node=tgo.id,
|
||||||
connected=connection.ground_object.id,
|
connected=connection.ground_object.id,
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class LeafletPoint(BaseModel):
|
|||||||
|
|
||||||
title = "LatLng"
|
title = "LatLng"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_pydcs(point: Point) -> LeafletPoint:
|
||||||
|
latlng = point.latlng()
|
||||||
|
return LeafletPoint(lat=latlng.lat, lng=latlng.lng)
|
||||||
|
|
||||||
|
|
||||||
LeafletLine = list[LeafletPoint]
|
LeafletLine = list[LeafletPoint]
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class UnculledZoneJs(BaseModel):
|
|||||||
def from_game(game: Game) -> list[UnculledZoneJs]:
|
def from_game(game: Game) -> list[UnculledZoneJs]:
|
||||||
return [
|
return [
|
||||||
UnculledZoneJs(
|
UnculledZoneJs(
|
||||||
position=zone.latlng(),
|
position=LeafletPoint.from_pydcs(zone),
|
||||||
radius=game.settings.perf_culling_distance * 1000,
|
radius=game.settings.perf_culling_distance * 1000,
|
||||||
)
|
)
|
||||||
for zone in game.get_culling_zones()
|
for zone in game.get_culling_zones()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
from pydantic import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
class ServerSettings(BaseSettings):
|
class ServerSettings(BaseSettings):
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class SupplyRouteJs(BaseModel):
|
|||||||
# https://reactjs.org/docs/lists-and-keys.html#keys
|
# https://reactjs.org/docs/lists-and-keys.html#keys
|
||||||
# https://github.com/dcs-liberation/dcs_liberation/issues/2167
|
# https://github.com/dcs-liberation/dcs_liberation/issues/2167
|
||||||
id=uuid.uuid4(),
|
id=uuid.uuid4(),
|
||||||
points=[p.latlng() for p in points],
|
points=[LeafletPoint.from_pydcs(p) for p in points],
|
||||||
front_active=not sea and a.front_is_active(b),
|
front_active=not sea and a.front_is_active(b),
|
||||||
is_sea=sea,
|
is_sea=sea,
|
||||||
blue=a.captured,
|
blue=a.captured,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class TgoJs(BaseModel):
|
|||||||
control_point_name=tgo.control_point.name,
|
control_point_name=tgo.control_point.name,
|
||||||
category=tgo.category,
|
category=tgo.category,
|
||||||
blue=tgo.control_point.captured,
|
blue=tgo.control_point.captured,
|
||||||
position=tgo.position.latlng(),
|
position=LeafletPoint.from_pydcs(tgo.position),
|
||||||
units=[unit.display_name for unit in tgo.units],
|
units=[unit.display_name for unit in tgo.units],
|
||||||
threat_ranges=threat_ranges,
|
threat_ranges=threat_ranges,
|
||||||
detection_ranges=detection_ranges,
|
detection_ranges=detection_ranges,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class FlightWaypointJs(BaseModel):
|
|||||||
|
|
||||||
return FlightWaypointJs(
|
return FlightWaypointJs(
|
||||||
name=waypoint.name,
|
name=waypoint.name,
|
||||||
position=waypoint.position.latlng(),
|
position=LeafletPoint.from_pydcs(waypoint.position),
|
||||||
altitude_ft=waypoint.alt.feet,
|
altitude_ft=waypoint.alt.feet,
|
||||||
altitude_reference=waypoint.alt_type,
|
altitude_reference=waypoint.alt_type,
|
||||||
is_movable=is_movable,
|
is_movable=is_movable,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from dcs.mapping import LatLng
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@@ -38,7 +37,7 @@ class GameUpdateEvents:
|
|||||||
updated_control_points: set[ControlPoint] = field(default_factory=set)
|
updated_control_points: set[ControlPoint] = field(default_factory=set)
|
||||||
updated_iads: set[IadsNetworkNode] = field(default_factory=set)
|
updated_iads: set[IadsNetworkNode] = field(default_factory=set)
|
||||||
deleted_iads_connections: set[UUID] = field(default_factory=set)
|
deleted_iads_connections: set[UUID] = field(default_factory=set)
|
||||||
reset_on_map_center: LatLng | None = None
|
reset_on_map_center: Point | None = None
|
||||||
game_unloaded: bool = False
|
game_unloaded: bool = False
|
||||||
new_turn: bool = False
|
new_turn: bool = False
|
||||||
shutting_down: bool = False
|
shutting_down: bool = False
|
||||||
@@ -140,9 +139,7 @@ class GameUpdateEvents:
|
|||||||
self.game_unloaded = True
|
self.game_unloaded = True
|
||||||
self.reset_on_map_center = None
|
self.reset_on_map_center = None
|
||||||
else:
|
else:
|
||||||
self.reset_on_map_center = (
|
self.reset_on_map_center = game.theater.terrain.map_view_default.position
|
||||||
game.theater.terrain.map_view_default.position.latlng()
|
|
||||||
)
|
|
||||||
self.game_unloaded = False
|
self.game_unloaded = False
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class MissionResultsProcessor:
|
|||||||
self.commit_damaged_runways(debriefing)
|
self.commit_damaged_runways(debriefing)
|
||||||
self.commit_captures(debriefing, events)
|
self.commit_captures(debriefing, events)
|
||||||
self.commit_front_line_battle_impact(debriefing, events)
|
self.commit_front_line_battle_impact(debriefing, events)
|
||||||
|
self.commit_unit_damage(debriefing)
|
||||||
self.record_carcasses(debriefing)
|
self.record_carcasses(debriefing)
|
||||||
|
|
||||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||||
@@ -245,7 +246,6 @@ class MissionResultsProcessor:
|
|||||||
delta = DEFEAT_INFLUENCE
|
delta = DEFEAT_INFLUENCE
|
||||||
status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory."
|
status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory."
|
||||||
elif ally_casualties > enemy_casualties:
|
elif ally_casualties > enemy_casualties:
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ally_units_alive > 2 * enemy_units_alive
|
ally_units_alive > 2 * enemy_units_alive
|
||||||
and player_aggresive
|
and player_aggresive
|
||||||
@@ -308,6 +308,14 @@ class MissionResultsProcessor:
|
|||||||
f"{enemy_cp.name}. {status_msg}",
|
f"{enemy_cp.name}. {status_msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def commit_unit_damage(debriefing: Debriefing) -> None:
|
||||||
|
for damaged_unit in debriefing.unit_hit_point_update_events():
|
||||||
|
logging.info(
|
||||||
|
f"{damaged_unit.unit.theater_unit.name} damaged, setting hit points to {damaged_unit.hit_points}"
|
||||||
|
)
|
||||||
|
damaged_unit.commit()
|
||||||
|
|
||||||
def redeploy_units(self, cp: ControlPoint) -> None:
|
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||||
""" "
|
""" "
|
||||||
Auto redeploy units to newly captured base
|
Auto redeploy units to newly captured base
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ class SquadronDef:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_yaml(cls, path: Path) -> SquadronDef:
|
def from_yaml(cls, path: Path) -> SquadronDef:
|
||||||
|
|
||||||
with path.open(encoding="utf8") as squadron_file:
|
with path.open(encoding="utf8") as squadron_file:
|
||||||
data = yaml.safe_load(squadron_file)
|
data = yaml.safe_load(squadron_file)
|
||||||
|
|
||||||
|
|||||||
@@ -271,15 +271,15 @@ class RunwayStatus:
|
|||||||
def needs_repair(self) -> bool:
|
def needs_repair(self) -> bool:
|
||||||
return self.damaged and self.repair_turns_remaining is None
|
return self.damaged and self.repair_turns_remaining is None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def describe(self) -> str:
|
||||||
if not self.damaged:
|
if not self.damaged:
|
||||||
return "Runway operational"
|
return "operational"
|
||||||
|
|
||||||
turns_remaining = self.repair_turns_remaining
|
turns_remaining = self.repair_turns_remaining
|
||||||
if turns_remaining is None:
|
if turns_remaining is None:
|
||||||
return "Runway damaged"
|
return "damaged"
|
||||||
|
|
||||||
return f"Runway repairing, {turns_remaining} turns remaining"
|
return f"repairing, {turns_remaining} turns remaining"
|
||||||
|
|
||||||
|
|
||||||
@total_ordering
|
@total_ordering
|
||||||
@@ -915,6 +915,10 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
|
|||||||
def runway_status(self) -> RunwayStatus:
|
def runway_status(self) -> RunwayStatus:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def describe_runway_status(self) -> str | None:
|
||||||
|
"""Description of the runway status suitable for UI use."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def runway_can_be_repaired(self) -> bool:
|
def runway_can_be_repaired(self) -> bool:
|
||||||
return self.runway_status.needs_repair
|
return self.runway_status.needs_repair
|
||||||
@@ -1157,6 +1161,9 @@ class Airfield(ControlPoint):
|
|||||||
def runway_status(self) -> RunwayStatus:
|
def runway_status(self) -> RunwayStatus:
|
||||||
return self._runway_status
|
return self._runway_status
|
||||||
|
|
||||||
|
def describe_runway_status(self) -> str:
|
||||||
|
return f"Runway {self.runway_status.describe()}"
|
||||||
|
|
||||||
def damage_runway(self) -> None:
|
def damage_runway(self) -> None:
|
||||||
self.runway_status.damage()
|
self.runway_status.damage()
|
||||||
|
|
||||||
@@ -1275,6 +1282,12 @@ class NavalControlPoint(ControlPoint, ABC):
|
|||||||
def runway_status(self) -> RunwayStatus:
|
def runway_status(self) -> RunwayStatus:
|
||||||
return RunwayStatus(damaged=not self.runway_is_operational())
|
return RunwayStatus(damaged=not self.runway_is_operational())
|
||||||
|
|
||||||
|
def describe_runway_status(self) -> str:
|
||||||
|
if self.runway_is_operational():
|
||||||
|
return f"Flight deck {self.runway_status.describe()}"
|
||||||
|
# Special handling for not operational carriers/LHAs
|
||||||
|
return f"Sunk"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def runway_can_be_repaired(self) -> bool:
|
def runway_can_be_repaired(self) -> bool:
|
||||||
return False
|
return False
|
||||||
@@ -1428,6 +1441,9 @@ class OffMapSpawn(ControlPoint):
|
|||||||
def runway_status(self) -> RunwayStatus:
|
def runway_status(self) -> RunwayStatus:
|
||||||
return RunwayStatus()
|
return RunwayStatus()
|
||||||
|
|
||||||
|
def describe_runway_status(self) -> str:
|
||||||
|
return f"Off-map airport {self.runway_status.describe()}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_deploy_ground_units(self) -> bool:
|
def can_deploy_ground_units(self) -> bool:
|
||||||
return False
|
return False
|
||||||
@@ -1474,6 +1490,11 @@ class Fob(ControlPoint):
|
|||||||
def runway_status(self) -> RunwayStatus:
|
def runway_status(self) -> RunwayStatus:
|
||||||
return RunwayStatus()
|
return RunwayStatus()
|
||||||
|
|
||||||
|
def describe_runway_status(self) -> str | None:
|
||||||
|
if not self.has_helipads:
|
||||||
|
return None
|
||||||
|
return f"FARP {self.runway_status.describe()}"
|
||||||
|
|
||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from game.ato import FlightType
|
from game.ato import FlightType
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class ModSettings:
|
|||||||
frenchpack: bool = False
|
frenchpack: bool = False
|
||||||
high_digit_sams: bool = False
|
high_digit_sams: bool = False
|
||||||
ov10a_bronco: bool = False
|
ov10a_bronco: bool = False
|
||||||
|
fa18efg: bool = False
|
||||||
|
|
||||||
def save_player_settings(self) -> None:
|
def save_player_settings(self) -> None:
|
||||||
"""Saves the player's global settings to the user directory."""
|
"""Saves the player's global settings to the user directory."""
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class TheaterUnit:
|
|||||||
position: PointWithHeading
|
position: PointWithHeading
|
||||||
# The parent ground object
|
# The parent ground object
|
||||||
ground_object: TheaterGroundObject
|
ground_object: TheaterGroundObject
|
||||||
|
# Number of hit points the unit has
|
||||||
|
hit_points: Optional[int] = None
|
||||||
# State of the unit, dead or alive
|
# State of the unit, dead or alive
|
||||||
alive: bool = True
|
alive: bool = True
|
||||||
|
|
||||||
@@ -42,13 +44,17 @@ class TheaterUnit:
|
|||||||
def from_template(
|
def from_template(
|
||||||
id: int, dcs_type: Type[DcsUnitType], t: LayoutUnit, go: TheaterGroundObject
|
id: int, dcs_type: Type[DcsUnitType], t: LayoutUnit, go: TheaterGroundObject
|
||||||
) -> TheaterUnit:
|
) -> TheaterUnit:
|
||||||
return TheaterUnit(
|
unit = TheaterUnit(
|
||||||
id,
|
id,
|
||||||
t.name,
|
t.name,
|
||||||
dcs_type,
|
dcs_type,
|
||||||
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
||||||
go,
|
go,
|
||||||
)
|
)
|
||||||
|
# if the TheaterUnit represents a GroundUnitType or ShipUnitType, initialize health to full hit points
|
||||||
|
if unit.unit_type is not None:
|
||||||
|
unit.hit_points = unit.unit_type.hit_points
|
||||||
|
return unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_type(self) -> Optional[UnitType[Any]]:
|
def unit_type(self) -> Optional[UnitType[Any]]:
|
||||||
@@ -70,14 +76,12 @@ class TheaterUnit:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def display_name(self) -> str:
|
def display_name(self) -> str:
|
||||||
dead_label = " [DEAD]" if not self.alive else ""
|
|
||||||
unit_label = self.unit_type or self.type.name or self.name
|
unit_label = self.unit_type or self.type.name or self.name
|
||||||
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
|
return f"{str(self.id).zfill(4)} | {unit_label}{self._status_label()}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def short_name(self) -> str:
|
def short_name(self) -> str:
|
||||||
dead_label = " [DEAD]" if not self.alive else ""
|
return f"<b>{self.type.id[0:18]}</b> {self._status_label()}"
|
||||||
return f"<b>{self.type.id[0:18]}</b> {dead_label}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_static(self) -> bool:
|
def is_static(self) -> bool:
|
||||||
@@ -117,6 +121,18 @@ class TheaterUnit:
|
|||||||
unit_range = getattr(self.type, "threat_range", None)
|
unit_range = getattr(self.type, "threat_range", None)
|
||||||
return meters(unit_range if unit_range is not None and self.alive else 0)
|
return meters(unit_range if unit_range is not None and self.alive else 0)
|
||||||
|
|
||||||
|
def _status_label(self) -> str:
|
||||||
|
if not self.alive:
|
||||||
|
return " [DEAD]"
|
||||||
|
if self.unit_type is None:
|
||||||
|
return ""
|
||||||
|
if self.hit_points is None:
|
||||||
|
return ""
|
||||||
|
if self.unit_type.hit_points == self.hit_points:
|
||||||
|
return ""
|
||||||
|
damage_percentage = 100 - int(100 * self.hit_points / self.unit_type.hit_points)
|
||||||
|
return f" [DAMAGED {damage_percentage}%]"
|
||||||
|
|
||||||
|
|
||||||
class SceneryUnit(TheaterUnit):
|
class SceneryUnit(TheaterUnit):
|
||||||
"""Special TheaterUnit for handling scenery ground objects"""
|
"""Special TheaterUnit for handling scenery ground objects"""
|
||||||
@@ -196,7 +212,8 @@ class TheaterGroup:
|
|||||||
|
|
||||||
def max_threat_range(self, radar_only: bool = False) -> Distance:
|
def max_threat_range(self, radar_only: bool = False) -> Distance:
|
||||||
"""Calculate the maximum threat range of the TheaterGroup.
|
"""Calculate the maximum threat range of the TheaterGroup.
|
||||||
This also checks for Launcher and Tracker Pairs and if they are functioning or not. Allows to also use only radar emitting units for the calculation with the parameter."""
|
This also checks for Launcher and Tracker Pairs and if they are functioning or not. Allows to also use only radar emitting units for the calculation with the parameter.
|
||||||
|
"""
|
||||||
max_non_radar = meters(0)
|
max_non_radar = meters(0)
|
||||||
max_telar_range = meters(0)
|
max_telar_range = meters(0)
|
||||||
max_tel_range = meters(0)
|
max_tel_range = meters(0)
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ class ThreatZones:
|
|||||||
cls, doctrine: Doctrine, control_point: ControlPoint
|
cls, doctrine: Doctrine, control_point: ControlPoint
|
||||||
) -> Distance:
|
) -> Distance:
|
||||||
cap_threat_range = (
|
cap_threat_range = (
|
||||||
doctrine.cap_max_distance_from_cp + doctrine.cap_engagement_range
|
doctrine.cap.max_distance_from_cp + doctrine.cap.engagement_range
|
||||||
)
|
)
|
||||||
opposing_airfield = cls.closest_enemy_airbase(
|
opposing_airfield = cls.closest_enemy_airbase(
|
||||||
control_point, cap_threat_range * 2
|
control_point, cap_threat_range * 2
|
||||||
|
|||||||
@@ -718,7 +718,6 @@ class PendingTransfers:
|
|||||||
self.order_airlift_assets_at(control_point)
|
self.order_airlift_assets_at(control_point)
|
||||||
|
|
||||||
def desired_airlift_capacity(self, control_point: ControlPoint) -> int:
|
def desired_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||||
|
|
||||||
if control_point.has_factory:
|
if control_point.has_factory:
|
||||||
is_major_hub = control_point.total_aircraft_parking > 0
|
is_major_hub = control_point.total_aircraft_parking > 0
|
||||||
# Check if there is a CP which is only reachable via Airlift
|
# Check if there is a CP which is only reachable via Airlift
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
MAJOR_VERSION = 9
|
MAJOR_VERSION = 11
|
||||||
MINOR_VERSION = 0
|
MINOR_VERSION = 0
|
||||||
MICRO_VERSION = 0
|
MICRO_VERSION = 0
|
||||||
VERSION_NUMBER = ".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
|
VERSION_NUMBER = ".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
|
||||||
|
|||||||
@@ -190,7 +190,6 @@ class Weather(ABC):
|
|||||||
def interpolate_solar_activity(
|
def interpolate_solar_activity(
|
||||||
time_of_day: TimeOfDay, high: float, low: float
|
time_of_day: TimeOfDay, high: float, low: float
|
||||||
) -> float:
|
) -> float:
|
||||||
|
|
||||||
scale: float = 0
|
scale: float = 0
|
||||||
|
|
||||||
match time_of_day:
|
match time_of_day:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from .jas39 import *
|
|||||||
from .ov10a import *
|
from .ov10a import *
|
||||||
from .su57 import *
|
from .su57 import *
|
||||||
from .uh60l import *
|
from .uh60l import *
|
||||||
|
from .fa18efg import *
|
||||||
|
|
||||||
|
|
||||||
def load_mods() -> None:
|
def load_mods() -> None:
|
||||||
|
|||||||
1
pydcs_extensions/fa18efg/__init__.py
Normal file
1
pydcs_extensions/fa18efg/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .fa18efg import *
|
||||||
2499
pydcs_extensions/fa18efg/fa18efg.py
Normal file
2499
pydcs_extensions/fa18efg/fa18efg.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ def init():
|
|||||||
|
|
||||||
if os.path.isfile(THEME_PREFERENCES_FILE_PATH):
|
if os.path.isfile(THEME_PREFERENCES_FILE_PATH):
|
||||||
try:
|
try:
|
||||||
with (open(THEME_PREFERENCES_FILE_PATH)) as prefs:
|
with open(THEME_PREFERENCES_FILE_PATH) as prefs:
|
||||||
pref_data = json.loads(prefs.read())
|
pref_data = json.loads(prefs.read())
|
||||||
__theme_index = pref_data["theme_index"]
|
__theme_index = pref_data["theme_index"]
|
||||||
set_theme_index(__theme_index)
|
set_theme_index(__theme_index)
|
||||||
@@ -83,5 +83,5 @@ def get_theme_css_file():
|
|||||||
# save current theme index to json file
|
# save current theme index to json file
|
||||||
def save_theme_config():
|
def save_theme_config():
|
||||||
pref_data = {"theme_index": get_theme_index()}
|
pref_data = {"theme_index": get_theme_index()}
|
||||||
with (open(THEME_PREFERENCES_FILE_PATH, "w")) as prefs:
|
with open(THEME_PREFERENCES_FILE_PATH, "w") as prefs:
|
||||||
prefs.write(json.dumps(pref_data))
|
prefs.write(json.dumps(pref_data))
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
LogHook = typing.Callable[[str], None]
|
LogHook = typing.Callable[[str], None]
|
||||||
|
|
||||||
@@ -15,6 +18,16 @@ class HookableInMemoryHandler(logging.Handler):
|
|||||||
self._log = ""
|
self._log = ""
|
||||||
self._hook = None
|
self._hook = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def iter_registered_handlers(
|
||||||
|
logger: logging.Logger | None = None,
|
||||||
|
) -> Iterator[HookableInMemoryHandler]:
|
||||||
|
if logger is None:
|
||||||
|
logger = logging.getLogger()
|
||||||
|
for handler in logger.handlers:
|
||||||
|
if isinstance(handler, HookableInMemoryHandler):
|
||||||
|
yield handler
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log(self) -> str:
|
def log(self) -> str:
|
||||||
return self._log
|
return self._log
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ class AtoModel(QAbstractListModel):
|
|||||||
return
|
return
|
||||||
|
|
||||||
package_model = self.find_matching_package_model(package)
|
package_model = self.find_matching_package_model(package)
|
||||||
for flight in package.flights:
|
for flight in list(package.flights):
|
||||||
if flight.state.cancelable:
|
if flight.state.cancelable:
|
||||||
package_model.delete_flight(flight)
|
package_model.delete_flight(flight)
|
||||||
events.delete_flight(flight)
|
events.delete_flight(flight)
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
return waypoints
|
return waypoints
|
||||||
|
|
||||||
def find_possible_waypoints(self):
|
def find_possible_waypoints(self):
|
||||||
|
|
||||||
self.wpts = []
|
self.wpts = []
|
||||||
model = QStandardItemModel()
|
model = QStandardItemModel()
|
||||||
i = 0
|
i = 0
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from game.debriefing import Debriefing
|
|||||||
|
|
||||||
|
|
||||||
class GameUpdateSignal(QObject):
|
class GameUpdateSignal(QObject):
|
||||||
|
|
||||||
instance = None
|
instance = None
|
||||||
gameupdated = Signal(Game)
|
gameupdated = Signal(Game)
|
||||||
budgetupdated = Signal(Game)
|
budgetupdated = Signal(Game)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
|
|||||||
from game.turnstate import TurnState
|
from game.turnstate import TurnState
|
||||||
from qt_ui import liberation_install
|
from qt_ui import liberation_install
|
||||||
from qt_ui.dialogs import Dialog
|
from qt_ui.dialogs import Dialog
|
||||||
|
from qt_ui.logging_handler import HookableInMemoryHandler
|
||||||
from qt_ui.models import GameModel
|
from qt_ui.models import GameModel
|
||||||
from qt_ui.simcontroller import SimController
|
from qt_ui.simcontroller import SimController
|
||||||
from qt_ui.uiflags import UiFlags
|
from qt_ui.uiflags import UiFlags
|
||||||
@@ -492,6 +493,7 @@ class QLiberationWindow(QMainWindow):
|
|||||||
"ColonelAkirNakesh",
|
"ColonelAkirNakesh",
|
||||||
"Nosajthedevil",
|
"Nosajthedevil",
|
||||||
"kivipe",
|
"kivipe",
|
||||||
|
"Chilli935",
|
||||||
]
|
]
|
||||||
text = (
|
text = (
|
||||||
"<h3>DCS Liberation "
|
"<h3>DCS Liberation "
|
||||||
@@ -576,6 +578,10 @@ class QLiberationWindow(QMainWindow):
|
|||||||
self._cp_dialog = QBaseMenu2(None, cp, self.game_model)
|
self._cp_dialog = QBaseMenu2(None, cp, self.game_model)
|
||||||
self._cp_dialog.show()
|
self._cp_dialog.show()
|
||||||
|
|
||||||
|
def _disconnect_log_signals(self) -> None:
|
||||||
|
for handler in HookableInMemoryHandler.iter_registered_handlers():
|
||||||
|
handler.clearHook()
|
||||||
|
|
||||||
def _qsettings(self) -> QSettings:
|
def _qsettings(self) -> QSettings:
|
||||||
return QSettings("DCS Liberation", "Qt UI")
|
return QSettings("DCS Liberation", "Qt UI")
|
||||||
|
|
||||||
@@ -597,6 +603,7 @@ class QLiberationWindow(QMainWindow):
|
|||||||
QMessageBox.Yes | QMessageBox.No,
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
)
|
)
|
||||||
if result == QMessageBox.Yes:
|
if result == QMessageBox.Yes:
|
||||||
|
self._disconnect_log_signals()
|
||||||
self._save_window_geometry()
|
self._save_window_geometry()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
self.dialog = None
|
self.dialog = None
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
|||||||
|
|
||||||
|
|
||||||
class DebriefingFileWrittenSignal(QObject):
|
class DebriefingFileWrittenSignal(QObject):
|
||||||
|
|
||||||
instance = None
|
instance = None
|
||||||
debriefingReceived = Signal(Debriefing)
|
debriefingReceived = Signal(Debriefing)
|
||||||
|
|
||||||
|
|||||||
@@ -254,19 +254,22 @@ class QBaseMenu2(QDialog):
|
|||||||
f" (Up to {ground_unit_limit} deployable, {unit_overage} reserve)"
|
f" (Up to {ground_unit_limit} deployable, {unit_overage} reserve)"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.intel_summary.setText(
|
intel_lines = [
|
||||||
"\n".join(
|
f"{aircraft}/{parking} aircraft",
|
||||||
[
|
f"{self.cp.base.total_armor} ground units" + deployable_unit_info,
|
||||||
f"{aircraft}/{parking} aircraft",
|
f"{allocated.total_transferring} more ground units en route, {allocated.total_ordered} ordered",
|
||||||
f"{self.cp.base.total_armor} ground units" + deployable_unit_info,
|
]
|
||||||
f"{allocated.total_transferring} more ground units en route, {allocated.total_ordered} ordered",
|
if (runway_description := self.cp.describe_runway_status()) is not None:
|
||||||
str(self.cp.runway_status),
|
intel_lines.append(runway_description)
|
||||||
f"{self.cp.active_ammo_depots_count}/{self.cp.total_ammo_depots_count} ammo depots",
|
intel_lines.extend(
|
||||||
f"{'Factory can produce units' if self.cp.has_factory else 'Does not have a factory'}",
|
[
|
||||||
]
|
f"{self.cp.active_ammo_depots_count}/{self.cp.total_ammo_depots_count} ammo depots",
|
||||||
)
|
f"{'Factory can produce units' if self.cp.has_factory else 'Does not have a factory'}",
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.intel_summary.setText("\n".join(intel_lines))
|
||||||
|
|
||||||
def generate_intel_tooltip(self) -> str:
|
def generate_intel_tooltip(self) -> str:
|
||||||
tooltip = (
|
tooltip = (
|
||||||
f"Deployable unit limit ({self.cp.frontline_unit_count_limit}) = {FREE_FRONTLINE_UNIT_SUPPLY} (base) + "
|
f"Deployable unit limit ({self.cp.frontline_unit_count_limit}) = {FREE_FRONTLINE_UNIT_SUPPLY} (base) + "
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class QGroundObjectMenu(QDialog):
|
|||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
|
|
||||||
self.mainLayout = QVBoxLayout()
|
self.mainLayout = QVBoxLayout()
|
||||||
self.budget = QBudgetBox(self.game)
|
self.budget = QBudgetBox(self.game)
|
||||||
self.budget.setGame(self.game)
|
self.budget.setGame(self.game)
|
||||||
@@ -105,7 +104,6 @@ class QGroundObjectMenu(QDialog):
|
|||||||
self.setLayout(self.mainLayout)
|
self.setLayout(self.mainLayout)
|
||||||
|
|
||||||
def doLayout(self):
|
def doLayout(self):
|
||||||
|
|
||||||
self.update_total_value()
|
self.update_total_value()
|
||||||
self.intelBox = QGroupBox("Units :")
|
self.intelBox = QGroupBox("Units :")
|
||||||
self.intelLayout = QGridLayout()
|
self.intelLayout = QGridLayout()
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ class IntelWindow(QDialog):
|
|||||||
self.refresh_layout()
|
self.refresh_layout()
|
||||||
|
|
||||||
def refresh_layout(self) -> None:
|
def refresh_layout(self) -> None:
|
||||||
|
|
||||||
# Clear the existing layout
|
# Clear the existing layout
|
||||||
if self.layout():
|
if self.layout():
|
||||||
idx = 0
|
idx = 0
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import logging
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from PySide6.QtCore import Signal
|
from PySide6.QtCore import Signal
|
||||||
|
from PySide6.QtGui import QTextCursor, QIcon
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QDialog,
|
QDialog,
|
||||||
QPlainTextEdit,
|
QPlainTextEdit,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
)
|
)
|
||||||
from PySide6.QtGui import QTextCursor, QIcon
|
|
||||||
|
|
||||||
from qt_ui.logging_handler import HookableInMemoryHandler
|
from qt_ui.logging_handler import HookableInMemoryHandler
|
||||||
|
|
||||||
@@ -50,12 +49,17 @@ class QLogsWindow(QDialog):
|
|||||||
|
|
||||||
self.appendLogSignal.connect(self.appendLog)
|
self.appendLogSignal.connect(self.appendLog)
|
||||||
|
|
||||||
self._logging_handler = None
|
try:
|
||||||
logger = logging.getLogger()
|
# This assumes that there's never more than one in memory handler. We don't
|
||||||
for handler in logger.handlers:
|
# configure more than one by default, but logging is customizable with
|
||||||
if isinstance(handler, HookableInMemoryHandler):
|
# resources/logging.yaml. If someone adds a second in-memory handler, only
|
||||||
self._logging_handler = handler
|
# the first one (in arbitrary order) will be shown.
|
||||||
break
|
self._logging_handler = next(
|
||||||
|
HookableInMemoryHandler.iter_registered_handlers()
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
self._logging_handler = None
|
||||||
|
|
||||||
if self._logging_handler is not None:
|
if self._logging_handler is not None:
|
||||||
self.textbox.setPlainText(self._logging_handler.log)
|
self.textbox.setPlainText(self._logging_handler.log)
|
||||||
self.textbox.moveCursor(QTextCursor.End)
|
self.textbox.moveCursor(QTextCursor.End)
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ class QNewPackageDialog(QPackageDialog):
|
|||||||
|
|
||||||
def on_cancel(self) -> None:
|
def on_cancel(self) -> None:
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
for flight in self.package_model.package.flights:
|
for flight in list(self.package_model.package.flights):
|
||||||
self.package_model.cancel_or_abort_flight(flight)
|
self.package_model.cancel_or_abort_flight(flight)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ class FlightMemberSelector(QSpinBox):
|
|||||||
def __init__(self, flight: Flight, parent: QWidget | None = None) -> None:
|
def __init__(self, flight: Flight, parent: QWidget | None = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.flight = flight
|
self.flight = flight
|
||||||
self.setMinimum(0)
|
self.setMinimum(1)
|
||||||
self.setMaximum(flight.count - 1)
|
self.setMaximum(flight.count)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def selected_member(self) -> FlightMember:
|
def selected_member(self) -> FlightMember:
|
||||||
return self.flight.roster.members[self.value()]
|
return self.flight.roster.members[self.value() - 1]
|
||||||
|
|
||||||
|
|
||||||
class QFlightPayloadTab(QFrame):
|
class QFlightPayloadTab(QFrame):
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class QFlightWaypointInfoBox(QGroupBox):
|
|||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self) -> None:
|
def init_ui(self) -> None:
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
x_pos_layout = QHBoxLayout()
|
x_pos_layout = QHBoxLayout()
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ class QFlightWaypointTab(QFrame):
|
|||||||
|
|
||||||
self.recreate_buttons.clear()
|
self.recreate_buttons.clear()
|
||||||
for task in self.package.target.mission_types(for_player=True):
|
for task in self.package.target.mission_types(for_player=True):
|
||||||
|
|
||||||
if (
|
if (
|
||||||
task == FlightType.AIR_ASSAULT
|
task == FlightType.AIR_ASSAULT
|
||||||
and not self.game.lua_plugin_manager.is_plugin_enabled("ctld")
|
and not self.game.lua_plugin_manager.is_plugin_enabled("ctld")
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ PREDEFINED_WAYPOINT_CATEGORIES = [
|
|||||||
|
|
||||||
|
|
||||||
class QPredefinedWaypointSelectionWindow(QDialog):
|
class QPredefinedWaypointSelectionWindow(QDialog):
|
||||||
|
|
||||||
# List of FlightWaypoint
|
# List of FlightWaypoint
|
||||||
waypoints_added = Signal(list)
|
waypoints_added = Signal(list)
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ class NewGameWizard(QtWidgets.QWizard):
|
|||||||
ov10a_bronco=self.field("ov10a_bronco"),
|
ov10a_bronco=self.field("ov10a_bronco"),
|
||||||
frenchpack=self.field("frenchpack"),
|
frenchpack=self.field("frenchpack"),
|
||||||
high_digit_sams=self.field("high_digit_sams"),
|
high_digit_sams=self.field("high_digit_sams"),
|
||||||
|
fa18efg=self.field("fa18efg"),
|
||||||
)
|
)
|
||||||
mod_settings.save_player_settings()
|
mod_settings.save_player_settings()
|
||||||
|
|
||||||
@@ -826,6 +827,10 @@ class GeneratorOptions(QtWidgets.QWizardPage):
|
|||||||
high_digit_sams.setChecked(mod_settings.high_digit_sams)
|
high_digit_sams.setChecked(mod_settings.high_digit_sams)
|
||||||
self.registerField("high_digit_sams", high_digit_sams)
|
self.registerField("high_digit_sams", high_digit_sams)
|
||||||
|
|
||||||
|
fa18efg = QtWidgets.QCheckBox()
|
||||||
|
fa18efg.setChecked(mod_settings.fa18efg)
|
||||||
|
self.registerField("fa18efg", fa18efg)
|
||||||
|
|
||||||
modHelpText = QtWidgets.QLabel(
|
modHelpText = QtWidgets.QLabel(
|
||||||
"<p>Select the mods you have installed. If your chosen factions support them, you'll be able to use these mods in your campaign.</p>"
|
"<p>Select the mods you have installed. If your chosen factions support them, you'll be able to use these mods in your campaign.</p>"
|
||||||
)
|
)
|
||||||
@@ -877,6 +882,13 @@ class GeneratorOptions(QtWidgets.QWizardPage):
|
|||||||
modLayout.addWidget(QtWidgets.QLabel("High Digit SAMs"), modLayout_row, 0)
|
modLayout.addWidget(QtWidgets.QLabel("High Digit SAMs"), modLayout_row, 0)
|
||||||
modLayout.addWidget(high_digit_sams, modLayout_row, 1)
|
modLayout.addWidget(high_digit_sams, modLayout_row, 1)
|
||||||
modSettingsGroup.setLayout(modLayout)
|
modSettingsGroup.setLayout(modLayout)
|
||||||
|
modLayout_row += 1
|
||||||
|
modLayout.addWidget(
|
||||||
|
QtWidgets.QLabel("F/A-18EFG Super Hornet (version 2.2.5)"), modLayout_row, 0
|
||||||
|
)
|
||||||
|
modLayout.addWidget(fa18efg, modLayout_row, 1)
|
||||||
|
modSettingsGroup.setLayout(modLayout)
|
||||||
|
modLayout_row += 1
|
||||||
|
|
||||||
mlayout = QVBoxLayout()
|
mlayout = QVBoxLayout()
|
||||||
mlayout.addWidget(generatorSettingsGroup)
|
mlayout.addWidget(generatorSettingsGroup)
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ class QLiberationPreferences(QFrame):
|
|||||||
self.edit_dcs_install_dir.setText(install_dir)
|
self.edit_dcs_install_dir.setText(install_dir)
|
||||||
|
|
||||||
def apply(self):
|
def apply(self):
|
||||||
|
|
||||||
print("Applying changes")
|
print("Applying changes")
|
||||||
self.saved_game_dir = self.edit_saved_game_dir.text()
|
self.saved_game_dir = self.edit_saved_game_dir.text()
|
||||||
self.dcs_install_dir = self.edit_dcs_install_dir.text()
|
self.dcs_install_dir = self.edit_dcs_install_dir.text()
|
||||||
|
|||||||
@@ -345,7 +345,6 @@ class QSettingsWindow(QDialog):
|
|||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def initCheatLayout(self):
|
def initCheatLayout(self):
|
||||||
|
|
||||||
self.cheatPage = QWidget()
|
self.cheatPage = QWidget()
|
||||||
self.cheatLayout = QVBoxLayout()
|
self.cheatLayout = QVBoxLayout()
|
||||||
self.cheatPage.setLayout(self.cheatLayout)
|
self.cheatPage.setLayout(self.cheatLayout)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class QAircraftChart(QFrame):
|
|||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def generateUnitCharts(self):
|
def generateUnitCharts(self):
|
||||||
|
|
||||||
self.alliedAircraft = [
|
self.alliedAircraft = [
|
||||||
d.allied_units.aircraft_count for d in self.game.game_stats.data_per_turn
|
d.allied_units.aircraft_count for d in self.game.game_stats.data_per_turn
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class QArmorChart(QFrame):
|
|||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def generateUnitCharts(self):
|
def generateUnitCharts(self):
|
||||||
|
|
||||||
self.alliedArmor = [
|
self.alliedArmor = [
|
||||||
d.allied_units.vehicles_count for d in self.game.game_stats.data_per_turn
|
d.allied_units.vehicles_count for d in self.game.game_stats.data_per_turn
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,66 +1,69 @@
|
|||||||
altgraph==0.17.3
|
altgraph==0.17.4
|
||||||
anyio==3.6.2
|
annotated-types==0.6.0
|
||||||
asgiref==3.6.0
|
anyio==3.7.1
|
||||||
attrs==22.2.0
|
asgiref==3.7.2
|
||||||
black==22.12.0
|
attrs==23.1.0
|
||||||
certifi==2023.7.22
|
black==24.3.0
|
||||||
cfgv==3.3.1
|
certifi==2023.11.17
|
||||||
click==8.1.3
|
cfgv==3.4.0
|
||||||
|
click==8.1.7
|
||||||
colorama==0.4.6
|
colorama==0.4.6
|
||||||
coverage==7.0.5
|
coverage==7.3.2
|
||||||
distlib==0.3.6
|
distlib==0.3.7
|
||||||
exceptiongroup==1.1.0
|
exceptiongroup==1.2.0
|
||||||
Faker==15.3.4
|
Faker==20.1.0
|
||||||
fastapi==0.95.2
|
fastapi==0.109.1
|
||||||
filelock==3.9.0
|
filelock==3.13.1
|
||||||
future==0.18.3
|
future==0.18.3
|
||||||
h11==0.14.0
|
h11==0.14.0
|
||||||
httptools==0.5.0
|
httptools==0.6.1
|
||||||
identify==2.5.11
|
identify==2.5.32
|
||||||
idna==3.4
|
idna==3.6
|
||||||
iniconfig==1.1.1
|
iniconfig==2.0.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.3
|
||||||
MarkupSafe==2.1.1
|
MarkupSafe==2.1.3
|
||||||
mypy==1.2.0
|
mypy==1.7.1
|
||||||
mypy-extensions==1.0.0
|
mypy-extensions==1.0.0
|
||||||
nodeenv==1.7.0
|
nodeenv==1.8.0
|
||||||
numpy==1.25.1
|
numpy==1.26.2
|
||||||
packaging==22.0
|
packaging==23.2
|
||||||
pathspec==0.10.3
|
pathspec==0.11.2
|
||||||
pefile==2022.5.30
|
pefile==2023.2.7
|
||||||
Pillow==10.0.1
|
Pillow==10.2.0
|
||||||
platformdirs==2.6.2
|
platformdirs==4.0.0
|
||||||
pluggy==1.0.0
|
pluggy==1.3.0
|
||||||
pre-commit==2.21.0
|
pre-commit==3.5.0
|
||||||
pydantic==1.10.7
|
pydantic==2.5.2
|
||||||
git+https://github.com/pydcs/dcs@f8232606a21eaef82af7ba78c2013403da4a86f5#egg=pydcs
|
pydantic-settings==2.1.0
|
||||||
pyinstaller==5.13.0
|
pydantic_core==2.14.5
|
||||||
|
pydcs @ git+https://github.com/zhexu14/dcs@bb41fa849e718fee1b97d5d7a7c2e417f78de3d8
|
||||||
|
pyinstaller==5.13.1
|
||||||
pyinstaller-hooks-contrib==2023.6
|
pyinstaller-hooks-contrib==2023.6
|
||||||
pyproj==3.4.1
|
pyproj==3.6.1
|
||||||
PySide6==6.4.1
|
PySide6==6.4.1
|
||||||
PySide6-Addons==6.4.1
|
PySide6-Addons==6.4.1
|
||||||
PySide6-Essentials==6.4.1
|
PySide6-Essentials==6.4.1
|
||||||
pytest==7.2.0
|
pytest==7.4.3
|
||||||
pytest-cov==4.0.0
|
pytest-cov==4.1.0
|
||||||
pytest-mock==3.10.0
|
pytest-mock==3.12.0
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
python-dotenv==0.21.0
|
python-dotenv==1.0.0
|
||||||
pywin32-ctypes==0.2.2
|
pywin32-ctypes==0.2.2
|
||||||
PyYAML==6.0
|
PyYAML==6.0.1
|
||||||
shapely==2.0.1
|
shapely==2.0.2
|
||||||
shiboken6==6.4.1
|
shiboken6==6.4.1
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
sniffio==1.3.0
|
sniffio==1.3.0
|
||||||
starlette==0.27.0
|
starlette==0.35.1
|
||||||
tabulate==0.9.0
|
tabulate==0.9.0
|
||||||
tomli==2.0.1
|
tomli==2.0.1
|
||||||
types-Jinja2==2.11.9
|
types-Jinja2==2.11.9
|
||||||
types-MarkupSafe==1.1.10
|
types-MarkupSafe==1.1.10
|
||||||
types-Pillow==9.3.0.4
|
types-Pillow==9.3.0.4
|
||||||
types-PyYAML==6.0.12.2
|
types-PyYAML==6.0.12.12
|
||||||
types-tabulate==0.9.0.0
|
types-tabulate==0.9.0.3
|
||||||
typing_extensions==4.4.0
|
typing_extensions==4.8.0
|
||||||
uvicorn==0.20.0
|
uvicorn==0.24.0.post1
|
||||||
virtualenv==20.17.1
|
virtualenv==20.24.7
|
||||||
watchfiles==0.18.1
|
watchfiles==0.21.0
|
||||||
websockets==10.4
|
websockets==12.0
|
||||||
|
|||||||
Binary file not shown.
@@ -1,14 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: Syria - The Falcon went over the mountain
|
name: Syria - The Falcon went over the mountain
|
||||||
theater: Syria
|
theater: Syria
|
||||||
authors: Sith1144
|
authors: Sith1144, updated by Astro
|
||||||
description: <p>Campaign about a task force attacking northern Syria from Incirlik. Culling recommended. Do you love SEAD? Know no greater joy in than showing SAMs who truly rules the skies? this is the campaign for you!</p>
|
description: <p>Campaign about a task force attacking northern Syria from Incirlik. Culling recommended. Do you love SEAD? Know no greater joy in than showing SAMs who truly rules the skies? this is the campaign for you!</p>
|
||||||
recommended_player_faction: USA 2005
|
recommended_player_faction: USA 2005
|
||||||
recommended_enemy_faction: Syria 2012'ish
|
recommended_enemy_faction: Syria 2012'ish
|
||||||
recommended_start_date: 2012-06-01
|
recommended_start_date: 2012-06-01
|
||||||
|
recommended_player_money: 400
|
||||||
|
recommended_enemy_money: 400
|
||||||
|
recommended_player_income_multiplier: 1.0
|
||||||
|
recommended_enemy_income_multiplier: 1.0
|
||||||
miz: TheFalconWentOverTheMountain.miz
|
miz: TheFalconWentOverTheMountain.miz
|
||||||
performance: 2
|
performance: 2
|
||||||
version: "10.4"
|
version: "11.0"
|
||||||
advanced_iads: true # Campaign has connection_nodes / power_sources / command_centers
|
advanced_iads: true # Campaign has connection_nodes / power_sources / command_centers
|
||||||
#IADS: EWR and C2 get power generators. batteries have their own generators.
|
#IADS: EWR and C2 get power generators. batteries have their own generators.
|
||||||
iads_config:
|
iads_config:
|
||||||
@@ -43,7 +47,7 @@ iads_config:
|
|||||||
- YellowEWRS: #mountainrange (center)
|
- YellowEWRS: #mountainrange (center)
|
||||||
- YellowPPW
|
- YellowPPW
|
||||||
- YellowControlW
|
- YellowControlW
|
||||||
- YellowEWRC: # internal
|
- YellowEWRC: #internal
|
||||||
- HamidiyeControl
|
- HamidiyeControl
|
||||||
- GaziantepControl
|
- GaziantepControl
|
||||||
- GaziantepPP
|
- GaziantepPP
|
||||||
@@ -243,94 +247,201 @@ iads_config:
|
|||||||
- Aleppo Control
|
- Aleppo Control
|
||||||
- Aleppo Control:
|
- Aleppo Control:
|
||||||
- Aleppo Power
|
- Aleppo Power
|
||||||
|
control_points:
|
||||||
|
From Reserves:
|
||||||
|
ferry_only: true
|
||||||
squadrons:
|
squadrons:
|
||||||
#Incirlik
|
#Incirlik (120)
|
||||||
16:
|
16:
|
||||||
- primary: BARCAP
|
- primary: BARCAP
|
||||||
|
secondary: air-to-air
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-15C Eagle
|
- F-15C Eagle
|
||||||
|
size: 12
|
||||||
- primary: SEAD
|
- primary: SEAD
|
||||||
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-16CM Fighting Falcon (Block 50)
|
- F-16CM Fighting Falcon (Block 50)
|
||||||
- primary: DEAD
|
size: 12
|
||||||
|
- primary: Strike
|
||||||
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-15E Strike Eagle
|
- F-15E Strike Eagle (Suite 4+)
|
||||||
- primary: BARCAP
|
size: 8
|
||||||
aircraft:
|
|
||||||
- F-16CM Fighting Falcon (Block 50)
|
|
||||||
- primary: CAS
|
|
||||||
aircraft:
|
|
||||||
- A-10C Thunderbolt II (Suite 3)
|
|
||||||
- primary: CAS
|
- primary: CAS
|
||||||
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- A-10C Thunderbolt II (Suite 7)
|
- A-10C Thunderbolt II (Suite 7)
|
||||||
|
size: 8
|
||||||
- primary: CAS
|
- primary: CAS
|
||||||
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- AH-64D Apache Longbow
|
- AH-64D Apache Longbow
|
||||||
|
size: 8
|
||||||
- primary: Strike
|
- primary: Strike
|
||||||
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-117A Nighthawk
|
- F-117A Nighthawk
|
||||||
|
size: 4
|
||||||
- primary: Strike
|
- primary: Strike
|
||||||
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- B-1B Lancer
|
- B-1B Lancer
|
||||||
|
size: 2
|
||||||
- primary: AEW&C
|
- primary: AEW&C
|
||||||
|
secondary: any
|
||||||
|
size: 1
|
||||||
- primary: Refueling
|
- primary: Refueling
|
||||||
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- KC-135 Stratotanker MPRS
|
- KC-135 Stratotanker MPRS
|
||||||
|
size: 1
|
||||||
#carrier
|
#carrier
|
||||||
Blue Carrier:
|
Blue Carrier:
|
||||||
- primary: BARCAP
|
- primary: BARCAP
|
||||||
|
secondary: air-to-air
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-14B Tomcat
|
- F-14B Tomcat
|
||||||
|
size: 12
|
||||||
- primary: BARCAP
|
- primary: BARCAP
|
||||||
aircraft:
|
|
||||||
- F-14B Tomcat
|
|
||||||
- primary: Strike
|
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F/A-18C Hornet (Lot 20)
|
- F/A-18C Hornet (Lot 20)
|
||||||
- primary: Strike
|
size: 12
|
||||||
|
- primary: AEW&C
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
size: 1
|
||||||
- F/A-18C Hornet (Lot 20)
|
|
||||||
- primary: Refueling
|
- primary: Refueling
|
||||||
|
secondary: any
|
||||||
|
size: 2
|
||||||
#LHA
|
#LHA
|
||||||
Blue LHA:
|
Blue LHA:
|
||||||
- primary: CAS
|
- primary: CAS
|
||||||
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- AV-8B Harrier II Night Attack
|
- AV-8B Harrier II Night Attack
|
||||||
#Abu Al-Duhur
|
size: 8
|
||||||
1:
|
#Ferry-only
|
||||||
- primary: BARCAP
|
From Reserves:
|
||||||
aircraft:
|
- primary: SEAD
|
||||||
- MiG-29S Fulcrum-C
|
secondary: any
|
||||||
- primary: BAI
|
aircraft:
|
||||||
aircraft:
|
- F-16CM Fighting Falcon (Block 50)
|
||||||
- Su-24M Fencer-D
|
size: 12
|
||||||
- primary: BARCAP
|
- primary: CAS
|
||||||
aircraft:
|
secondary: air-to-ground
|
||||||
- Su-30 Flanker-C
|
aircraft:
|
||||||
#Hatay
|
- A-10C Thunderbolt II (Suite 3)
|
||||||
|
size: 12
|
||||||
|
# REDFOR squadrons
|
||||||
|
# Smaller number of modern fighters in forward airfields (Hatay, Minakh and Gaziantep)
|
||||||
|
# Larger number of older fighters in the rear (Aleppo, Abu Al-Duhur and Jirah)
|
||||||
|
# CAS aircraft distributed over all airfields, helos more forward
|
||||||
|
# Aleppo is main airfield for AWACS, Refueling and Transport, for protection it has some modern fighters
|
||||||
|
#Hatay (10)
|
||||||
15:
|
15:
|
||||||
- primary: BARCAP
|
- primary: BARCAP
|
||||||
aircraft:
|
secondary: any
|
||||||
- MiG-23MLD Flogger-K
|
aircraft:
|
||||||
#Aleppo
|
- MiG-29S Fulcrum-C
|
||||||
27:
|
size: 4
|
||||||
- primary: AEW&C
|
|
||||||
- primary: Refueling
|
|
||||||
- primary: CAS
|
- primary: CAS
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Su-25 Frogfoot
|
||||||
|
size: 4
|
||||||
|
- primary: CAS
|
||||||
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- Mi-24P Hind-F
|
- Mi-24P Hind-F
|
||||||
- primary: Transport
|
size: 2
|
||||||
#Jirah
|
#Minakh (20)
|
||||||
17:
|
26:
|
||||||
|
- primary: BARCAP
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Su-30 Flanker-C
|
||||||
|
size: 8
|
||||||
- primary: SEAD
|
- primary: SEAD
|
||||||
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- Su-34 Fullback
|
- Su-34 Fullback
|
||||||
|
size: 4
|
||||||
- primary: Strike
|
- primary: Strike
|
||||||
#Gaziantep
|
secondary: any
|
||||||
11:
|
size: 4
|
||||||
- primary: CAS
|
- primary: CAS
|
||||||
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- Su-25 Frogfoot
|
- Su-25 Frogfoot
|
||||||
|
size: 4
|
||||||
|
#Gaziantep (12)
|
||||||
|
11:
|
||||||
|
- primary: BARCAP
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- MiG-29S Fulcrum-C
|
||||||
|
size: 4
|
||||||
|
- primary: CAS
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Su-25 Frogfoot
|
||||||
|
size: 4
|
||||||
|
- primary: Strike
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Su-24M Fencer-D
|
||||||
|
size: 4
|
||||||
|
#Aleppo (14)
|
||||||
|
27:
|
||||||
|
- primary: BARCAP
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- MiG-29S Fulcrum-C
|
||||||
|
size: 4
|
||||||
|
- primary: BARCAP
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- MiG-23MLD Flogger-K
|
||||||
|
size: 4
|
||||||
|
- primary: AEW&C
|
||||||
|
secondary: any
|
||||||
|
size: 1
|
||||||
|
- primary: Refueling
|
||||||
|
secondary: any
|
||||||
|
size: 1
|
||||||
|
- primary: Transport
|
||||||
|
secondary: any
|
||||||
|
size: 2
|
||||||
|
#Abu Al-Duhur (36)
|
||||||
|
1:
|
||||||
|
- primary: BARCAP
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- MiG-23MLD Flogger-K
|
||||||
|
size: 12
|
||||||
|
- primary: SEAD
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Su-34 Fullback
|
||||||
|
size: 8
|
||||||
|
- primary: Strike
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Su-24M Fencer-D
|
||||||
|
size: 8
|
||||||
|
#Kuweires (37) ID: 31
|
||||||
|
#Jirah (28)
|
||||||
|
17:
|
||||||
|
- primary: BARCAP
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- MiG-23MLD Flogger-K
|
||||||
|
size: 12
|
||||||
|
- primary: BAI
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Su-24M Fencer-D
|
||||||
|
size: 8
|
||||||
|
#
|
||||||
|
# air-to-air: Barcap, Tarcap, Escort, and Fighter Sweep
|
||||||
BIN
resources/campaigns/battle_for_no_mans_land.miz
Normal file
BIN
resources/campaigns/battle_for_no_mans_land.miz
Normal file
Binary file not shown.
70
resources/campaigns/battle_for_no_mans_land.yaml
Normal file
70
resources/campaigns/battle_for_no_mans_land.yaml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
name: Falklands - Battle for No Man's Land
|
||||||
|
theater: Falklands
|
||||||
|
authors: Starfire
|
||||||
|
recommended_player_faction: USA 2005
|
||||||
|
recommended_enemy_faction: Private Military Company - Russian (Hard)
|
||||||
|
description:
|
||||||
|
<p><strong>Note:</strong> This campaign was designed for helicopters.</p><p>
|
||||||
|
Set against the rugged and windswept backdrop of the Falkland Islands,
|
||||||
|
this fictional campaign scenario unfolds with a dramatic dawn sneak attack
|
||||||
|
on RAF Mount Pleasant Airbase. Orchestrated by a Russia-backed private
|
||||||
|
military company, the deadly offensive with helicopter gunships and ground troops
|
||||||
|
has left the airbase's runways in ruins and its defences obliterated. This brutal
|
||||||
|
incursion resulted in significant casualties among the RAF personnel, with many
|
||||||
|
killed or wounded in the unexpected onslaught. The carrier HMS Queen Elizabeth and
|
||||||
|
its task force are on their way to evacuate the survivors and retake Mount Pleasant.
|
||||||
|
However, they are eight days away at full steam.</p><p>
|
||||||
|
Amidst this chaos, a beacon of hope emerges in the heart of the Falklands. At Port
|
||||||
|
Stanley, a small detachment of US military personnel, including helicopter pilots
|
||||||
|
and armor units, find themselves inadvertently thrust into the fray. Originally at
|
||||||
|
Port Stanley for some R&R following a training exercise, these soldiers now face
|
||||||
|
an unexpected and urgent call to action. Their mission is daunting but clear - to
|
||||||
|
prevent the capture of Port Stanley and liberate East Falkland from the clutches
|
||||||
|
of the PMC forces.</p><p>
|
||||||
|
This small group must strategically destroy the PMC forces deployed around the treacherous
|
||||||
|
valley lying between Wickham Heights and the Onion Ranges, an area ominously known
|
||||||
|
as No Man's Land. Their plan involves a daring assault to destroy the enemy's
|
||||||
|
helicopter gunships stationed at San Carlos FOB. Following this, they aim to force
|
||||||
|
the PMC ground forces into a strategic retreat southward, along the 1.6 mile wide
|
||||||
|
isthmus into Lafonia. This offensive is designed to create a defensible position
|
||||||
|
at Goose Green on the narrow isthmus, which can be held against a numerically
|
||||||
|
superior force until the arrival of Big Lizzie.</p>
|
||||||
|
miz: battle_for_no_mans_land.miz
|
||||||
|
performance: 1
|
||||||
|
recommended_start_date: 2001-11-10
|
||||||
|
version: "11.0"
|
||||||
|
squadrons:
|
||||||
|
#Port Stanley
|
||||||
|
1:
|
||||||
|
- primary: DEAD
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- AH-64D Apache Longbow
|
||||||
|
size: 6
|
||||||
|
- primary: BAI
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- AH-64D Apache Longbow
|
||||||
|
size: 6
|
||||||
|
- primary: Air Assault
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- UH-60L
|
||||||
|
- UH-60A
|
||||||
|
size: 4
|
||||||
|
#San Carlos FOB
|
||||||
|
3:
|
||||||
|
- primary: BAI
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Mi-24P Hind-F
|
||||||
|
size: 6
|
||||||
|
#Goose Green
|
||||||
|
24:
|
||||||
|
- primary: DEAD
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- Ka-50 Hokum III
|
||||||
|
- Ka-50 Hokum (Blackshark 3)
|
||||||
|
size: 6
|
||||||
Binary file not shown.
@@ -3,17 +3,29 @@ name: Sinai - Exercise Bright Star
|
|||||||
theater: Sinai
|
theater: Sinai
|
||||||
authors: Starfire
|
authors: Starfire
|
||||||
recommended_player_faction: Bluefor Modern
|
recommended_player_faction: Bluefor Modern
|
||||||
recommended_enemy_faction: Egypt 2000s
|
recommended_enemy_faction: Egypt 2000
|
||||||
description:
|
description:
|
||||||
<p>For over 4 decades, the United States and Egypt have run a series of
|
<p>For over four decades, the United States and Egypt have conducted a series
|
||||||
biannual joint military exercises called Bright Star. Over the years, the
|
of biannual joint military exercises known as Bright Star. As the
|
||||||
number of participating countries has grown substantially. Exercise Bright
|
geopolitical landscape has transformed, so too has the scope and scale of
|
||||||
Star 2025 boasts 8 participant nations and 14 observer nations. The United
|
Exercise Bright Star. The exercise has grown over the years to incorporate
|
||||||
States and a portion of the exercise coalition will play the part of a
|
a wide array of international participants. The 2025 iteration of
|
||||||
fictional hostile nation dubbed Orangeland, staging a mock invasion against
|
Exercise Bright Star features eight participating nations alongside
|
||||||
Cairo. Israel, having for the first time accepted the invitation to observe,
|
fourteen observer nations.</p><p>
|
||||||
is hosting the aggressor faction of the exercise coalition at its
|
For the 2025 exercises, the United States, along with a select contingent
|
||||||
airfields.</p>
|
from the exercise coalition, will take on the role of a hypothetical
|
||||||
|
adversarial nation, dubbed Orangeland. This scenario is designed to
|
||||||
|
simulate a mock invasion against Cairo, and presents a valuable
|
||||||
|
opportunity for participating nations to refine their joint operational
|
||||||
|
capabilities and improve logistical and tactical interoperability</p><p>
|
||||||
|
A historic addition to Exercise Bright Star 2025 is the participation of
|
||||||
|
Israel as an observer nation. This marks a significant milestone, given
|
||||||
|
the complex historical relations in the region, and symbolises a step
|
||||||
|
forward in regional collaboration and military diplomacy. Israel's role,
|
||||||
|
hosting the aggressor faction of the exercise coalition at its airfields,
|
||||||
|
not only demonstrates the broadening scope of the exercise but also highlights
|
||||||
|
the value of fostering an environment of mutual cooperation and shared
|
||||||
|
security objectives.</p>
|
||||||
miz: exercise_bright_star.miz
|
miz: exercise_bright_star.miz
|
||||||
performance: 1
|
performance: 1
|
||||||
recommended_start_date: 2025-09-01
|
recommended_start_date: 2025-09-01
|
||||||
@@ -27,7 +39,7 @@ squadrons:
|
|||||||
size: 24
|
size: 24
|
||||||
- primary: AEW&C
|
- primary: AEW&C
|
||||||
aircraft:
|
aircraft:
|
||||||
- E-2C Hawkeye
|
- E-2D Advanced Hawkeye
|
||||||
size: 2
|
size: 2
|
||||||
- primary: Refueling
|
- primary: Refueling
|
||||||
aircraft:
|
aircraft:
|
||||||
@@ -46,6 +58,11 @@ squadrons:
|
|||||||
size: 8
|
size: 8
|
||||||
# Hatzerim (141)
|
# Hatzerim (141)
|
||||||
7:
|
7:
|
||||||
|
- primary: CAS
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- A-10C Thunderbolt II (Suite 7)
|
||||||
|
size: 6
|
||||||
- primary: Escort
|
- primary: Escort
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
@@ -55,12 +72,7 @@ squadrons:
|
|||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-15E Strike Eagle (Suite 4+)
|
- F-15E Strike Eagle (Suite 4+)
|
||||||
size: 8
|
size: 16
|
||||||
- primary: Strike
|
|
||||||
secondary: any
|
|
||||||
aircraft:
|
|
||||||
- F-15E Strike Eagle
|
|
||||||
size: 8
|
|
||||||
- primary: DEAD
|
- primary: DEAD
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
@@ -83,6 +95,11 @@ squadrons:
|
|||||||
aircraft:
|
aircraft:
|
||||||
- CH-47D
|
- CH-47D
|
||||||
size: 20
|
size: 20
|
||||||
|
- primary: BAI
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- AH-64D Apache Longbow
|
||||||
|
size: 8
|
||||||
- primary: Air Assault
|
- primary: Air Assault
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
@@ -90,25 +107,22 @@ squadrons:
|
|||||||
- UH-60A
|
- UH-60A
|
||||||
size: 4
|
size: 4
|
||||||
# Nevatim (106)
|
# Nevatim (106)
|
||||||
8:
|
# Nevatim temporarilly disabled because airfield is borked
|
||||||
- primary: AEW&C
|
# 8:
|
||||||
aircraft:
|
# - primary: AEW&C
|
||||||
- E-3A
|
# aircraft:
|
||||||
size: 2
|
# - E-3A
|
||||||
- primary: Refueling
|
# size: 2
|
||||||
aircraft:
|
# - primary: Refueling
|
||||||
- KC-135 Stratotanker
|
# aircraft:
|
||||||
size: 2
|
# - KC-135 Stratotanker
|
||||||
- primary: CAS
|
# size: 2
|
||||||
secondary: air-to-ground
|
|
||||||
aircraft:
|
|
||||||
- A-10C Thunderbolt II (Suite 7)
|
|
||||||
size: 8
|
|
||||||
# Melez (30)
|
# Melez (30)
|
||||||
5:
|
5:
|
||||||
- primary: CAS
|
- primary: CAS
|
||||||
secondary: air-to-ground
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
|
- Ka-50 Hokum III
|
||||||
- Ka-50 Hokum (Blackshark 3)
|
- Ka-50 Hokum (Blackshark 3)
|
||||||
size: 4
|
size: 4
|
||||||
- primary: BAI
|
- primary: BAI
|
||||||
|
|||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user