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