mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
68 Commits
develop-9.
...
update_rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef8eeeb1f1 | ||
|
|
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)
|
||||||
|
|
||||||
|
|||||||
27
changelog.md
27
changelog.md
@@ -1,3 +1,28 @@
|
|||||||
|
# 11.0.0
|
||||||
|
|
||||||
|
Saves from 10.x are not compatible with 11.0.0.
|
||||||
|
|
||||||
|
## Features/Improvements
|
||||||
|
|
||||||
|
* **[Engine]** Support for DCS 2.9.3.51704 Open Beta.
|
||||||
|
|
||||||
|
## Fixes
|
||||||
|
|
||||||
|
# 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 +71,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.
|
||||||
|
|||||||
12
client/package-lock.json
generated
12
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",
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,8 @@ 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.superhornet:
|
||||||
|
self.remove_aircraft("Super-Hornet")
|
||||||
# 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,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
|
||||||
|
|||||||
@@ -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,9 @@ 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:
|
||||||
|
return f"Flight deck {self.runway_status.describe()}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def runway_can_be_repaired(self) -> bool:
|
def runway_can_be_repaired(self) -> bool:
|
||||||
return False
|
return False
|
||||||
@@ -1428,6 +1438,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 +1487,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
|
||||||
|
superhornet: 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."""
|
||||||
|
|||||||
@@ -196,7 +196,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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -576,6 +577,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 +602,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"),
|
||||||
|
superhornet=self.field("superhornet"),
|
||||||
)
|
)
|
||||||
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)
|
||||||
|
|
||||||
|
superhornet = QtWidgets.QCheckBox()
|
||||||
|
superhornet.setChecked(mod_settings.superhornet)
|
||||||
|
self.registerField("superhornet", superhornet)
|
||||||
|
|
||||||
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,11 @@ 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("Super Hornet"), modLayout_row, 0)
|
||||||
|
modLayout.addWidget(superhornet, 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,53 +1,56 @@
|
|||||||
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==23.11.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.104.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/pydcs/dcs@7eeec23ea428846ebbbd0ea4c746f8eafea04e0d
|
||||||
|
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
|
||||||
@@ -57,10 +60,10 @@ 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.
69
resources/campaigns/battle_for_no_mans_land.yaml
Normal file
69
resources/campaigns/battle_for_no_mans_land.yaml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
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 push the PMC forces back through 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 calculated 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: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- AH-64D Apache Longbow
|
||||||
|
size: 6
|
||||||
|
- primary: BAI
|
||||||
|
secondary: air-to-ground
|
||||||
|
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: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- Mi-24P Hind-F
|
||||||
|
size: 6
|
||||||
|
#Goose Green
|
||||||
|
24:
|
||||||
|
- primary: DEAD
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- Ka-50 Hokum (Blackshark 3)
|
||||||
|
size: 6
|
||||||
@@ -1,160 +1,155 @@
|
|||||||
---
|
---
|
||||||
name: Sinai - Exercise Bright Star
|
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 2000s
|
||||||
description:
|
description:
|
||||||
<p>For over 4 decades, the United States and Egypt have run a series of
|
<p>For over 4 decades, the United States and Egypt have run a series of
|
||||||
biannual joint military exercises called Bright Star. Over the years, the
|
biannual joint military exercises called Bright Star. Over the years, the
|
||||||
number of participating countries has grown substantially. Exercise Bright
|
number of participating countries has grown substantially. Exercise Bright
|
||||||
Star 2025 boasts 8 participant nations and 14 observer nations. The United
|
Star 2025 boasts 8 participant nations and 14 observer nations. The United
|
||||||
States and a portion of the exercise coalition will play the part of a
|
States and a portion of the exercise coalition will play the part of a
|
||||||
fictional hostile nation dubbed Orangeland, staging a mock invasion against
|
fictional hostile nation dubbed Orangeland, staging a mock invasion against
|
||||||
Cairo. Israel, having for the first time accepted the invitation to observe,
|
Cairo. Israel, having for the first time accepted the invitation to observe,
|
||||||
is hosting the aggressor faction of the exercise coalition at its
|
is hosting the aggressor faction of the exercise coalition at its
|
||||||
airfields.</p>
|
airfields.</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
|
||||||
version: "11.0"
|
version: "11.0"
|
||||||
squadrons:
|
squadrons:
|
||||||
Blue CV-1:
|
Blue CV-1:
|
||||||
- primary: SEAD
|
- primary: SEAD
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F/A-18C Hornet (Lot 20)
|
- F/A-18C Hornet (Lot 20)
|
||||||
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:
|
||||||
- S-3B Tanker
|
- S-3B Tanker
|
||||||
size: 4
|
size: 4
|
||||||
Bombers from RAF Fairford:
|
Bombers from RAF Fairford:
|
||||||
- primary: Anti-ship
|
- primary: Anti-ship
|
||||||
secondary: air-to-ground
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- B-52H Stratofortress
|
- B-52H Stratofortress
|
||||||
size: 8
|
size: 8
|
||||||
- primary: Strike
|
- primary: Strike
|
||||||
secondary: air-to-ground
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- B-1B Lancer
|
- B-1B Lancer
|
||||||
size: 8
|
size: 8
|
||||||
# Hatzerim (141)
|
# Hatzerim (141)
|
||||||
7:
|
7:
|
||||||
- primary: Escort
|
- primary: CAS
|
||||||
secondary: any
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-15C Eagle
|
- A-10C Thunderbolt II (Suite 7)
|
||||||
size: 20
|
size: 6
|
||||||
- primary: OCA/Runway
|
- primary: Escort
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-15E Strike Eagle (Suite 4+)
|
- F-15C Eagle
|
||||||
size: 8
|
size: 20
|
||||||
- primary: Strike
|
- primary: OCA/Runway
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-15E Strike Eagle
|
- F-15E Strike Eagle (Suite 4+)
|
||||||
size: 8
|
size: 16
|
||||||
- primary: DEAD
|
- primary: DEAD
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-16CM Fighting Falcon (Block 50)
|
- F-16CM Fighting Falcon (Block 50)
|
||||||
size: 20
|
size: 20
|
||||||
- primary: BAI
|
- primary: BAI
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- JF-17 Thunder
|
- JF-17 Thunder
|
||||||
size: 16
|
size: 16
|
||||||
- primary: BARCAP
|
- primary: BARCAP
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- Mirage 2000C
|
- Mirage 2000C
|
||||||
size: 12
|
size: 12
|
||||||
# Kedem
|
# Kedem
|
||||||
12:
|
12:
|
||||||
- primary: Transport
|
- primary: Transport
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- CH-47D
|
- CH-47D
|
||||||
size: 20
|
size: 20
|
||||||
- primary: Air Assault
|
- primary: Air Assault
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- UH-60L
|
- UH-60L
|
||||||
- UH-60A
|
- UH-60A
|
||||||
size: 4
|
size: 4
|
||||||
# Nevatim (106)
|
# Nevatim (106)
|
||||||
8:
|
# 8:
|
||||||
- primary: AEW&C
|
# - primary: AEW&C
|
||||||
aircraft:
|
# aircraft:
|
||||||
- E-3A
|
# - E-3A
|
||||||
size: 2
|
# size: 2
|
||||||
- primary: Refueling
|
# - primary: Refueling
|
||||||
aircraft:
|
# aircraft:
|
||||||
- KC-135 Stratotanker
|
# - KC-135 Stratotanker
|
||||||
size: 2
|
# size: 2
|
||||||
- primary: CAS
|
# Melez (30)
|
||||||
secondary: air-to-ground
|
5:
|
||||||
aircraft:
|
- primary: CAS
|
||||||
- A-10C Thunderbolt II (Suite 7)
|
secondary: air-to-ground
|
||||||
size: 8
|
aircraft:
|
||||||
# Melez (30)
|
- Ka-50 Hokum (Blackshark 3)
|
||||||
5:
|
size: 4
|
||||||
- primary: CAS
|
- primary: BAI
|
||||||
secondary: air-to-ground
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- Ka-50 Hokum (Blackshark 3)
|
- Mirage 2000C
|
||||||
size: 4
|
size: 12
|
||||||
- primary: BAI
|
- primary: Escort
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- Mirage 2000C
|
- MiG-21bis Fishbed-N
|
||||||
size: 12
|
size: 12
|
||||||
- primary: Escort
|
# Wadi al Jandali (72)
|
||||||
secondary: any
|
13:
|
||||||
aircraft:
|
- primary: AEW&C
|
||||||
- MiG-21bis Fishbed-N
|
aircraft:
|
||||||
size: 12
|
- E-2C Hawkeye
|
||||||
# Wadi al Jandali (72)
|
size: 2
|
||||||
13:
|
- primary: SEAD
|
||||||
- primary: AEW&C
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- E-2C Hawkeye
|
- F-4E Phantom II
|
||||||
size: 2
|
size: 20
|
||||||
- primary: SEAD
|
- primary: DEAD
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-4E Phantom II
|
- F-16CM Fighting Falcon (Block 50)
|
||||||
size: 20
|
size: 20
|
||||||
- primary: DEAD
|
- primary: Air Assault
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-16CM Fighting Falcon (Block 50)
|
- Mi-24P Hind-F
|
||||||
size: 20
|
size: 4
|
||||||
- primary: Air Assault
|
- primary: OCA/Aircraft
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- Mi-24P Hind-F
|
- SA 342L Gazelle
|
||||||
size: 4
|
size: 4
|
||||||
- primary: OCA/Aircraft
|
# Cairo West (95)
|
||||||
secondary: any
|
18:
|
||||||
aircraft:
|
- primary: Transport
|
||||||
- SA 342L Gazelle
|
aircraft:
|
||||||
size: 4
|
- C-130
|
||||||
# Cairo West (95)
|
size: 8
|
||||||
18:
|
- primary: BARCAP
|
||||||
- primary: Transport
|
secondary: air-to-air
|
||||||
aircraft:
|
aircraft:
|
||||||
- C-130
|
- MiG-29S Fulcrum-C
|
||||||
size: 8
|
|
||||||
- primary: BARCAP
|
|
||||||
secondary: air-to-air
|
|
||||||
aircraft:
|
|
||||||
- MiG-29S Fulcrum-C
|
|
||||||
size: 20
|
size: 20
|
||||||
Binary file not shown.
@@ -17,17 +17,29 @@ performance: 1
|
|||||||
recommended_start_date: 2011-02-24
|
recommended_start_date: 2011-02-24
|
||||||
version: "11.0"
|
version: "11.0"
|
||||||
squadrons:
|
squadrons:
|
||||||
|
Bombers from Minot AFB:
|
||||||
|
- primary: Strike
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- B-52H Stratofortress
|
||||||
|
size: 4
|
||||||
|
Bombers from Ellsworth AFB:
|
||||||
|
- primary: OCA/Runway
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- B-1B Lancer
|
||||||
|
size: 4
|
||||||
# Tonopah Airport
|
# Tonopah Airport
|
||||||
17:
|
17:
|
||||||
- primary: TARCAP
|
- primary: TARCAP
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-15E Strike Eagle (Suite 4+)
|
- F-15C Eagle
|
||||||
size: 12
|
size: 12
|
||||||
- primary: Strike
|
- primary: Strike
|
||||||
secondary: air-to-ground
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- F-15E Strike Eagle
|
- F-15E Strike Eagle (Suite 4+)
|
||||||
size: 12
|
size: 12
|
||||||
- primary: AEW&C
|
- primary: AEW&C
|
||||||
aircraft:
|
aircraft:
|
||||||
@@ -58,7 +70,8 @@ squadrons:
|
|||||||
- primary: Air Assault
|
- primary: Air Assault
|
||||||
secondary: air-to-ground
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- UH-1H Iroquois
|
- UH-60L
|
||||||
|
- UH-60A
|
||||||
size: 2
|
size: 2
|
||||||
# Groom Lake
|
# Groom Lake
|
||||||
2:
|
2:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ recommended_player_faction:
|
|||||||
- F-14B Tomcat
|
- F-14B Tomcat
|
||||||
- F/A-18C Hornet (Lot 20)
|
- F/A-18C Hornet (Lot 20)
|
||||||
- S-3B Viking
|
- S-3B Viking
|
||||||
|
- UH-60L
|
||||||
- UH-60A
|
- UH-60A
|
||||||
awacs:
|
awacs:
|
||||||
- E-2C Hawkeye
|
- E-2C Hawkeye
|
||||||
@@ -107,8 +108,9 @@ squadrons:
|
|||||||
- primary: Air Assault
|
- primary: Air Assault
|
||||||
secondary: any
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
|
- UH-60L
|
||||||
- UH-60A
|
- UH-60A
|
||||||
size: 4
|
size: 6
|
||||||
#Stoney Cross (39)
|
#Stoney Cross (39)
|
||||||
58:
|
58:
|
||||||
- primary: OCA/Runway
|
- primary: OCA/Runway
|
||||||
|
|||||||
Binary file not shown.
@@ -35,11 +35,6 @@ squadrons:
|
|||||||
aircraft:
|
aircraft:
|
||||||
- F-15C Eagle
|
- F-15C Eagle
|
||||||
size: 8
|
size: 8
|
||||||
- primary: BAI
|
|
||||||
secondary: air-to-ground
|
|
||||||
aircraft:
|
|
||||||
- F-15E Strike Eagle
|
|
||||||
size: 8
|
|
||||||
- primary: Refueling
|
- primary: Refueling
|
||||||
aircraft:
|
aircraft:
|
||||||
- KC-135 Stratotanker
|
- KC-135 Stratotanker
|
||||||
@@ -90,12 +85,12 @@ squadrons:
|
|||||||
- AV-8B Harrier II Night Attack
|
- AV-8B Harrier II Night Attack
|
||||||
size: 18
|
size: 18
|
||||||
- primary: Air Assault
|
- primary: Air Assault
|
||||||
secondary: air-to-ground
|
secondary: any
|
||||||
aircraft:
|
aircraft:
|
||||||
- UH-1H Iroquois
|
- UH-1H Iroquois
|
||||||
size: 2
|
size: 2
|
||||||
Bombers from Edwards AFB:
|
Bombers from Edwards AFB:
|
||||||
- primary: DEAD
|
- primary: Strike
|
||||||
secondary: air-to-ground
|
secondary: air-to-ground
|
||||||
aircraft:
|
aircraft:
|
||||||
- B-52H Stratofortress
|
- B-52H Stratofortress
|
||||||
|
|||||||
BIN
resources/campaigns/operation_aegean_aegis.miz
Normal file
BIN
resources/campaigns/operation_aegean_aegis.miz
Normal file
Binary file not shown.
100
resources/campaigns/operation_aegean_aegis.yaml
Normal file
100
resources/campaigns/operation_aegean_aegis.yaml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
name: Syria - Operation Aegean Aegis
|
||||||
|
theater: Syria
|
||||||
|
authors: Starfire
|
||||||
|
recommended_player_faction: USA 2005
|
||||||
|
recommended_enemy_faction: Turkey 2005
|
||||||
|
description:
|
||||||
|
<p><strong>Note:</strong> This fictional campaign was designed for the Apache
|
||||||
|
and Harrier. It requires manual flight planning. While enemy aircraft are present
|
||||||
|
at airfields, there will be no enemy flights as their aircraft are grounded.</p>
|
||||||
|
<p>
|
||||||
|
In a sudden and alarming escalation of tensions in Cyprus, the Anatolian Order,
|
||||||
|
a North Cypriot insurgent faction, has carried out a bold night-time assault on
|
||||||
|
three crucial airfields in the Republic of Cyprus; Paphos, Akrotiri, and Larnaca.
|
||||||
|
The insurgents, equipped with stolen surplus Turkish military hardware, used
|
||||||
|
chemical weapons in their attack, forcing the evacuation of all three airfields.
|
||||||
|
Notably, the capture of Akrotiri, a British Overseas Territory hosting a Royal
|
||||||
|
Air Force base, has drawn significant international attention and concern.</p>
|
||||||
|
<p>
|
||||||
|
The EU has strongly condemned this unprovoked attack against one of its member
|
||||||
|
states. Turkey, despite its historical connections with North Cyprus, has also
|
||||||
|
denounced the Anatolian Order's actions and is investigating how its aircraft,
|
||||||
|
ground vehicles, and weaponry held in storage ended up in insurgent hands.</p>
|
||||||
|
<p>
|
||||||
|
Amidst the crisis, a lone US Navy LHA, strategically positioned in the Aegean Sea,
|
||||||
|
is preparing a response to the crisis. Its mission is to deploy Apache helicopters
|
||||||
|
to neutralise the hastily erected air defenses around the captured airfields, before
|
||||||
|
Harrier jumpjets neutralise the Anatolian Order's aircraft. These aircraft, a
|
||||||
|
selection of mothballed Turkish F-4s and helicopters, are currently grounded due to
|
||||||
|
lack of suitable fuel and spare parts. It is imperative that they are dealt with
|
||||||
|
swiftly before ground troops are air-lifted in to reclaim the airfields.</p>
|
||||||
|
<p>
|
||||||
|
The operation's final phase involves targeting North Cyprus's only airport at Ercan.
|
||||||
|
The plan is to bomb its runway, preventing any further airborne reinforcements by the
|
||||||
|
insurgents. However, due to the air defenses established along the northern edge of
|
||||||
|
the Green Line (the UN-patrolled demilitarised zone) there are strict advisories against
|
||||||
|
overflying North Cyprus unless absolutely necessary, to minimise the risk of losses.
|
||||||
|
</p>
|
||||||
|
miz: operation_aegean_aegis.miz
|
||||||
|
performance: 1
|
||||||
|
recommended_start_date: 2017-04-20
|
||||||
|
recommended_player_money: 1000
|
||||||
|
recommended_enemy_money: 0
|
||||||
|
recommended_player_income_multiplier: 1.0
|
||||||
|
recommended_enemy_income_multiplier: 0.0
|
||||||
|
version: "11.0"
|
||||||
|
squadrons:
|
||||||
|
#Tarawa Class LHA
|
||||||
|
Blue-LHA:
|
||||||
|
- primary: DEAD
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- AH-64D Apache Longbow
|
||||||
|
size: 12
|
||||||
|
- primary: DEAD
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- AV-8B Harrier II Night Attack
|
||||||
|
size: 6
|
||||||
|
- primary: Air Assault
|
||||||
|
secondary: any
|
||||||
|
aircraft:
|
||||||
|
- UH-1H Iroquois
|
||||||
|
size: 2
|
||||||
|
#Paphos
|
||||||
|
46:
|
||||||
|
- primary: DEAD
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- F-4E Phantom II
|
||||||
|
size: 12
|
||||||
|
#Akrotiri
|
||||||
|
44:
|
||||||
|
- primary: BAI
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- AH-1W SuperCobra
|
||||||
|
size: 4
|
||||||
|
- primary: CAS
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- OH-58D Kiowa Warrior
|
||||||
|
size: 4
|
||||||
|
- primary: Air Assault
|
||||||
|
secondary: air-to-ground
|
||||||
|
aircraft:
|
||||||
|
- UH-60A
|
||||||
|
size: 4
|
||||||
|
#Larnaca
|
||||||
|
47:
|
||||||
|
- primary: Transport
|
||||||
|
aircraft:
|
||||||
|
- C-130
|
||||||
|
size: 6
|
||||||
|
#Ercan
|
||||||
|
49:
|
||||||
|
- primary: Transport
|
||||||
|
aircraft:
|
||||||
|
- CH-47D
|
||||||
|
size: 6
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user