Merge branch 'develop' into new_frontline

This commit is contained in:
walterroach 2020-11-16 16:25:21 -06:00
commit 658d808524
26 changed files with 785 additions and 119 deletions

View File

@ -44,7 +44,7 @@ jobs:
- name: update build number - name: update build number
run: | run: |
[IO.File]::WriteAllLines($pwd.path + "\buildnumber", $env:GITHUB_RUN_NUMBER) [IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
- name: Build binaries - name: Build binaries
run: | run: |

View File

@ -29,6 +29,10 @@ jobs:
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead # For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
- name: Finalize version
run: |
New-Item -ItemType file resources\final
- name: mypy game - name: mypy game
run: | run: |
./venv/scripts/activate ./venv/scripts/activate

View File

@ -1,4 +1,19 @@
# 2.2.X # 2.3.0
# Features/Improvements
* **[Flight Planner]** Added fighter sweep missions.
* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package.
# 2.2.1
# Features/Improvements
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
* **[Factions]** Added map Persian Gulf full by Plob
## Fixes :
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
# 2.2.0
## Features/Improvements : ## Features/Improvements :
* **[Campaign Generator]** Added early warning radar generation * **[Campaign Generator]** Added early warning radar generation
@ -13,10 +28,9 @@
* **[Map]** Highlight the selected flight path on the map * **[Map]** Highlight the selected flight path on the map
* **[Map]** Improved SAM display settings * **[Map]** Improved SAM display settings
* **[Map]** Improved flight plan display settings * **[Map]** Improved flight plan display settings
* **[Map]** Caucasus and The Channel map use a new system to generate SAM and strike target location to reduce probability of targets generated in the middle of a forests
* **[Misc]** Flexible Dedicated Hosting Options for Mission Files via environment variables * **[Misc]** Flexible Dedicated Hosting Options for Mission Files via environment variables
* **[Moddability]** Custom campaigns can be designed through json files * **[Moddability]** Custom campaigns can be designed through json files
* **[Moddability]** Custom campaigns can be designed through json files
* **[Moddability]** Custom factions can be designed through json files
* **[Moddability]** LUA plugins can now be injected into Liberation missions. * **[Moddability]** LUA plugins can now be injected into Liberation missions.
* **[Moddability]** Optional Skynet IADS lua plugin now included * **[Moddability]** Optional Skynet IADS lua plugin now included
* **[New Game]** Starting budget can be freely selected * **[New Game]** Starting budget can be freely selected
@ -25,8 +39,7 @@
* **[UI]** Add polygon drawing mode for map background * **[UI]** Add polygon drawing mode for map background
* **[UI]** Added a warning if you press takeoff with no player enabled flights * **[UI]** Added a warning if you press takeoff with no player enabled flights
* **[UI]** Packages and flights now visible in the main window sidebar * **[UI]** Packages and flights now visible in the main window sidebar
* **[Units/Factions]** Added bombers to coalitions * **[Units/Factions]** Added bombers to some coalitions
* **[Units/Factions]** Added frenchpack mod units
* **[Units/Factions]** Added support for SU-57 mod by Cubanace * **[Units/Factions]** Added support for SU-57 mod by Cubanace
* **[Units]** Added Freya EWR sites to german WW2 factions * **[Units]** Added Freya EWR sites to german WW2 factions
* **[Units]** Added support for many bombers (B-52H, B-1B, Tu-22, Tu-142) * **[Units]** Added support for many bombers (B-52H, B-1B, Tu-22, Tu-142)

View File

@ -16,6 +16,8 @@ class Doctrine:
sead_max_range: int sead_max_range: int
rendezvous_altitude: int rendezvous_altitude: int
hold_distance: int
push_distance: int
join_distance: int join_distance: int
split_distance: int split_distance: int
ingress_egress_distance: int ingress_egress_distance: int
@ -34,6 +36,8 @@ class Doctrine:
cas_duration: timedelta cas_duration: timedelta
sweep_distance: int
MODERN_DOCTRINE = Doctrine( MODERN_DOCTRINE = Doctrine(
cap=True, cap=True,
@ -44,6 +48,8 @@ MODERN_DOCTRINE = Doctrine(
strike_max_range=1500000, strike_max_range=1500000,
sead_max_range=1500000, sead_max_range=1500000,
rendezvous_altitude=feet_to_meter(25000), rendezvous_altitude=feet_to_meter(25000),
hold_distance=nm_to_meter(15),
push_distance=nm_to_meter(20),
join_distance=nm_to_meter(20), join_distance=nm_to_meter(20),
split_distance=nm_to_meter(20), split_distance=nm_to_meter(20),
ingress_egress_distance=nm_to_meter(45), ingress_egress_distance=nm_to_meter(45),
@ -58,6 +64,7 @@ MODERN_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(10), cap_min_distance_from_cp=nm_to_meter(10),
cap_max_distance_from_cp=nm_to_meter(40), cap_max_distance_from_cp=nm_to_meter(40),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(60),
) )
COLDWAR_DOCTRINE = Doctrine( COLDWAR_DOCTRINE = Doctrine(
@ -69,6 +76,8 @@ COLDWAR_DOCTRINE = Doctrine(
strike_max_range=1500000, strike_max_range=1500000,
sead_max_range=1500000, sead_max_range=1500000,
rendezvous_altitude=feet_to_meter(22000), rendezvous_altitude=feet_to_meter(22000),
hold_distance=nm_to_meter(10),
push_distance=nm_to_meter(10),
join_distance=nm_to_meter(10), join_distance=nm_to_meter(10),
split_distance=nm_to_meter(10), split_distance=nm_to_meter(10),
ingress_egress_distance=nm_to_meter(30), ingress_egress_distance=nm_to_meter(30),
@ -83,6 +92,7 @@ COLDWAR_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(8), cap_min_distance_from_cp=nm_to_meter(8),
cap_max_distance_from_cp=nm_to_meter(25), cap_max_distance_from_cp=nm_to_meter(25),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(40),
) )
WWII_DOCTRINE = Doctrine( WWII_DOCTRINE = Doctrine(
@ -93,6 +103,8 @@ WWII_DOCTRINE = Doctrine(
antiship=True, antiship=True,
strike_max_range=1500000, strike_max_range=1500000,
sead_max_range=1500000, sead_max_range=1500000,
hold_distance=nm_to_meter(5),
push_distance=nm_to_meter(5),
join_distance=nm_to_meter(5), join_distance=nm_to_meter(5),
split_distance=nm_to_meter(5), split_distance=nm_to_meter(5),
rendezvous_altitude=feet_to_meter(10000), rendezvous_altitude=feet_to_meter(10000),
@ -108,4 +120,5 @@ WWII_DOCTRINE = Doctrine(
cap_min_distance_from_cp=nm_to_meter(0), cap_min_distance_from_cp=nm_to_meter(0),
cap_max_distance_from_cp=nm_to_meter(5), cap_max_distance_from_cp=nm_to_meter(5),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nm_to_meter(10),
) )

View File

@ -1,8 +1,18 @@
from pathlib import Path from pathlib import Path
def _build_version_string() -> str:
components = ["2.3.0"]
build_number_path = Path("resources/buildnumber")
if build_number_path.exists():
with build_number_path.open("r") as build_number_file:
components.append(build_number_file.readline())
if not Path("resources/final").exists():
components.append("preview")
return "-".join(components)
#: Current version of Liberation. #: Current version of Liberation.
VERSION = "2.2.0-preview" VERSION = _build_version_string()
if Path("buildnumber").exists():
with open("buildnumber", "r") as file:
VERSION += f"-{file.readline()}"

View File

@ -5,7 +5,7 @@ import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import Dict, List, Optional, Type, Union, TYPE_CHECKING from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
from dcs import helicopters from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup from dcs.action import AITaskPush, ActivateGroup
@ -13,10 +13,12 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country from dcs.country import Country
from dcs.flyingunit import FlyingUnit from dcs.flyingunit import FlyingUnit
from dcs.helicopters import UH_1H, helicopter_map from dcs.helicopters import UH_1H, helicopter_map
from dcs.mapping import Point
from dcs.mission import Mission, StartType from dcs.mission import Mission, StartType
from dcs.planes import ( from dcs.planes import (
AJS37, AJS37,
B_17G, B_17G,
B_52H,
Bf_109K_4, Bf_109K_4,
FW_190A8, FW_190A8,
FW_190D9, FW_190D9,
@ -31,7 +33,8 @@ from dcs.planes import (
P_51D_30_NA, P_51D_30_NA,
SpitfireLFMkIX, SpitfireLFMkIX,
SpitfireLFMkIXCW, SpitfireLFMkIXCW,
Su_33, A_20G, Tu_22M3, B_52H, Su_33,
Tu_22M3,
) )
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.task import ( from dcs.task import (
@ -49,10 +52,8 @@ from dcs.task import (
OptRTBOnBingoFuel, OptRTBOnBingoFuel,
OptRTBOnOutOfAmmo, OptRTBOnOutOfAmmo,
OptReactOnThreat, OptReactOnThreat,
OptRestrictAfterburner,
OptRestrictJettison, OptRestrictJettison,
OrbitAction, OrbitAction,
PinpointStrike,
SEAD, SEAD,
StartCommand, StartCommand,
Targets, Targets,
@ -71,6 +72,7 @@ from game.utils import nm_to_meter
from gen.airsupportgen import AirSupport from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit from gen.callsigns import create_group_callsign_from_unit
from gen.conflictgen import FRONTLINE_LENGTH
from gen.flights.flight import ( from gen.flights.flight import (
Flight, Flight,
FlightType, FlightType,
@ -79,15 +81,14 @@ from gen.flights.flight import (
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData from gen.runways import RunwayData
from gen.conflictgen import FRONTLINE_LENGTH
from dcs.mapping import Point
from theater import TheaterGroundObject from theater import TheaterGroundObject
from theater.controlpoint import ControlPoint, ControlPointType from theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict from .conflictgen import Conflict
from .flights.flightplan import ( from .flights.flightplan import (
CasFlightPlan, CasFlightPlan,
FormationFlightPlan, LoiterFlightPlan,
PatrollingFlightPlan, PatrollingFlightPlan,
SweepFlightPlan,
) )
from .flights.traveltime import TotEstimator from .flights.traveltime import TotEstimator
from .naming import namegen from .naming import namegen
@ -1035,9 +1036,6 @@ class AircraftConflictGenerator:
self.configure_behavior(group, rtb_winchester=ammo_type) self.configure_behavior(group, rtb_winchester=ammo_type)
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50),
targets=[Targets.All.Air]))
def configure_cas(self, group: FlyingGroup, package: Package, def configure_cas(self, group: FlyingGroup, package: Package,
flight: Flight, flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
@ -1118,7 +1116,7 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData]) -> None: dynamic_runways: Dict[str, RunwayData]) -> None:
flight_type = flight.flight_type flight_type = flight.flight_type
if flight_type in [FlightType.BARCAP, FlightType.TARCAP, if flight_type in [FlightType.BARCAP, FlightType.TARCAP,
FlightType.INTERCEPTION]: FlightType.INTERCEPTION, FlightType.SWEEP]:
self.configure_cap(group, package, flight, dynamic_runways) self.configure_cap(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]: elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways) self.configure_cas(group, package, flight, dynamic_runways)
@ -1278,6 +1276,7 @@ class PydcsWaypointBuilder:
FlightWaypointType.LANDING_POINT: LandingPointBuilder, FlightWaypointType.LANDING_POINT: LandingPointBuilder,
FlightWaypointType.LOITER: HoldPointBuilder, FlightWaypointType.LOITER: HoldPointBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder, FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.INGRESS_SWEEP: SweepIngressBuilder,
} }
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(waypoint, group, package, flight, mission) return builder(waypoint, group, package, flight, mission)
@ -1314,7 +1313,7 @@ class HoldPointBuilder(PydcsWaypointBuilder):
altitude=waypoint.alt, altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.Circle pattern=OrbitAction.OrbitPattern.Circle
)) ))
if not isinstance(self.flight.flight_plan, FormationFlightPlan): if not isinstance(self.flight.flight_plan, LoiterFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__ flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error( logging.error(
f"Cannot configure hold for for {self.flight} because " f"Cannot configure hold for for {self.flight} because "
@ -1458,6 +1457,24 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
return waypoint return waypoint
class SweepIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
if not isinstance(self.flight.flight_plan, SweepFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error(
f"Cannot create sweep for {self.flight} because "
f"{flight_plan_type} is not a sweep flight plan.")
return waypoint
waypoint.tasks.append(EngageTargets(
max_distance=nm_to_meter(50),
targets=[Targets.All.Air.Planes.Fighters]))
return waypoint
class JoinPointBuilder(PydcsWaypointBuilder): class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
@ -1532,4 +1549,14 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack.stop_after_time( racetrack.stop_after_time(
int(self.flight.flight_plan.patrol_end_time.total_seconds())) int(self.flight.flight_plan.patrol_end_time.total_seconds()))
waypoint.add_task(racetrack) waypoint.add_task(racetrack)
# TODO: Move the properties of this task into the flight plan?
# CAP is the only current user of this so it's not a big deal, but might
# be good to make this usable for things like BAI when we add that
# later.
cap_types = {FlightType.BARCAP, FlightType.TARCAP}
if self.flight.flight_type in cap_types:
waypoint.tasks.append(EngageTargets(max_distance=nm_to_meter(50),
targets=[Targets.All.Air]))
return waypoint return waypoint

View File

@ -159,6 +159,7 @@ class Package:
FlightType.TARCAP, FlightType.TARCAP,
FlightType.CAP, FlightType.CAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.SWEEP,
FlightType.EWAR, FlightType.EWAR,
FlightType.ESCORT, FlightType.ESCORT,
] ]

View File

@ -496,7 +496,11 @@ class CoalitionMissionPlanner:
error = random.randint(-margin, margin) error = random.randint(-margin, margin)
yield timedelta(minutes=max(0, time + error)) yield timedelta(minutes=max(0, time + error))
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION) dca_types = {
FlightType.BARCAP,
FlightType.INTERCEPTION,
FlightType.TARCAP,
}
non_dca_packages = [p for p in self.ato.packages if non_dca_packages = [p for p in self.ato.packages if
p.primary_task not in dca_types] p.primary_task not in dca_types]

View File

@ -38,6 +38,8 @@ class FlightType(Enum):
RECON = 15 RECON = 15
EWAR = 16 EWAR = 16
SWEEP = 17
class FlightWaypointType(Enum): class FlightWaypointType(Enum):
TAKEOFF = 0 # Take off point TAKEOFF = 0 # Take off point
@ -61,6 +63,7 @@ class FlightWaypointType(Enum):
LOITER = 18 LOITER = 18
INGRESS_ESCORT = 19 INGRESS_ESCORT = 19
INGRESS_DEAD = 20 INGRESS_DEAD = 20
INGRESS_SWEEP = 21
class FlightWaypoint: class FlightWaypoint:

View File

@ -7,6 +7,7 @@ generating the waypoints for the mission.
""" """
from __future__ import annotations from __future__ import annotations
import math
from datetime import timedelta from datetime import timedelta
from functools import cached_property from functools import cached_property
import logging import logging
@ -104,6 +105,15 @@ class FlightPlan:
""" """
raise NotImplementedError raise NotImplementedError
@property
def tot_offset(self) -> timedelta:
"""This flight's offset from the package's TOT.
Positive values represent later TOTs. An offset of -2 minutes is used
for a flight that has a TOT 2 minutes before the rest of the package.
"""
return timedelta()
# Not cached because changes to the package might alter the formation speed. # Not cached because changes to the package might alter the formation speed.
@property @property
def travel_time_to_target(self) -> Optional[timedelta]: def travel_time_to_target(self) -> Optional[timedelta]:
@ -146,8 +156,33 @@ class FlightPlan:
@dataclass(frozen=True) @dataclass(frozen=True)
class FormationFlightPlan(FlightPlan): class LoiterFlightPlan(FlightPlan):
hold: FlightWaypoint hold: FlightWaypoint
@property
def waypoints(self) -> List[FlightWaypoint]:
raise NotImplementedError
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
raise NotImplementedError
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
raise NotImplementedError
@property
def push_time(self) -> timedelta:
raise NotImplementedError
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@dataclass(frozen=True)
class FormationFlightPlan(LoiterFlightPlan):
join: FlightWaypoint join: FlightWaypoint
split: FlightWaypoint split: FlightWaypoint
@ -214,12 +249,6 @@ class FormationFlightPlan(FlightPlan):
return self.split_time return self.split_time
return None return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@property @property
def push_time(self) -> timedelta: def push_time(self) -> timedelta:
return self.join_time - TravelTime.between_points( return self.join_time - TravelTime.between_points(
@ -275,18 +304,14 @@ class PatrollingFlightPlan(FlightPlan):
@dataclass(frozen=True) @dataclass(frozen=True)
class BarCapFlightPlan(PatrollingFlightPlan): class BarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
ascent: FlightWaypoint
descent: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
@property @property
def waypoints(self) -> List[FlightWaypoint]: def waypoints(self) -> List[FlightWaypoint]:
return [ return [
self.takeoff, self.takeoff,
self.ascent,
self.patrol_start, self.patrol_start,
self.patrol_end, self.patrol_end,
self.descent,
self.land, self.land,
] ]
@ -294,20 +319,16 @@ class BarCapFlightPlan(PatrollingFlightPlan):
@dataclass(frozen=True) @dataclass(frozen=True)
class CasFlightPlan(PatrollingFlightPlan): class CasFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
ascent: FlightWaypoint
target: FlightWaypoint target: FlightWaypoint
descent: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
@property @property
def waypoints(self) -> List[FlightWaypoint]: def waypoints(self) -> List[FlightWaypoint]:
return [ return [
self.takeoff, self.takeoff,
self.ascent,
self.patrol_start, self.patrol_start,
self.target, self.target,
self.patrol_end, self.patrol_end,
self.descent,
self.land, self.land,
] ]
@ -319,23 +340,24 @@ class CasFlightPlan(PatrollingFlightPlan):
@dataclass(frozen=True) @dataclass(frozen=True)
class FrontLineCapFlightPlan(PatrollingFlightPlan): class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
ascent: FlightWaypoint
descent: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
lead_time: timedelta
@property @property
def waypoints(self) -> List[FlightWaypoint]: def waypoints(self) -> List[FlightWaypoint]:
return [ return [
self.takeoff, self.takeoff,
self.ascent,
self.patrol_start, self.patrol_start,
self.patrol_end, self.patrol_end,
self.descent,
self.land, self.land,
] ]
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
def depart_time_for_waypoint( def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]: self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.patrol_end: if waypoint == self.patrol_end:
@ -346,8 +368,8 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan):
def patrol_start_time(self) -> timedelta: def patrol_start_time(self) -> timedelta:
start = self.package.escort_start_time start = self.package.escort_start_time
if start is not None: if start is not None:
return start return start + self.tot_offset
return super().patrol_start_time return super().patrol_start_time + self.tot_offset
@property @property
def patrol_end_time(self) -> timedelta: def patrol_end_time(self) -> timedelta:
@ -357,31 +379,31 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan):
return super().patrol_end_time return super().patrol_end_time
# TODO: Remove when breaking save compat.
FrontLineCapFlightPlan = TarCapFlightPlan
@dataclass(frozen=True) @dataclass(frozen=True)
class StrikeFlightPlan(FormationFlightPlan): class StrikeFlightPlan(FormationFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
ascent: FlightWaypoint
hold: FlightWaypoint hold: FlightWaypoint
join: FlightWaypoint join: FlightWaypoint
ingress: FlightWaypoint ingress: FlightWaypoint
targets: List[FlightWaypoint] targets: List[FlightWaypoint]
egress: FlightWaypoint egress: FlightWaypoint
split: FlightWaypoint split: FlightWaypoint
descent: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
@property @property
def waypoints(self) -> List[FlightWaypoint]: def waypoints(self) -> List[FlightWaypoint]:
return [ return [
self.takeoff, self.takeoff,
self.ascent,
self.hold, self.hold,
self.join, self.join,
self.ingress self.ingress
] + self.targets + [ ] + self.targets + [
self.egress, self.egress,
self.split, self.split,
self.descent,
self.land, self.land,
] ]
@ -476,6 +498,64 @@ class StrikeFlightPlan(FormationFlightPlan):
return super().tot_for_waypoint(waypoint) return super().tot_for_waypoint(waypoint)
@dataclass(frozen=True)
class SweepFlightPlan(LoiterFlightPlan):
takeoff: FlightWaypoint
sweep_start: FlightWaypoint
sweep_end: FlightWaypoint
land: FlightWaypoint
lead_time: timedelta
@property
def waypoints(self) -> List[FlightWaypoint]:
return [
self.takeoff,
self.hold,
self.sweep_start,
self.sweep_end,
self.land,
]
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.sweep_end
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
@property
def sweep_start_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.sweep_start, self.sweep_end)
return self.sweep_end_time - travel_time
@property
def sweep_end_time(self) -> timedelta:
return self.package.time_over_target + self.tot_offset
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.sweep_start:
return self.sweep_start_time
if waypoint == self.sweep_end:
return self.sweep_end_time
return None
def depart_time_for_waypoint(
self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.push_time
return None
@property
def push_time(self) -> timedelta:
return self.sweep_end_time - TravelTime.between_points(
self.hold.position,
self.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.hold.alt)
)
@dataclass(frozen=True) @dataclass(frozen=True)
class CustomFlightPlan(FlightPlan): class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint] custom_waypoints: List[FlightWaypoint]
@ -561,8 +641,10 @@ class FlightPlanBuilder:
return self.generate_sead(flight, custom_targets) return self.generate_sead(flight, custom_targets)
elif task == FlightType.STRIKE: elif task == FlightType.STRIKE:
return self.generate_strike(flight) return self.generate_strike(flight)
elif task == FlightType.SWEEP:
return self.generate_sweep(flight)
elif task == FlightType.TARCAP: elif task == FlightType.TARCAP:
return self.generate_frontline_cap(flight) return self.generate_tarcap(flight)
elif task == FlightType.TROOP_TRANSPORT: elif task == FlightType.TROOP_TRANSPORT:
logging.error( logging.error(
"Troop transport flight plan generation not implemented" "Troop transport flight plan generation not implemented"
@ -631,11 +713,57 @@ class FlightPlanBuilder:
if isinstance(location, FrontLine): if isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
start, end = self.racetrack_for_objective(location)
patrol_alt = random.randint( patrol_alt = random.randint(
self.doctrine.min_patrol_altitude, self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude self.doctrine.max_patrol_altitude
) )
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
start, end = builder.race_track(start, end, patrol_alt)
descent, land = builder.rtb(flight.from_cp)
return BarCapFlightPlan(
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cap_duration,
takeoff=builder.takeoff(flight.from_cp),
patrol_start=start,
patrol_end=end,
land=land
)
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
"""Generate a BARCAP flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
target = self.package.target.position
heading = self._heading_to_package_airfield(target)
start = target.point_from_heading(heading,
-self.doctrine.sweep_distance)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
descent, land = builder.rtb(flight.from_cp)
start, end = builder.sweep(start, target,
self.doctrine.ingress_altitude)
return SweepFlightPlan(
package=self.package,
flight=flight,
lead_time=timedelta(minutes=5),
takeoff=builder.takeoff(flight.from_cp),
hold=builder.hold(self._hold_point(flight)),
sweep_start=start,
sweep_end=end,
land=land
)
def racetrack_for_objective(self,
location: MissionTarget) -> Tuple[Point, Point]:
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location) closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in closest_cache.closest_airfields: for airfield in closest_cache.closest_airfields:
# If the mission is a BARCAP of an enemy airfield, find the *next* # If the mission is a BARCAP of an enemy airfield, find the *next*
@ -671,37 +799,11 @@ class FlightPlanBuilder:
self.doctrine.cap_max_track_length self.doctrine.cap_max_track_length
) )
start = end.point_from_heading(heading - 180, diameter) start = end.point_from_heading(heading - 180, diameter)
return start, end
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) def racetrack_for_frontline(self,
start, end = builder.race_track(start, end, patrol_alt) front_line: FrontLine) -> Tuple[Point, Point]:
descent, land = builder.rtb(flight.from_cp) ally_cp, enemy_cp = front_line.control_points
return BarCapFlightPlan(
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cap_duration,
takeoff=builder.takeoff(flight.from_cp),
ascent=builder.ascent(flight.from_cp),
patrol_start=start,
patrol_end=end,
descent=descent,
land=land
)
def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan:
"""Generate a CAP flight plan for the given front line.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if not isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
ally_cp, enemy_cp = location.control_points
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude)
# Find targets waypoints # Find targets waypoints
ingress, heading, distance = Conflict.frontline_vector( ingress, heading, distance = Conflict.frontline_vector(
@ -722,24 +824,41 @@ class FlightPlanBuilder:
orbit0p = orbit_center.point_from_heading(heading, radius) orbit0p = orbit_center.point_from_heading(heading, radius)
orbit1p = orbit_center.point_from_heading(heading + 180, radius) orbit1p = orbit_center.point_from_heading(heading + 180, radius)
return orbit0p, orbit1p
def generate_tarcap(self, flight: Flight) -> TarCapFlightPlan:
"""Generate a CAP flight plan for the given front line.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
patrol_alt = random.randint(self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude)
# Create points # Create points
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine) builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
if isinstance(location, FrontLine):
orbit0p, orbit1p = self.racetrack_for_frontline(location)
else:
orbit0p, orbit1p = self.racetrack_for_objective(location)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt) start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
descent, land = builder.rtb(flight.from_cp) descent, land = builder.rtb(flight.from_cp)
return FrontLineCapFlightPlan( return TarCapFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
lead_time=timedelta(minutes=2),
# Note that this duration only has an effect if there are no # Note that this duration only has an effect if there are no
# flights in the package that have requested escort. If the package # flights in the package that have requested escort. If the package
# requests an escort the CAP flight will remain on station for the # requests an escort the CAP flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo. # duration of the escorted mission, or until it is winchester/bingo.
patrol_duration=self.doctrine.cap_duration, patrol_duration=self.doctrine.cap_duration,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.from_cp),
ascent=builder.ascent(flight.from_cp),
patrol_start=start, patrol_start=start,
patrol_end=end, patrol_end=end,
descent=descent,
land=land land=land
) )
@ -805,14 +924,12 @@ class FlightPlanBuilder:
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.from_cp),
ascent=builder.ascent(flight.from_cp),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join), join=builder.join(self.package.waypoints.join),
ingress=ingress, ingress=ingress,
targets=[target], targets=[target],
egress=egress, egress=egress,
split=builder.split(self.package.waypoints.split), split=builder.split(self.package.waypoints.split),
descent=descent,
land=land land=land
) )
@ -842,11 +959,9 @@ class FlightPlanBuilder:
flight=flight, flight=flight,
patrol_duration=self.doctrine.cas_duration, patrol_duration=self.doctrine.cas_duration,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.from_cp),
ascent=builder.ascent(flight.from_cp),
patrol_start=builder.ingress_cas(ingress, location), patrol_start=builder.ingress_cas(ingress, location),
target=builder.cas(center), target=builder.cas(center),
patrol_end=builder.egress(egress, location), patrol_end=builder.egress(egress, location),
descent=descent,
land=land land=land
) )
@ -871,12 +986,50 @@ class FlightPlanBuilder:
return builder.strike_area(location) return builder.strike_area(location)
def _hold_point(self, flight: Flight) -> Point: def _hold_point(self, flight: Flight) -> Point:
heading = flight.from_cp.position.heading_between_point( assert self.package.waypoints is not None
self.package.target.position origin = flight.from_cp.position
) target = self.package.target.position
return flight.from_cp.position.point_from_heading( join = self.package.waypoints.join
heading, nm_to_meter(15) origin_to_target = origin.distance_to_point(target)
join_to_target = join.distance_to_point(target)
if origin_to_target < join_to_target:
# If the origin airfield is closer to the target than the join
# point, plan the hold point such that it retreats from the origin
# airfield.
return join.point_from_heading(target.heading_between_point(origin),
self.doctrine.push_distance)
heading_to_join = origin.heading_between_point(join)
hold_point = origin.point_from_heading(heading_to_join,
self.doctrine.push_distance)
if hold_point.distance_to_point(join) >= self.doctrine.push_distance:
# Hold point is between the origin airfield and the join point and
# spaced sufficiently.
return hold_point
# The hold point is between the origin airfield and the join point, but
# the distance between the hold point and the join point is too short.
# Bend the hold point out to extend the distance while maintaining the
# minimum distance from the origin airfield to keep the AI flying
# properly.
origin_to_join = origin.distance_to_point(join)
cos_theta = (
(self.doctrine.hold_distance ** 2 +
origin_to_join ** 2 -
self.doctrine.join_distance ** 2) /
(2 * self.doctrine.hold_distance * origin_to_join)
) )
try:
theta = math.acos(cos_theta)
except ValueError:
# No solution that maintains hold and join distances. Extend the
# hold point away from the target.
return origin.point_from_heading(
target.heading_between_point(origin),
self.doctrine.hold_distance)
return origin.point_from_heading(heading_to_join - theta,
self.doctrine.hold_distance)
# TODO: Make a model for the waypoint builder and use that in the UI. # TODO: Make a model for the waypoint builder and use that in the UI.
def generate_ascend_point(self, flight: Flight, def generate_ascend_point(self, flight: Flight,
@ -944,23 +1097,37 @@ class FlightPlanBuilder:
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.from_cp), takeoff=builder.takeoff(flight.from_cp),
ascent=builder.ascent(flight.from_cp),
hold=builder.hold(self._hold_point(flight)), hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join), join=builder.join(self.package.waypoints.join),
ingress=ingress, ingress=ingress,
targets=target_waypoints, targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location), egress=builder.egress(self.package.waypoints.egress, location),
split=builder.split(self.package.waypoints.split), split=builder.split(self.package.waypoints.split),
descent=descent,
land=land land=land
) )
def _join_point(self, ingress_point: Point) -> Point: def _join_point(self, ingress_point: Point) -> Point:
ingress_distance = self._distance_to_package_airfield(ingress_point)
if ingress_distance < self.doctrine.join_distance:
# If the ingress point is close to the origin, plan the join point
# farther back.
return ingress_point.point_from_heading(
self.package.target.position.heading_between_point(
self.package_airfield().position),
self.doctrine.join_distance)
heading = self._heading_to_package_airfield(ingress_point) heading = self._heading_to_package_airfield(ingress_point)
return ingress_point.point_from_heading(heading, return ingress_point.point_from_heading(heading,
-self.doctrine.join_distance) -self.doctrine.join_distance)
def _split_point(self, egress_point: Point) -> Point: def _split_point(self, egress_point: Point) -> Point:
egress_distance = self._distance_to_package_airfield(egress_point)
if egress_distance < self.doctrine.split_distance:
# If the ingress point is close to the origin, plan the split point
# farther back.
return egress_point.point_from_heading(
self.package.target.position.heading_between_point(
self.package_airfield().position),
self.doctrine.split_distance)
heading = self._heading_to_package_airfield(egress_point) heading = self._heading_to_package_airfield(egress_point)
return egress_point.point_from_heading(heading, return egress_point.point_from_heading(heading,
-self.doctrine.split_distance) -self.doctrine.split_distance)
@ -983,6 +1150,9 @@ class FlightPlanBuilder:
def _heading_to_package_airfield(self, point: Point) -> int: def _heading_to_package_airfield(self, point: Point) -> int:
return self.package_airfield().position.heading_between_point(point) return self.package_airfield().position.heading_between_point(point)
def _distance_to_package_airfield(self, point: Point) -> int:
return self.package_airfield().position.distance_to_point(point)
def package_airfield(self) -> ControlPoint: def package_airfield(self) -> ControlPoint:
# We'll always have a package, but if this is being planned via the UI # We'll always have a package, but if this is being planned via the UI
# it could be the first flight in the package. # it could be the first flight in the package.

View File

@ -128,7 +128,14 @@ class TotEstimator:
f"time for {flight} will be immediate.") f"time for {flight} will be immediate.")
return timedelta() return timedelta()
else: else:
tot = self.package.time_over_target tot_waypoint = flight.flight_plan.tot_waypoint
if tot_waypoint is None:
tot = self.package.time_over_target
else:
tot = flight.flight_plan.tot_for_waypoint(tot_waypoint)
if tot is None:
logging.error(f"TOT waypoint for {flight} has no TOT")
tot = self.package.time_over_target
return tot - travel_time - self.HOLD_TIME return tot - travel_time - self.HOLD_TIME
def earliest_tot(self) -> timedelta: def earliest_tot(self) -> timedelta:
@ -165,9 +172,13 @@ class TotEstimator:
# Return 0 so this flight's travel time does not affect the rest # Return 0 so this flight's travel time does not affect the rest
# of the package. # of the package.
return timedelta() return timedelta()
# Account for TOT offsets for the flight plan. An offset of -2 minutes
# means the flight's TOT is 2 minutes ahead of the package's so it needs
# an extra two minutes.
offset = -flight.flight_plan.tot_offset
startup = self.estimate_startup(flight) startup = self.estimate_startup(flight)
ground_ops = self.estimate_ground_ops(flight) ground_ops = self.estimate_ground_ops(flight)
return startup + ground_ops + time_to_target return startup + ground_ops + time_to_target + offset
@staticmethod @staticmethod
def estimate_startup(flight: Flight) -> timedelta: def estimate_startup(flight: Flight) -> timedelta:

View File

@ -326,6 +326,56 @@ class WaypointBuilder:
return (self.race_track_start(start, altitude), return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude)) self.race_track_end(end, altitude))
@staticmethod
def sweep_start(position: Point, altitude: int) -> FlightWaypoint:
"""Creates a sweep start waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.INGRESS_SWEEP,
position.x,
position.y,
altitude
)
waypoint.name = "SWEEP START"
waypoint.description = "Proceed to the target and engage enemy aircraft"
waypoint.pretty_name = "Sweep start"
return waypoint
@staticmethod
def sweep_end(position: Point, altitude: int) -> FlightWaypoint:
"""Creates a sweep end waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the sweep in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
position.x,
position.y,
altitude
)
waypoint.name = "SWEEP END"
waypoint.description = "End of sweep"
waypoint.pretty_name = "Sweep end"
return waypoint
def sweep(self, start: Point, end: Point,
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit.
Args:
start: The beginning of the sweep.
end: The end of the sweep.
altitude: The sweep altitude.
"""
return (self.sweep_start(start, altitude),
self.sweep_end(end, altitude))
def rtb(self, def rtb(self,
arrival: ControlPoint) -> Tuple[FlightWaypoint, FlightWaypoint]: arrival: ControlPoint) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates descent ant landing waypoints for the given control point. """Creates descent ant landing waypoints for the given control point.

2
pydcs

@ -1 +1 @@
Subproject commit 8e74bfb61b829477d35a80148fc295f2158902dd Subproject commit fa9195fbccbf96775d108a22c13c3ee2375e4c0b

View File

@ -11,7 +11,6 @@ analysis = Analysis(
('resources', 'resources'), ('resources', 'resources'),
('resources/caucasus.p', 'dcs/terrain/'), ('resources/caucasus.p', 'dcs/terrain/'),
('resources/nevada.p', 'dcs/terrain/'), ('resources/nevada.p', 'dcs/terrain/'),
('buildnumber', './')
], ],
hookspath=[], hookspath=[],
runtime_hooks=[], runtime_hooks=[],

View File

@ -18,9 +18,11 @@ class QFlightTypeComboBox(QComboBox):
"""Combo box for selecting a flight task type.""" """Combo box for selecting a flight task type."""
COMMON_ENEMY_MISSIONS = [ COMMON_ENEMY_MISSIONS = [
FlightType.TARCAP,
FlightType.ESCORT, FlightType.ESCORT,
FlightType.SEAD, FlightType.SEAD,
FlightType.DEAD, FlightType.DEAD,
FlightType.SWEEP,
# TODO: FlightType.ELINT, # TODO: FlightType.ELINT,
# TODO: FlightType.EWAR, # TODO: FlightType.EWAR,
# TODO: FlightType.RECON, # TODO: FlightType.RECON,
@ -49,7 +51,6 @@ class QFlightTypeComboBox(QComboBox):
] ]
ENEMY_AIRBASE_MISSIONS = [ ENEMY_AIRBASE_MISSIONS = [
FlightType.BARCAP,
# TODO: FlightType.STRIKE # TODO: FlightType.STRIKE
] + COMMON_ENEMY_MISSIONS ] + COMMON_ENEMY_MISSIONS
@ -59,13 +60,11 @@ class QFlightTypeComboBox(QComboBox):
] + COMMON_FRIENDLY_MISSIONS ] + COMMON_FRIENDLY_MISSIONS
ENEMY_GROUND_OBJECT_MISSIONS = [ ENEMY_GROUND_OBJECT_MISSIONS = [
FlightType.BARCAP,
FlightType.STRIKE, FlightType.STRIKE,
] + COMMON_ENEMY_MISSIONS ] + COMMON_ENEMY_MISSIONS
FRONT_LINE_MISSIONS = [ FRONT_LINE_MISSIONS = [
FlightType.CAS, FlightType.CAS,
FlightType.TARCAP,
# TODO: FlightType.TROOP_TRANSPORT # TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC # TODO: FlightType.EVAC
] + COMMON_ENEMY_MISSIONS ] + COMMON_ENEMY_MISSIONS

View File

@ -149,10 +149,10 @@ class QFlightWaypointTab(QFrame):
# departs, whereas BARCAP usually isn't part of a strike package and # departs, whereas BARCAP usually isn't part of a strike package and
# has a fixed mission time. # has a fixed mission time.
if task == FlightType.CAP: if task == FlightType.CAP:
if isinstance(self.package.target, FrontLine): if self.package.target.is_friendly(to_player=True):
task = FlightType.TARCAP
else:
task = FlightType.BARCAP task = FlightType.BARCAP
else:
task = FlightType.TARCAP
self.flight.flight_type = task self.flight.flight_type = task
self.planner.populate_flight_plan(self.flight) self.planner.populate_flight_plan(self.flight)
self.flight_waypoint_list.update_list() self.flight_waypoint_list.update_list()

View File

@ -16,6 +16,7 @@ We do not have a single vehicle available to hold our position. The situation i
{% if frontline.enemy_zero %} {% if frontline.enemy_zero %}
The enemy forces have been crushed, we will be able to make significant progress toward {{ frontline.enemy_base.name }} The enemy forces have been crushed, we will be able to make significant progress toward {{ frontline.enemy_base.name }}
{% endif %} {% endif %}
{% if not frontline.player_zero %}
{# Pick a random sentence to describe each frontline #} {# Pick a random sentence to describe each frontline #}
{% set fl_sent1 %}There are combats between {{ frontline.player_base.name }} and {{frontline.enemy_base.name}}. {%+ endset %} {% set fl_sent1 %}There are combats between {{ frontline.player_base.name }} and {{frontline.enemy_base.name}}. {%+ endset %}
{% set fl_sent2 %}The war on the ground is still going on between {{frontline.player_base.name}} and {{frontline.enemy_base.name}}. {%+ endset %} {% set fl_sent2 %}The war on the ground is still going on between {{frontline.player_base.name}} and {{frontline.enemy_base.name}}. {%+ endset %}
@ -57,8 +58,9 @@ On this location, our ground forces have been ordered to hold still, and defend
{# TODO: Write a retreat sentence #} {# TODO: Write a retreat sentence #}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% endif %}
{% endfor %}{% endif %} {%+ endfor %}{% endif %}
Your flights: Your flights:
==================== ====================

View File

@ -0,0 +1,137 @@
{
"name": "Persian Gulf - Full Map",
"theater": "Persian Gulf",
"authors": "Plob",
"description": "<p>In this scenario, you start at Liwa Airfield, and must work your way north through the whole map.</p>",
"player_points": [
{
"type": "airbase",
"id": "Liwa Airbase",
"size": 1000,
"importance": 0.2
},
{
"type": "lha",
"id": 1002,
"x": -164000,
"y": -257000,
"captured_invert": true
},
{
"type": "carrier",
"id": 1001,
"x": -124000,
"y": -303000,
"captured_invert": true
}
],
"enemy_points": [
{
"type": "airbase",
"id": "Al Ain International Airport",
"size": 1000,
"importance": 1
},
{
"type": "airbase",
"id": "Al Dhafra AB",
"size": 2000,
"importance": 1
},
{
"type": "airbase",
"id": "Al Minhad AB",
"size": 1000,
"importance": 1
},
{
"type": "airbase",
"id": "Ras Al Khaimah",
"size": 1000,
"importance": 1
},
{
"type": "airbase",
"id": "Khasab",
"size": 1000,
"importance": 1
},
{
"type": "airbase",
"id": "Bandar Abbas Intl",
"size": 2000,
"importance": 1
},
{
"type": "airbase",
"id": "Jiroft Airport",
"size": 2000,
"importance": 1.4
},
{
"type": "airbase",
"id": "Kerman Airport",
"size": 2000,
"importance": 1.7,
"captured_invert": true
},
{
"type": "airbase",
"id": "Lar Airbase",
"size": 1000,
"importance": 1.4
},
{
"type": "airbase",
"id": "Shiraz International Airport",
"size": 2000,
"importance": 1
}
],
"links": [
[
"Al Dhafra AB",
"Liwa Airbase"
],
[
"Al Dhafra AB",
"Al Ain International Airport"
],
[
"Al Ain International Airport",
"Al Minhad AB"
],
[
"Al Dhafra AB",
"Al Minhad AB"
],
[
"Al Minhad AB",
"Ras Al Khaimah"
],
[
"Khasab",
"Ras Al Khaimah"
],
[
"Bandar Abbas Intl",
"Lar Airbase"
],
[
"Shiraz International Airport",
"Lar Airbase"
],
[
"Shiraz International Airport",
"Kerman Airport"
],
[
"Jiroft Airport",
"Lar Airbase"
],
[
"Jiroft Airport",
"Kerman Airport"
]
]
}

View File

@ -5,28 +5,37 @@ local unitPayloads = {
["name"] = "CAS", ["name"] = "CAS",
["pylons"] = { ["pylons"] = {
[1] = { [1] = {
["CLSID"] = "{ARAKM70BHE}", ["CLSID"] = "{RB75}",
["num"] = 3, ["num"] = 5,
}, },
[2] = { [2] = {
["CLSID"] = "{ARAKM70BHE}", ["CLSID"] = "{RB75}",
["num"] = 2, ["num"] = 3,
}, },
[3] = { [3] = {
["CLSID"] = "{RB75}",
["num"] = 2,
},
[4] = {
["CLSID"] = "{RB75}",
["num"] = 6,
},
[5] = {
["CLSID"] = "{Robot24J}",
["num"] = 1,
},
[6] = {
["CLSID"] = "{Robot24J}",
["num"] = 7,
},
[7] = {
["CLSID"] = "{VIGGEN_X-TANK}", ["CLSID"] = "{VIGGEN_X-TANK}",
["num"] = 4, ["num"] = 4,
}, },
[4] = {
["CLSID"] = "{ARAKM70BHE}",
["num"] = 5,
},
[5] = {
["CLSID"] = "{ARAKM70BHE}",
["num"] = 6,
},
}, },
["tasks"] = { ["tasks"] = {
[1] = 31, [1] = 32,
[2] = 31,
}, },
}, },
[2] = { [2] = {

View File

@ -0,0 +1,84 @@
{
"country": "France",
"name": "France 2005 (Frenchpack)",
"authors": "HerrTom",
"description": "<p>French equipment using the Frenchpack, but without the Rafale mod.</p>",
"aircrafts": [
"M_2000C",
"Mirage_2000_5",
"SA342M",
"SA342L",
"SA342Mistral"
],
"awacs": [
"E_3A"
],
"tankers": [
"KC_135",
"KC130"
],
"frontline_units": [
"AMX_10RCR",
"AMX_10RCR_SEPAR",
"ERC_90",
"TRM_2000_PAMELA",
"VAB__50",
"VAB_MEPHISTO",
"VAB_T20_13",
"VAB_T20_13",
"VBL__50",
"VBL_AANF1",
"VBAE_CRAB",
"VBAE_CRAB_MMP",
"AMX_30B2",
"Leclerc_Serie_XXI"
],
"artillery_units": [
"MLRS_M270",
"SPH_M109_Paladin"
],
"logistics_units": [
"Transport_M818"
],
"infantry_units": [
"Infantry_M4",
"Soldier_M249",
"Stinger_MANPADS"
],
"shorads": [
"HQ7Generator",
"RolandGenerator"
],
"sams": [
"RolandGenerator",
"HawkGenerator"
],
"aircraft_carrier": [
"CVN_74_John_C__Stennis"
],
"helicopter_carrier": [
"LHA_1_Tarawa"
],
"destroyers": [
"USS_Arleigh_Burke_IIa"
],
"cruisers": [
"Ticonderoga_class"
],
"requirements": {
"frenchpack V3.5": "https://forums.eagle.ru/showthread.php?t=279974"
},
"carrier_names": [
"L9013 Mistral",
"L9014 Tonerre",
"L9015 Dixmude"
],
"helicopter_carrier_names": [
"Jeanne d'Arc"
],
"navy_generators": [
"ArleighBurkeGroupGenerator"
],
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"
}

View File

@ -0,0 +1,46 @@
{
"country": "Georgia",
"name": "Georgia 2008",
"authors": "HerrTom",
"description": "<p>A faction that represents Georgia during the South Ossetian War. They will have a lot more aircraft than historically, and no real A2A capability.</p>",
"aircrafts": [
"L_39ZA",
"Su_25",
"Mi_8MT",
"Mi_24V",
"UH_1H"
],
"frontline_units": [
"APC_BTR_80",
"APC_MTLB",
"APC_Cobra",
"IFV_BMP_1",
"IFV_BMP_2",
"MBT_T_72B",
"MBT_T_55"
],
"artillery_units": [
"MLRS_BM21_Grad",
"SPH_2S1_Gvozdika",
"SPH_2S3_Akatsia"
],
"logistics_units": [
"Transport_Ural_375",
"Transport_UAZ_469"
],
"infantry_units": [
"Paratrooper_AKS",
"Paratrooper_RPG_16"
],
"shorads": [
"SA13Generator",
"SA8Generator"
],
"sams": [
"SA6Generator",
"SA11Generator"
],
"requirements": {},
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"
}

View File

@ -0,0 +1,73 @@
{
"country": "USA",
"name": "US Navy 1985",
"authors": "HerrTom",
"description": "<p>Highway to the Danger Zone! For Tomcat lovers.</p>",
"aircrafts": [
"F_4E",
"F_14B",
"S_3B",
"UH_1H",
"AH_1W"
],
"awacs": [
"E_3A"
],
"tankers": [
"S_3B_Tanker"
],
"frontline_units": [
"MBT_M60A3_Patton",
"APC_M113",
"APC_M1025_HMMWV"
],
"artillery_units": [
"SPH_M109_Paladin",
"MLRS_M270"
],
"logistics_units": [
"Transport_M818"
],
"infantry_units": [
"Infantry_M4",
"Soldier_M249"
],
"shorads": [
"VulcanGenerator",
"ChaparralGenerator"
],
"sams": [
"HawkGenerator",
"ChaparralGenerator"
],
"aircraft_carrier": [
"CVN_74_John_C__Stennis"
],
"helicopter_carrier": [
"LHA_1_Tarawa"
],
"destroyers": [
"Oliver_Hazzard_Perry_class"
],
"cruisers": [
"Ticonderoga_class"
],
"carrier_names": [
"CVN-71 Theodore Roosevelt",
"CVN-72 Abraham Lincoln",
"CVN-73 George Washington",
"CVN-74 John C. Stennis"
],
"helicopter_carrier_names": [
"LHA-1 Tarawa",
"LHA-2 Saipan",
"LHA-3 Belleau Wood",
"LHA-4 Nassau",
"LHA-5 Peleliu"
],
"navy_generators": [
"OliverHazardPerryGroupGenerator"
],
"requirements": {},
"doctrine": "coldwar"
}

View File

@ -350,6 +350,10 @@ class FrontLine(MissionTarget):
self._build_segments() self._build_segments()
self.name = f"Front line {control_point_a}/{control_point_b}" self.name = f"Front line {control_point_a}/{control_point_b}"
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
raise False
@property @property
def position(self): def position(self):
""" """

View File

@ -1,2 +1,2 @@
"""Only here to keep compatibility for save games generated in version 2.2.0""" """Only here to keep compatibility for save games generated in version 2.2.0"""
from theater.conflicttheater import * from theater.conflicttheater import *

View File

@ -17,3 +17,7 @@ class MissionTarget:
def distance_to(self, other: MissionTarget) -> int: def distance_to(self, other: MissionTarget) -> int:
"""Computes the distance to the given mission target.""" """Computes the distance to the given mission target."""
return self.position.distance_to_point(other.position) return self.position.distance_to_point(other.position)
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
raise NotImplementedError

View File

@ -113,6 +113,9 @@ class TheaterGroundObject(MissionTarget):
def faction_color(self) -> str: def faction_color(self) -> str:
return "BLUE" if self.control_point.captured else "RED" return "BLUE" if self.control_point.captured else "RED"
def is_friendly(self, to_player: bool) -> bool:
return not self.control_point.is_friendly(to_player)
class BuildingGroundObject(TheaterGroundObject): class BuildingGroundObject(TheaterGroundObject):
def __init__(self, name: str, category: str, group_id: int, object_id: int, def __init__(self, name: str, category: str, group_id: int, object_id: int,