Merge pull request #403 from Khopa/develop_2_2_x

Release 2.2.1
This commit is contained in:
C. Perreau 2020-11-20 00:29:15 +01:00 committed by GitHub
commit 6524286f04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1178 additions and 291 deletions

View File

@ -31,6 +31,10 @@ First, a big thanks to shdwp, for starting the original DCS Liberation project.
Then, DCS Liberation uses [pydcs](http://github.com/pydcs/dcs) for mission generation, and nothing would be possible without this.
It also uses the popular [Mist](https://github.com/mrSkortch/MissionScriptingTools) lua framework for mission scripting.
And for the JTAC feature, DCS Liberation embed Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
Excellent lua scripts DCS Liberation uses as plugins:
* For the JTAC feature, DCS Liberation embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
Please also show some support to these projects !

View File

@ -1,3 +1,21 @@
# 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
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
* **[UI]** Mission start screen now informs players about delayed flights.
* **[Units]** Added support for F-14A-135-GR
* **[Modding]** Possible to setup liveries overrides in factions definition files
## Fixes :
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
# 2.2.0
## Features/Improvements :

View File

@ -16,6 +16,8 @@ class Doctrine:
sead_max_range: int
rendezvous_altitude: int
hold_distance: int
push_distance: int
join_distance: int
split_distance: int
ingress_egress_distance: int
@ -44,6 +46,8 @@ MODERN_DOCTRINE = Doctrine(
strike_max_range=1500000,
sead_max_range=1500000,
rendezvous_altitude=feet_to_meter(25000),
hold_distance=nm_to_meter(15),
push_distance=nm_to_meter(20),
join_distance=nm_to_meter(20),
split_distance=nm_to_meter(20),
ingress_egress_distance=nm_to_meter(45),
@ -69,6 +73,8 @@ COLDWAR_DOCTRINE = Doctrine(
strike_max_range=1500000,
sead_max_range=1500000,
rendezvous_altitude=feet_to_meter(22000),
hold_distance=nm_to_meter(10),
push_distance=nm_to_meter(10),
join_distance=nm_to_meter(10),
split_distance=nm_to_meter(10),
ingress_egress_distance=nm_to_meter(30),
@ -93,6 +99,8 @@ WWII_DOCTRINE = Doctrine(
antiship=True,
strike_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),
split_distance=nm_to_meter(5),
rendezvous_altitude=feet_to_meter(10000),

View File

@ -2,9 +2,7 @@ from datetime import datetime
from enum import Enum
from typing import Dict, List, Optional, Tuple, Type, Union
from dcs import Mission
from dcs.countries import country_dict
from dcs.country import Country
from dcs.helicopters import (
AH_1W,
AH_64A,
@ -46,6 +44,7 @@ from dcs.planes import (
FW_190A8,
FW_190D9,
F_117A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
@ -105,7 +104,7 @@ from dcs.planes import (
Tu_95MS,
WingLoong_I,
Yak_40,
plane_map,
plane_map
)
from dcs.ships import (
Armed_speedboat,
@ -155,7 +154,6 @@ from dcs.vehicles import (
)
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from game.factions.faction import Faction
# PATCH pydcs data with MODS
from game.factions.faction_loader import FactionLoader
from pydcs_extensions.a4ec.a4ec import A_4E_C
@ -204,7 +202,6 @@ vehicle_map["Toyota_vert"] = frenchpack.DIM__TOYOTA_GREEN
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
"""
---------- BEGINNING OF CONFIGURATION SECTION
"""
@ -273,6 +270,7 @@ PRICES = {
F_15E: 24,
F_16C_50: 20,
F_16A: 14,
F_14A_135_GR: 20,
F_14B: 24,
Tornado_IDS: 20,
Tornado_GR4: 20,
@ -401,20 +399,20 @@ PRICES = {
Unarmed.Transport_M818: 3,
# WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G:24,
Armor.MT_Pz_Kpfw_IV_Ausf_H:16,
Armor.HT_Pz_Kpfw_VI_Tiger_I:24,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II:26,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G: 24,
Armor.MT_Pz_Kpfw_IV_Ausf_H: 16,
Armor.HT_Pz_Kpfw_VI_Tiger_I: 24,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II: 26,
Armor.TD_Jagdpanther_G1: 18,
Armor.TD_Jagdpanzer_IV: 11,
Armor.Sd_Kfz_184_Elefant: 18,
Armor.APC_Sd_Kfz_251:4,
Armor.AC_Sd_Kfz_234_2_Puma:8,
Armor.MT_M4_Sherman:12,
Armor.MT_M4A4_Sherman_Firefly:16,
Armor.CT_Cromwell_IV:12,
Armor.M30_Cargo_Carrier:2,
Armor.APC_M2A1:4,
Armor.APC_Sd_Kfz_251: 4,
Armor.AC_Sd_Kfz_234_2_Puma: 8,
Armor.MT_M4_Sherman: 12,
Armor.MT_M4A4_Sherman_Firefly: 16,
Armor.CT_Cromwell_IV: 12,
Armor.M30_Cargo_Carrier: 2,
Armor.APC_M2A1: 4,
Armor.CT_Centaur_IV: 10,
Armor.HIT_Churchill_VII: 16,
Armor.LAC_M8_Greyhound: 8,
@ -577,6 +575,7 @@ UNIT_BY_TASK = {
MiG_31,
FA_18C_hornet,
F_15C,
F_14A_135_GR,
F_14B,
F_16A,
F_16C_50,
@ -902,7 +901,6 @@ SAM_CONVERT = {
}
}
"""
Units that will always be spawned in the air
"""
@ -913,7 +911,7 @@ TAKEOFF_BAN: List[Type[FlyingType]] = [
Units that will be always spawned in the air if launched from the carrier
"""
CARRIER_TAKEOFF_BAN: List[Type[FlyingType]] = [
Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned
Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned
]
"""
@ -924,6 +922,7 @@ FACTIONS = FactionLoader()
CARRIER_TYPE_BY_PLANE = {
FA_18C_hornet: CVN_74_John_C__Stennis,
F_14A_135_GR: CVN_74_John_C__Stennis,
F_14B: CVN_74_John_C__Stennis,
Ka_50: LHA_1_Tarawa,
SA342M: LHA_1_Tarawa,
@ -997,6 +996,7 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
AV8BNA: COMMON_OVERRIDE,
C_101CC: COMMON_OVERRIDE,
F_5E_3: COMMON_OVERRIDE,
F_14A_135_GR: COMMON_OVERRIDE,
F_14B: COMMON_OVERRIDE,
F_15C: COMMON_OVERRIDE,
F_16C_50: COMMON_OVERRIDE,
@ -1006,14 +1006,14 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
MiG_19P: COMMON_OVERRIDE,
MiG_21Bis: COMMON_OVERRIDE,
AJS37: COMMON_OVERRIDE,
Su_25T:COMMON_OVERRIDE,
Su_25:COMMON_OVERRIDE,
Su_27:COMMON_OVERRIDE,
Su_33:COMMON_OVERRIDE,
MiG_29A:COMMON_OVERRIDE,
MiG_29G:COMMON_OVERRIDE,
MiG_29S:COMMON_OVERRIDE,
Su_24M:COMMON_OVERRIDE,
Su_25T: COMMON_OVERRIDE,
Su_25: COMMON_OVERRIDE,
Su_27: COMMON_OVERRIDE,
Su_33: COMMON_OVERRIDE,
MiG_29A: COMMON_OVERRIDE,
MiG_29G: COMMON_OVERRIDE,
MiG_29S: COMMON_OVERRIDE,
Su_24M: COMMON_OVERRIDE,
Su_30: COMMON_OVERRIDE,
Su_34: COMMON_OVERRIDE,
Su_57: COMMON_OVERRIDE,
@ -1022,21 +1022,21 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
Tornado_GR4: COMMON_OVERRIDE,
Tornado_IDS: COMMON_OVERRIDE,
Mirage_2000_5: COMMON_OVERRIDE,
MiG_31:COMMON_OVERRIDE,
SA342M:COMMON_OVERRIDE,
SA342L:COMMON_OVERRIDE,
SA342Mistral:COMMON_OVERRIDE,
Mi_8MT:COMMON_OVERRIDE,
Mi_24V:COMMON_OVERRIDE,
Mi_28N:COMMON_OVERRIDE,
Ka_50:COMMON_OVERRIDE,
L_39ZA:COMMON_OVERRIDE,
L_39C:COMMON_OVERRIDE,
MiG_31: COMMON_OVERRIDE,
SA342M: COMMON_OVERRIDE,
SA342L: COMMON_OVERRIDE,
SA342Mistral: COMMON_OVERRIDE,
Mi_8MT: COMMON_OVERRIDE,
Mi_24V: COMMON_OVERRIDE,
Mi_28N: COMMON_OVERRIDE,
Ka_50: COMMON_OVERRIDE,
L_39ZA: COMMON_OVERRIDE,
L_39C: COMMON_OVERRIDE,
Su_17M4: COMMON_OVERRIDE,
F_4E: COMMON_OVERRIDE,
P_47D_30:COMMON_OVERRIDE,
P_47D_30bl1:COMMON_OVERRIDE,
P_47D_40:COMMON_OVERRIDE,
P_47D_30: COMMON_OVERRIDE,
P_47D_30bl1: COMMON_OVERRIDE,
P_47D_40: COMMON_OVERRIDE,
B_17G: COMMON_OVERRIDE,
P_51D: COMMON_OVERRIDE,
P_51D_30_NA: COMMON_OVERRIDE,
@ -1129,6 +1129,7 @@ PLAYER_BUDGET_BASE = 20
CARRIER_CAPABLE = [
FA_18C_hornet,
F_14A_135_GR,
F_14B,
AV8BNA,
Su_33,
@ -1164,7 +1165,6 @@ LHA_CAPABLE = [
SA342Mistral
]
"""
---------- END OF CONFIGURATION SECTION
"""
@ -1216,16 +1216,20 @@ def find_unittype(for_task: Task, country_name: str) -> List[UnitType]:
def find_infantry(country_name: str) -> List[UnitType]:
inf = [
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Soldier_RPG,
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
Infantry.Soldier_M249,
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
Infantry.Paratrooper_RPG_16,
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand,
Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents
]
@ -1356,7 +1360,7 @@ def unitdict_from(fd: AssignedUnitsDict) -> Dict:
def country_id_from_name(name):
for k,v in country_dict.items():
for k, v in country_dict.items():
if v.name == name:
return k
return -1
@ -1374,8 +1378,10 @@ def _validate_db():
for unit_type in total_set:
assert unit_type in PRICES, "{} not in prices".format(unit_type)
_validate_db()
class DefaultLiveries:
class Default(Enum):
af_standard = ""
@ -1385,4 +1391,4 @@ OH_58D.Liveries = DefaultLiveries
F_16C_50.Liveries = DefaultLiveries
P_51D_30_NA.Liveries = DefaultLiveries
Ju_88A4.Liveries = DefaultLiveries
B_17G.Liveries = DefaultLiveries
B_17G.Liveries = DefaultLiveries

View File

@ -105,6 +105,9 @@ class Faction:
# List of available buildings for this faction
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
liveries_overrides: Dict[UnitType, List[str]] = field(default_factory=dict)
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@ -183,6 +186,14 @@ class Faction:
else:
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
# Load liveries override
faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {})
for k, v in liveries_overrides.items():
k = load_aircraft(k)
if k is not None:
faction.liveries_overrides[k] = [s.lower() for s in v]
return faction
@property

View File

@ -713,6 +713,17 @@ class AircraftConflictGenerator:
for unit_instance in group.units:
unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]
# Override livery by faction file data
if flight.from_cp.captured:
faction = self.game.player_faction
else:
faction = self.game.enemy_faction
if unit_type in faction.liveries_overrides:
livery = random.choice(faction.liveries_overrides[unit_type])
for unit_instance in group.units:
unit_instance.livery_id = livery
for idx in range(0, min(len(group.units), flight.client_count)):
unit = group.units[idx]
if self.use_client:
@ -1162,12 +1173,13 @@ class AircraftConflictGenerator:
viggen_target_points = [
(idx, point) for idx, point in enumerate(filtered_points) if point.waypoint_type in TARGET_WAYPOINTS
]
keep_target = viggen_target_points[random.randint(0, len(viggen_target_points) - 1)]
filtered_points = [
point for idx, point in enumerate(filtered_points) if (
point.waypoint_type not in TARGET_WAYPOINTS or idx == keep_target[0]
)
]
if viggen_target_points:
keep_target = viggen_target_points[random.randint(0, len(viggen_target_points) - 1)]
filtered_points = [
point for idx, point in enumerate(filtered_points) if (
point.waypoint_type not in TARGET_WAYPOINTS or idx == keep_target[0]
)
]
for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint(
@ -1187,6 +1199,12 @@ class AircraftConflictGenerator:
if not flight.client_count:
return True
if start_time < timedelta(minutes=10):
# Don't bother delaying client flights with short start delays. Much
# more than ten minutes starts to eat into fuel a bit more
# (espeicially for something fuel limited like a Harrier).
return False
return not self.settings.never_delay_player_flights
def set_takeoff_time(self, waypoint: FlightWaypoint, package: Package,
@ -1213,15 +1231,6 @@ class AircraftConflictGenerator:
@staticmethod
def should_activate_late(flight: Flight) -> bool:
if flight.client_count:
# Never delay players. Note that cold start player flights with
# AI members will still be marked as uncontrolled until the start
# trigger fires to postpone engine start.
#
# Player flights that start on the runway or in the air will start
# immediately, and AI flight members will not be delayed.
return False
if flight.start_type != "Cold":
# Avoid spawning aircraft in the air or on the runway until it's
# time for their mission. Also avoid burning through gas spawning

View File

@ -27,6 +27,7 @@ from dcs.planes import (
FW_190A8,
FW_190D9,
F_117A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
@ -104,6 +105,7 @@ INTERCEPT_CAPABLE = [
Mirage_2000_5,
Rafale_M,
F_14A_135_GR,
F_14B,
F_15C,
@ -135,6 +137,7 @@ CAP_CAPABLE = [
F_86F_Sabre,
F_4E,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
@ -183,6 +186,7 @@ CAP_PREFERRED = [
Mirage_2000_5,
F_86F_Sabre,
F_14A_135_GR,
F_14B,
F_15C,
@ -226,6 +230,7 @@ CAS_CAPABLE = [
F_86F_Sabre,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15E,
F_16A,
@ -390,6 +395,7 @@ STRIKE_CAPABLE = [
F_86F_Sabre,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15E,
F_16A,

View File

@ -7,6 +7,7 @@ generating the waypoints for the mission.
"""
from __future__ import annotations
import math
from datetime import timedelta
from functools import cached_property
import logging
@ -275,18 +276,14 @@ class PatrollingFlightPlan(FlightPlan):
@dataclass(frozen=True)
class BarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
ascent: FlightWaypoint
descent: FlightWaypoint
land: FlightWaypoint
@property
def waypoints(self) -> List[FlightWaypoint]:
return [
self.takeoff,
self.ascent,
self.patrol_start,
self.patrol_end,
self.descent,
self.land,
]
@ -294,20 +291,16 @@ class BarCapFlightPlan(PatrollingFlightPlan):
@dataclass(frozen=True)
class CasFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
ascent: FlightWaypoint
target: FlightWaypoint
descent: FlightWaypoint
land: FlightWaypoint
@property
def waypoints(self) -> List[FlightWaypoint]:
return [
self.takeoff,
self.ascent,
self.patrol_start,
self.target,
self.patrol_end,
self.descent,
self.land,
]
@ -321,18 +314,14 @@ class CasFlightPlan(PatrollingFlightPlan):
@dataclass(frozen=True)
class FrontLineCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
ascent: FlightWaypoint
descent: FlightWaypoint
land: FlightWaypoint
@property
def waypoints(self) -> List[FlightWaypoint]:
return [
self.takeoff,
self.ascent,
self.patrol_start,
self.patrol_end,
self.descent,
self.land,
]
@ -360,28 +349,24 @@ class FrontLineCapFlightPlan(PatrollingFlightPlan):
@dataclass(frozen=True)
class StrikeFlightPlan(FormationFlightPlan):
takeoff: FlightWaypoint
ascent: FlightWaypoint
hold: FlightWaypoint
join: FlightWaypoint
ingress: FlightWaypoint
targets: List[FlightWaypoint]
egress: FlightWaypoint
split: FlightWaypoint
descent: FlightWaypoint
land: FlightWaypoint
@property
def waypoints(self) -> List[FlightWaypoint]:
return [
self.takeoff,
self.ascent,
self.hold,
self.join,
self.ingress
] + self.targets + [
self.egress,
self.split,
self.descent,
self.land,
]
@ -573,8 +558,8 @@ class FlightPlanBuilder:
def regenerate_package_waypoints(self) -> None:
ingress_point = self._ingress_point()
egress_point = self._egress_point()
join_point = self._join_point(ingress_point)
split_point = self._split_point(egress_point)
join_point = self._rendezvous_point(ingress_point)
split_point = self._rendezvous_point(egress_point)
from gen.ato import PackageWaypoints
self.package.waypoints = PackageWaypoints(
@ -674,18 +659,15 @@ class FlightPlanBuilder:
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),
ascent=builder.ascent(flight.from_cp),
patrol_start=start,
patrol_end=end,
descent=descent,
land=land
land=builder.land(flight.from_cp)
)
def generate_frontline_cap(self, flight: Flight) -> FrontLineCapFlightPlan:
@ -724,9 +706,8 @@ class FlightPlanBuilder:
# Create points
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
descent, land = builder.rtb(flight.from_cp)
return FrontLineCapFlightPlan(
package=self.package,
flight=flight,
@ -736,11 +717,9 @@ class FlightPlanBuilder:
# duration of the escorted mission, or until it is winchester/bingo.
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
land=builder.land(flight.from_cp)
)
def generate_dead(self, flight: Flight,
@ -799,21 +778,18 @@ class FlightPlanBuilder:
ingress, target, egress = builder.escort(
self.package.waypoints.ingress, self.package.target,
self.package.waypoints.egress)
descent, land = builder.rtb(flight.from_cp)
return StrikeFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.from_cp),
ascent=builder.ascent(flight.from_cp),
hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join),
ingress=ingress,
targets=[target],
egress=egress,
split=builder.split(self.package.waypoints.split),
descent=descent,
land=land
land=builder.land(flight.from_cp)
)
def generate_cas(self, flight: Flight) -> CasFlightPlan:
@ -835,19 +811,16 @@ class FlightPlanBuilder:
egress = ingress.point_from_heading(heading, distance)
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
descent, land = builder.rtb(flight.from_cp)
return CasFlightPlan(
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cas_duration,
takeoff=builder.takeoff(flight.from_cp),
ascent=builder.ascent(flight.from_cp),
patrol_start=builder.ingress_cas(ingress, location),
target=builder.cas(center),
patrol_end=builder.egress(egress, location),
descent=descent,
land=land
land=builder.land(flight.from_cp)
)
@staticmethod
@ -871,36 +844,52 @@ class FlightPlanBuilder:
return builder.strike_area(location)
def _hold_point(self, flight: Flight) -> Point:
heading = flight.from_cp.position.heading_between_point(
self.package.target.position
)
return flight.from_cp.position.point_from_heading(
heading, nm_to_meter(15)
assert self.package.waypoints is not None
origin = flight.from_cp.position
target = self.package.target.position
join = self.package.waypoints.join
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.
def generate_ascend_point(self, flight: Flight,
departure: ControlPoint) -> FlightWaypoint:
"""Generate ascend point.
Args:
flight: The flight to generate the descend point for.
departure: Departure airfield or carrier.
"""
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
return builder.ascent(departure)
def generate_descend_point(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
"""Generate approach/descend point.
Args:
flight: The flight to generate the descend point for.
arrival: Arrival airfield or carrier.
"""
builder = WaypointBuilder(self.game.conditions, flight, self.doctrine)
return builder.descent(arrival)
def generate_rtb_waypoint(self, flight: Flight,
arrival: ControlPoint) -> FlightWaypoint:
"""Generate RTB landing point.
@ -939,31 +928,54 @@ class FlightPlanBuilder:
target_waypoints.append(
self.target_area_waypoint(flight, location, builder))
descent, land = builder.rtb(flight.from_cp)
return StrikeFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.from_cp),
ascent=builder.ascent(flight.from_cp),
hold=builder.hold(self._hold_point(flight)),
join=builder.join(self.package.waypoints.join),
ingress=ingress,
targets=target_waypoints,
egress=builder.egress(self.package.waypoints.egress, location),
split=builder.split(self.package.waypoints.split),
descent=descent,
land=land
land=builder.land(flight.from_cp)
)
def _join_point(self, ingress_point: Point) -> Point:
heading = self._heading_to_package_airfield(ingress_point)
return ingress_point.point_from_heading(heading,
-self.doctrine.join_distance)
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that retreats from the origin airfield."""
return attack_transition.point_from_heading(
self.package.target.position.heading_between_point(
self.package_airfield().position),
self.doctrine.join_distance)
def _split_point(self, egress_point: Point) -> Point:
heading = self._heading_to_package_airfield(egress_point)
return egress_point.point_from_heading(heading,
-self.doctrine.split_distance)
def _advancing_rendezvous_point(self, attack_transition: Point) -> Point:
"""Creates a rendezvous point that advances toward the target."""
heading = self._heading_to_package_airfield(attack_transition)
return attack_transition.point_from_heading(heading,
-self.doctrine.join_distance)
def _rendezvous_should_retreat(self, attack_transition: Point) -> bool:
transition_target_distance = attack_transition.distance_to_point(
self.package.target.position
)
origin_target_distance = self._distance_to_package_airfield(
self.package.target.position
)
# If the origin point is closer to the target than the ingress point,
# the rendezvous point should be positioned in a position that retreats
# from the origin airfield.
return origin_target_distance < transition_target_distance
def _rendezvous_point(self, attack_transition: Point) -> Point:
"""Returns the position of the rendezvous point.
Args:
attack_transition: The ingress or egress point for this rendezvous.
"""
if self._rendezvous_should_retreat(attack_transition):
return self._retreating_rendezvous_point(attack_transition)
return self._advancing_rendezvous_point(attack_transition)
def _ingress_point(self) -> Point:
heading = self._target_heading_to_package_airfield()
@ -983,6 +995,9 @@ class FlightPlanBuilder:
def _heading_to_package_airfield(self, point: Point) -> int:
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:
# We'll always have a package, but if this is being planned via the UI
# it could be the first flight in the package.

View File

@ -96,6 +96,11 @@ class TotEstimator:
def mission_start_time(self, flight: Flight) -> timedelta:
takeoff_time = self.takeoff_time_for_flight(flight)
if takeoff_time is None:
# Could not determine takeoff time, probably due to a custom flight
# plan. Start immediately.
return timedelta()
startup_time = self.estimate_startup(flight)
ground_ops_time = self.estimate_ground_ops(flight)
start_time = takeoff_time - startup_time - ground_ops_time
@ -110,13 +115,15 @@ class TotEstimator:
# Round down so *barely* above zero start times are just zero.
return timedelta(seconds=math.floor(start_time.total_seconds()))
def takeoff_time_for_flight(self, flight: Flight) -> timedelta:
def takeoff_time_for_flight(self, flight: Flight) -> Optional[timedelta]:
travel_time = self.travel_time_to_rendezvous_or_target(flight)
if travel_time is None:
logging.warning("Found no join point or patrol point. Cannot "
f"estimate takeoff time takeoff time for {flight}")
# Takeoff immediately.
return timedelta()
from gen.flights.flightplan import CustomFlightPlan
if not isinstance(flight.flight_plan, CustomFlightPlan):
logging.warning(
"Found no rendezvous or target point. Cannot estimate "
f"takeoff time takeoff time for {flight}.")
return None
from gen.flights.flightplan import FormationFlightPlan
if isinstance(flight.flight_plan, FormationFlightPlan):
@ -126,7 +133,7 @@ class TotEstimator:
logging.warning(
"Could not determine the TOT of the join point. Takeoff "
f"time for {flight} will be immediate.")
return timedelta()
return None
else:
tot = self.package.time_over_target
return tot - travel_time - self.HOLD_TIME

View File

@ -7,11 +7,9 @@ from dcs.mapping import Point
from dcs.unit import Unit
from game.data.doctrine import Doctrine
from game.utils import nm_to_meter
from game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType
from ..runways import RunwayAssigner
@dataclass(frozen=True)
@ -57,52 +55,6 @@ class WaypointBuilder:
waypoint.pretty_name = "Takeoff"
return waypoint
def ascent(self, departure: ControlPoint) -> FlightWaypoint:
"""Create ascent waypoint for the given departure airfield or carrier.
Args:
departure: Departure airfield or carrier.
"""
heading = RunwayAssigner(self.conditions).takeoff_heading(departure)
position = departure.position.point_from_heading(
heading, nm_to_meter(5)
)
waypoint = FlightWaypoint(
FlightWaypointType.ASCEND_POINT,
position.x,
position.y,
500 if self.is_helo else self.doctrine.pattern_altitude
)
waypoint.name = "ASCEND"
waypoint.alt_type = "RADIO"
waypoint.description = "Ascend"
waypoint.pretty_name = "Ascend"
return waypoint
def descent(self, arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
landing_heading = RunwayAssigner(self.conditions).landing_heading(
arrival)
heading = (landing_heading + 180) % 360
position = arrival.position.point_from_heading(
heading, nm_to_meter(5)
)
waypoint = FlightWaypoint(
FlightWaypointType.DESCENT_POINT,
position.x,
position.y,
300 if self.is_helo else self.doctrine.pattern_altitude
)
waypoint.name = "DESCEND"
waypoint.alt_type = "RADIO"
waypoint.description = "Descend to pattern altitude"
waypoint.pretty_name = "Descend"
return waypoint
@staticmethod
def land(arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier.
@ -326,15 +278,6 @@ class WaypointBuilder:
return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude))
def rtb(self,
arrival: ControlPoint) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates descent ant landing waypoints for the given control point.
Args:
arrival: Arrival airfield or carrier.
"""
return self.descent(arrival), self.land(arrival)
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package.

2
pydcs

@ -1 +1 @@
Subproject commit fa9195fbccbf96775d108a22c13c3ee2375e4c0b
Subproject commit 2883be31c2eb80834b93efd8d20ca17913986e9b

View File

@ -1,9 +1,10 @@
"""Combo box for selecting a departure airfield."""
from typing import Iterable
from PySide2.QtCore import Signal
from PySide2.QtWidgets import QComboBox
from dcs.planes import PlaneType
from game.inventory import GlobalAircraftInventory
from theater.controlpoint import ControlPoint
@ -15,6 +16,8 @@ class QOriginAirfieldSelector(QComboBox):
that have unassigned inventory of the given aircraft type.
"""
availability_changed = Signal(int)
def __init__(self, global_inventory: GlobalAircraftInventory,
origins: Iterable[ControlPoint],
aircraft: PlaneType) -> None:
@ -23,6 +26,7 @@ class QOriginAirfieldSelector(QComboBox):
self.origins = list(origins)
self.aircraft = aircraft
self.rebuild_selector()
self.currentIndexChanged.connect(self.index_changed)
def change_aircraft(self, aircraft: PlaneType) -> None:
if self.aircraft == aircraft:
@ -47,3 +51,10 @@ class QOriginAirfieldSelector(QComboBox):
return 0
inventory = self.global_inventory.for_control_point(origin)
return inventory.available(self.aircraft)
def index_changed(self, index: int) -> None:
origin = self.itemData(index)
if origin is None:
return
inventory = self.global_inventory.for_control_point(origin)
self.availability_changed.emit(inventory.available(self.aircraft))

View File

@ -44,7 +44,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
i = 0
def add_model_item(i, model, name, wpt):
print(name)
item = QStandardItem(name)
model.setItem(i, 0, item)
self.wpts.append(wpt)
@ -79,7 +78,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
0
)
wpt.alt_type = "RADIO"
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + ground_object.category + " #" + str(ground_object.object_id)
wpt.name = ground_object.waypoint_name
wpt.pretty_name = wpt.name
wpt.obj_name = ground_object.obj_name
wpt.targets.append(ground_object)

View File

@ -241,11 +241,12 @@ class QLiberationWindow(QMainWindow):
"<h4>Authors</h4>" + \
"<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>." \
"<h4>Contributors</h4>" + \
"shdwp, Khopa, ColonelPanic, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57" + \
"shdwp, Khopa, ColonelPanic, Roach, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57" + \
"<h4>Special Thanks :</h4>" \
"<b>rp-</b> <i>for the pydcs framework</i><br/>"\
"<b>Grimes (mrSkortch)</b> & <b>Speed</b> <i>for the MIST framework</i><br/>"\
"<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"
"<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"\
"<b>Walder </b> <i>for the Skynet-IADS script</i><br/>"
about = QMessageBox()
about.setWindowTitle("About DCS Liberation")
about.setIcon(QMessageBox.Icon.Information)

View File

@ -13,8 +13,9 @@ from PySide2.QtWidgets import (
QLabel,
QMessageBox,
QPushButton,
QTextEdit,
QTextBrowser,
)
from jinja2 import Environment, FileSystemLoader, select_autoescape
from game.debriefing import Debriefing, wait_for_debriefing
from game.game import Event, Game, logging
@ -65,27 +66,21 @@ class QWaitingForMissionResultWindow(QDialog):
self.layout.addWidget(header, 0, 0)
self.gridLayout = QGridLayout()
TEXT = "" + \
"<b>You are clear for takeoff</b>" + \
"" + \
"<h2>For Singleplayer :</h2>\n" + \
"In DCS, open the Mission Editor, and load the file : \n" + \
"<i>liberation_nextturn</i>\n" + \
"<p>Then once the mission is loaded in ME, in menu \"Flight\",\n" + \
"click on FLY Mission to launch.</p>\n" + \
"" + \
"<h2>For Multiplayer :</h2>" + \
"In DCS, open the Mission Editor, and load the file : " + \
"<i>liberation_nextturn</i>" + \
"<p>Click on File/Save. Then exit the mission editor, and go to Multiplayer.</p>" + \
"<p>Then host a server with the mission, and tell your friends to join !</p>" + \
"<i>(The step in the mission editor is important, and fix a game breaking bug.)</i>" + \
"<h2>Finishing</h2>" + \
"<p>Once you have played the mission, click on the \"Accept Results\" button.</p>" + \
"<p>If DCS Liberation does not detect mission end, use the manually submit button, and choose the state.json file.</p>"
self.instructions_text = QTextEdit(TEXT)
self.instructions_text.setReadOnly(True)
jinja = Environment(
loader=FileSystemLoader("resources/ui/templates"),
autoescape=select_autoescape(
disabled_extensions=("",),
default_for_string=True,
default=True,
),
trim_blocks=True,
lstrip_blocks=True,
)
self.instructions_text = QTextBrowser()
self.instructions_text.setHtml(
jinja.get_template("mission_start_EN.j2").render())
self.instructions_text.setOpenExternalLinks(True)
self.gridLayout.addWidget(self.instructions_text, 1, 0)
progress = QLabel("")

View File

@ -54,11 +54,11 @@ class QFlightCreator(QDialog):
[cp for cp in game.theater.controlpoints if cp.captured],
self.aircraft_selector.currentData()
)
self.airfield_selector.currentIndexChanged.connect(self.update_max_size)
self.airfield_selector.availability_changed.connect(self.update_max_size)
layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector))
self.flight_size_spinner = QFlightSizeSpinner()
self.update_max_size()
self.update_max_size(self.airfield_selector.available)
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
self.client_slots_spinner = QFlightSizeSpinner(
@ -91,6 +91,8 @@ class QFlightCreator(QDialog):
return f"{origin.name} has no {aircraft.id} available."
if size > available:
return f"{origin.name} has only {available} {aircraft.id} available."
if size <= 0:
return f"Flight must have at least one aircraft."
return None
def create_flight(self) -> None:
@ -120,7 +122,8 @@ class QFlightCreator(QDialog):
new_aircraft = self.aircraft_selector.itemData(index)
self.airfield_selector.change_aircraft(new_aircraft)
def update_max_size(self) -> None:
self.flight_size_spinner.setMaximum(
min(self.airfield_selector.available, 4)
)
def update_max_size(self, available: int) -> None:
self.flight_size_spinner.setMaximum(min(available, 4))
if self.flight_size_spinner.maximum() >= 2:
if self.flight_size_spinner.value() < 2:
self.flight_size_spinner.setValue(2)

View File

@ -1,4 +1,4 @@
from typing import List, Optional
from typing import Iterable, List, Optional
from PySide2.QtCore import Signal
from PySide2.QtWidgets import (
@ -12,11 +12,15 @@ from PySide2.QtWidgets import (
from game import Game
from gen.ato import Package
from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.flight import Flight, FlightType, FlightWaypoint
from gen.flights.flightplan import (
CustomFlightPlan,
FlightPlanBuilder,
StrikeFlightPlan,
)
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \
QFlightWaypointList
from qt_ui.windows.mission.flight.waypoints\
from qt_ui.windows.mission.flight.waypoints \
.QPredefinedWaypointSelectionWindow import \
QPredefinedWaypointSelectionWindow
from theater import FrontLine
@ -34,8 +38,6 @@ class QFlightWaypointTab(QFrame):
self.planner = FlightPlanBuilder(self.game, package, is_player=True)
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
self.ascend_waypoint: Optional[QPushButton] = None
self.descend_waypoint: Optional[QPushButton] = None
self.rtb_waypoint: Optional[QPushButton] = None
self.delete_selected: Optional[QPushButton] = None
self.open_fast_waypoint_button: Optional[QPushButton] = None
@ -78,14 +80,6 @@ class QFlightWaypointTab(QFrame):
rlayout.addWidget(QLabel("<strong>Advanced : </strong>"))
rlayout.addWidget(QLabel("<small>Do not use for AI flights</small>"))
self.ascend_waypoint = QPushButton("Add Ascend Waypoint")
self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint)
rlayout.addWidget(self.ascend_waypoint)
self.descend_waypoint = QPushButton("Add Descend Waypoint")
self.descend_waypoint.clicked.connect(self.on_descend_waypoint)
rlayout.addWidget(self.descend_waypoint)
self.rtb_waypoint = QPushButton("Add RTB Waypoint")
self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint)
rlayout.addWidget(self.rtb_waypoint)
@ -103,35 +97,51 @@ class QFlightWaypointTab(QFrame):
def on_delete_waypoint(self):
wpt = self.flight_waypoint_list.selectionModel().currentIndex().row()
if wpt > 0:
del self.flight.points[wpt-1]
self.delete_waypoint(self.flight.flight_plan.waypoints[wpt])
self.flight_waypoint_list.update_list()
self.on_change()
def delete_waypoint(self, waypoint: FlightWaypoint) -> None:
# Need to degrade to a custom flight plan and remove the waypoint.
# If the waypoint is a target waypoint and is not the last target
# waypoint, we don't need to degrade.
if isinstance(self.flight.flight_plan, StrikeFlightPlan):
is_target = waypoint in self.flight.flight_plan.targets
if is_target and len(self.flight.flight_plan.targets) > 1:
self.flight.flight_plan.targets.remove(waypoint)
return
self.degrade_to_custom_flight_plan()
self.flight.flight_plan.waypoints.remove(waypoint)
def on_fast_waypoint(self):
self.subwindow = QPredefinedWaypointSelectionWindow(self.game, self.flight, self.flight_waypoint_list)
self.subwindow.finished.connect(self.on_change)
self.subwindow.waypoints_added.connect(self.on_waypoints_added)
self.subwindow.show()
def on_ascend_waypoint(self):
ascend = self.planner.generate_ascend_point(self.flight,
self.flight.from_cp)
self.flight.points.append(ascend)
def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None:
if not waypoints:
return
self.degrade_to_custom_flight_plan()
self.flight.flight_plan.waypoints.extend(waypoints)
self.flight_waypoint_list.update_list()
self.on_change()
def on_rtb_waypoint(self):
rtb = self.planner.generate_rtb_waypoint(self.flight,
self.flight.from_cp)
self.flight.points.append(rtb)
self.degrade_to_custom_flight_plan()
self.flight.flight_plan.waypoints.append(rtb)
self.flight_waypoint_list.update_list()
self.on_change()
def on_descend_waypoint(self):
descend = self.planner.generate_descend_point(self.flight,
self.flight.from_cp)
self.flight.points.append(descend)
self.flight_waypoint_list.update_list()
self.on_change()
def degrade_to_custom_flight_plan(self) -> None:
if not isinstance(self.flight.flight_plan, CustomFlightPlan):
self.flight.flight_plan = CustomFlightPlan(
package=self.flight.package,
flight=self.flight,
custom_waypoints=self.flight.flight_plan.waypoints
)
def confirm_recreate(self, task: FlightType) -> None:
result = QMessageBox.question(

View File

@ -1,11 +1,20 @@
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QDialog, QLabel, QHBoxLayout, QVBoxLayout, QPushButton, QCheckBox
from PySide2.QtCore import Qt, Signal
from PySide2.QtWidgets import (
QCheckBox,
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
)
from game import Game
from gen.flights.flight import Flight
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox
from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import \
QPredefinedWaypointSelectionComboBox
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import \
QFlightWaypointInfoBox
PREDEFINED_WAYPOINT_CATEGORIES = [
"Frontline (CAS AREA)",
@ -17,6 +26,8 @@ PREDEFINED_WAYPOINT_CATEGORIES = [
class QPredefinedWaypointSelectionWindow(QDialog):
# List of FlightWaypoint
waypoints_added = Signal(list)
def __init__(self, game: Game, flight: Flight, flight_waypoint_list):
super(QPredefinedWaypointSelectionWindow, self).__init__()
@ -44,7 +55,6 @@ class QPredefinedWaypointSelectionWindow(QDialog):
self.init_ui()
self.on_select_wpt_changed()
print("DONE")
def init_ui(self):
@ -77,12 +87,5 @@ class QPredefinedWaypointSelectionWindow(QDialog):
self.add_button.setDisabled(False)
def add_waypoint(self):
for wpt in self.selected_waypoints:
self.flight.points.append(wpt)
self.flight_waypoint_list.update_list()
self.waypoints_added.emit(self.selected_waypoints)
self.close()

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 %}
The enemy forces have been crushed, we will be able to make significant progress toward {{ frontline.enemy_base.name }}
{% endif %}
{% if not frontline.player_zero %}
{# 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_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 #}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}{% endif %}
{%+ endfor %}{% endif %}
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",
["pylons"] = {
[1] = {
["CLSID"] = "{ARAKM70BHE}",
["num"] = 3,
["CLSID"] = "{RB75}",
["num"] = 5,
},
[2] = {
["CLSID"] = "{ARAKM70BHE}",
["num"] = 2,
["CLSID"] = "{RB75}",
["num"] = 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}",
["num"] = 4,
},
[4] = {
["CLSID"] = "{ARAKM70BHE}",
["num"] = 5,
},
[5] = {
["CLSID"] = "{ARAKM70BHE}",
["num"] = 6,
},
},
["tasks"] = {
[1] = 31,
[1] = 32,
[2] = 31,
},
},
[2] = {

View File

@ -0,0 +1,343 @@
local unitPayloads = {
["name"] = "F-14A",
["payloads"] = {
[1] = {
["name"] = "CAP",
["pylons"] = {
[1] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 10,
},
[2] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{SHOULDER AIM_54C_Mk47 L}",
["num"] = 2,
},
[4] = {
["CLSID"] = "{SHOULDER AIM_54C_Mk47 R}",
["num"] = 9,
},
[5] = {
["CLSID"] = "{F14-300gal}",
["num"] = 8,
},
[6] = {
["CLSID"] = "{F14-300gal}",
["num"] = 3,
},
[7] = {
["CLSID"] = "{AIM_54C_Mk47}",
["num"] = 7,
},
[8] = {
["CLSID"] = "{AIM_54C_Mk47}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{AIM_54C_Mk47}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{AIM_54C_Mk47}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 10,
},
},
[2] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 10,
},
[2] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{F14-LANTIRN-TP}",
["num"] = 9,
},
[4] = {
["CLSID"] = "{PHXBRU3242_2*LAU10 LS}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{F14-300gal}",
["num"] = 8,
},
[6] = {
["CLSID"] = "{F14-300gal}",
["num"] = 3,
},
[7] = {
["CLSID"] = "{BRU-32 MK-82}",
["num"] = 7,
},
[8] = {
["CLSID"] = "{BRU-32 MK-82}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{BRU-32 MK-82}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{BRU-32 MK-82}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 10,
},
},
[3] = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 10,
},
[2] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{SHOULDER AIM_54C_Mk47 R}",
["num"] = 9,
},
[4] = {
["CLSID"] = "{SHOULDER AIM_54C_Mk47 L}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{F14-300gal}",
["num"] = 8,
},
[6] = {
["CLSID"] = "{F14-300gal}",
["num"] = 3,
},
[7] = {
["CLSID"] = "{BRU3242_ADM141}",
["num"] = 7,
},
[8] = {
["CLSID"] = "{BRU3242_ADM141}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{BRU3242_ADM141}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{BRU3242_ADM141}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 10,
},
},
[4] = {
["name"] = "DEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 10,
},
[2] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{F14-LANTIRN-TP}",
["num"] = 9,
},
[4] = {
["CLSID"] = "{PHXBRU3242_2*LAU10 LS}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{F14-300gal}",
["num"] = 8,
},
[6] = {
["CLSID"] = "{F14-300gal}",
["num"] = 3,
},
[7] = {
["CLSID"] = "{BRU-32 GBU-12}",
["num"] = 7,
},
[8] = {
["CLSID"] = "{BRU-32 GBU-12}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{BRU-32 GBU-12}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{BRU-32 GBU-12}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 10,
},
},
[5] = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 10,
},
[2] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{F14-LANTIRN-TP}",
["num"] = 9,
},
[4] = {
["CLSID"] = "{SHOULDER AIM-7MH}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{F14-300gal}",
["num"] = 8,
},
[6] = {
["CLSID"] = "{F14-300gal}",
["num"] = 3,
},
[7] = {
["CLSID"] = "{BRU-32 GBU-16}",
["num"] = 7,
},
[8] = {
["CLSID"] = "{BRU-32 GBU-16}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{BRU-32 GBU-16}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{BRU-32 GBU-16}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 10,
},
},
[6] = {
["name"] = "BAI",
["pylons"] = {
[1] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 10,
},
[2] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{F14-LANTIRN-TP}",
["num"] = 9,
},
[4] = {
["CLSID"] = "{PHXBRU3242_2*LAU10 LS}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{F14-300gal}",
["num"] = 8,
},
[6] = {
["CLSID"] = "{F14-300gal}",
["num"] = 3,
},
[7] = {
["CLSID"] = "{BRU-32 MK-82}",
["num"] = 7,
},
[8] = {
["CLSID"] = "{BRU-32 MK-82}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{BRU-32 MK-20}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{BRU-32 MK-20}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 10,
},
},
[7] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{F14-LANTIRN-TP}",
["num"] = 9,
},
[2] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 10,
},
[3] = {
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
["num"] = 1,
},
[4] = {
["CLSID"] = "{PHXBRU3242_2*LAU10 LS}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{BRU-32 GBU-16}",
["num"] = 7,
},
[6] = {
["CLSID"] = "{BRU-32 GBU-16}",
["num"] = 4,
},
[7] = {
["CLSID"] = "{BRU3242_ADM141}",
["num"] = 6,
},
[8] = {
["CLSID"] = "{BRU3242_ADM141}",
["num"] = 5,
},
[9] = {
["CLSID"] = "{F14-300gal}",
["num"] = 3,
},
[10] = {
["CLSID"] = "{F14-300gal}",
["num"] = 8,
},
},
["tasks"] = {
[1] = 10,
},
},
},
["unitType"] = "F-14A-135-GR",
}
return unitPayloads

View File

@ -4,6 +4,7 @@
"authors": "Khopa",
"description": "<p>A generic bluefor coldwar faction.</p>",
"aircrafts": [
"F_14A_135_GR",
"F_14B",
"F_4E",
"F_5E_3",

View File

@ -4,6 +4,7 @@
"authors": "Khopa",
"description": "<p>A generic bluefor coldwar faction. (With the A-4E-C mod)</p>",
"aircrafts": [
"F_14A_135_GR",
"F_14B",
"F_4E",
"F_5E_3",

View File

@ -4,6 +4,7 @@
"authors": "Khopa",
"description": "<p>A generic bluefor coldwar faction. (With the A-4E-C and the MB-339 mods)</p>",
"aircrafts": [
"F_14A_135_GR",
"F_14B",
"F_4E",
"F_5E_3",

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

@ -8,7 +8,7 @@
"MiG_29A",
"F_4E",
"F_5E_3",
"F_14B",
"F_14A_135_GR",
"Su_17M4",
"Su_24M",
"Su_25",

View File

@ -61,5 +61,24 @@
"OliverHazardPerryGroupGenerator"
],
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"
"jtac_unit": "MQ_9_Reaper",
"liveries_overrides": {
"FA_18C_hornet": [
"NSAWC brown splinter",
"NAWDC black",
"VFC-12"
],
"F_15C": [
"65th Aggressor SQN (WA) MiG",
"65th Aggressor SQN (WA) MiG",
"65th Aggressor SQN (WA) SUPER_Flanker"
],
"F_16C_50": [
"usaf 64th aggressor sqn - shark",
"usaf 64th aggressor sqn-splinter",
"64th_aggressor_squadron_ghost"
], "F_14B": [
"vf-74 adversary"
]
}
}

View File

@ -6,7 +6,7 @@
"aircrafts": [
"F_5E_3",
"F_4E",
"F_14B",
"F_14A_135_GR",
"B_52H",
"UH_1H"
],

View File

@ -6,6 +6,7 @@
"aircrafts": [
"F_15C",
"F_15E",
"F_14A_135_GR",
"F_14B",
"FA_18C_hornet",
"F_16C_50",
@ -85,5 +86,25 @@
"ArleighBurkeGroupGenerator"
],
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"
"jtac_unit": "MQ_9_Reaper",
"liveries_overrides": {
"FA_18C_hornet": [
"VFA-37",
"VFA-106",
"VFA-113",
"VFA-122",
"VFA-131",
"VFA-192",
"VFA-34",
"VFA-83",
"VFA-87",
"VFA-97",
"VMFA-122",
"VMFA-132",
"VMFA-251",
"VMFA-312",
"VMFA-314",
"VMFA-323"
]
}
}

View File

@ -86,5 +86,25 @@
"OliverHazardPerryGroupGenerator"
],
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"
"jtac_unit": "MQ_9_Reaper",
"liveries_overrides": {
"FA_18C_hornet": [
"VFA-37",
"VFA-106",
"VFA-113",
"VFA-122",
"VFA-131",
"VFA-192",
"VFA-34",
"VFA-83",
"VFA-87",
"VFA-97",
"VMFA-122",
"VMFA-132",
"VMFA-251",
"VMFA-312",
"VMFA-314",
"VMFA-323"
]
}
}

View File

@ -0,0 +1,88 @@
{
"country": "USA",
"name": "US Navy 1985",
"authors": "HerrTom",
"description": "<p>Highway to the Danger Zone! For Tomcat lovers.</p>",
"aircrafts": [
"F_4E",
"F_14A_135_GR",
"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",
"liveries_overrides": {
"FA_18C_hornet": [
"VFA-37",
"VFA-106",
"VFA-113",
"VFA-122",
"VFA-131",
"VFA-192",
"VFA-34",
"VFA-83",
"VFA-87",
"VFA-97"
]
}
}

View File

@ -0,0 +1,58 @@
<b>You are clear for takeoff</b>
<p>
Some player flights may be delayed to start. For such flights, it will not be
possible to enter the cockpit for a delayed flight until its mission start
time, shown in the flight information window.
</p>
<p>
To reduce delays, schedule packages with player flights with an earlier TOT.
Note that if some flights within the package will take a long time to reach the
target, a player flight may still be delayed.
</p>
<p>
To avoid delays entirely, use the "Never delay player flights" option in the
mission generation settings. Note that this will <strong>not</strong> adjust
the timing of your mission; this option only allows you to wait in the
cockpit.
</p>
<p>
For more information, see the mission planning documentation on
<a href="https://github.com/Khopa/dcs_liberation/wiki/Mission-planning">
the wiki</a>.
</p>
<h2>For Singleplayer:</h2>
<p>
In DCS, open the Mission Editor and load the file: <i>liberation_nextturn</i>.
</p>
<p>
Once the mission is loaded in the ME, use the "FLY" option in the "Flight"
menu to launch.
</p>
<h2>For Multiplayer:</h2>
<p>
In DCS, open the Mission Editor, and load the file: <i>liberation_nextturn</i>
</p>
<p>Select File/Save, exit the mission editor, and then select Multiplayer.</p>
<p>Then host a server with the mission, and tell your friends to join!</p>
<i>(The step in the mission editor is important, and fix a game breaking bug.)</i>
<h2>Finishing</h2>
<p>Once you have played the mission, click on the \"Accept Results\" button.</p>
<p>
If DCS Liberation does not detect mission end, use the manually submit button,
and choose the state.json file.
</p>

View File

@ -99,6 +99,10 @@ class TheaterGroundObject(MissionTarget):
"""The name of the unit group."""
return f"{self.category}|{self.group_id}"
@property
def waypoint_name(self) -> str:
return f"[{self.name}] {self.category}"
def __str__(self) -> str:
return NAME_BY_CATEGORY[self.category]
@ -136,6 +140,10 @@ class BuildingGroundObject(TheaterGroundObject):
"""The name of the unit group."""
return f"{self.category}|{self.group_id}|{self.object_id}"
@property
def waypoint_name(self) -> str:
return f"{super().waypoint_name} #{self.object_id}"
class GenericCarrierGroundObject(TheaterGroundObject):
pass