Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
David Pierron 2020-10-07 09:21:41 +02:00
commit 59d6cc7625
171 changed files with 7618 additions and 3782 deletions

View File

@ -1,4 +1,7 @@
# 2.1.3
# 2.1.4
## Fixes :
* **[UI]** Fixed an issue that prevented generating the mission (take off button no working) on old savegames.
## Features/Improvements :
* **[Units/Factions]** Added A-10C_2 to USA 2005 and Bluefor modern factions

View File

@ -1,4 +1,22 @@
from dcs.planes import *
from dcs.planes import (
Bf_109K_4,
C_101CC,
FW_190A8,
FW_190D9,
F_5E_3,
F_86F_Sabre,
I_16,
L_39ZA,
MiG_15bis,
MiG_19P,
MiG_21Bis,
P_47D_30,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
)
from pydcs_extensions.a4ec.a4ec import A_4E_C
"""

View File

@ -1,95 +1,101 @@
from dataclasses import dataclass
from game.utils import nm_to_meter, feet_to_meter
MODERN_DOCTRINE = {
"GENERATORS": {
"CAS": True,
"CAP": True,
"SEAD": True,
"STRIKE": True,
"ANTISHIP": True,
},
@dataclass(frozen=True)
class Doctrine:
cas: bool
cap: bool
sead: bool
strike: bool
antiship: bool
"STRIKE_MAX_RANGE": 1500000,
"SEAD_MAX_RANGE": 1500000,
strike_max_range: int
sead_max_range: int
"CAP_EVERY_X_MINUTES": 20,
"CAS_EVERY_X_MINUTES": 30,
"SEAD_EVERY_X_MINUTES": 40,
"STRIKE_EVERY_X_MINUTES": 40,
rendezvous_altitude: int
join_distance: int
split_distance: int
ingress_egress_distance: int
ingress_altitude: int
egress_altitude: int
"INGRESS_EGRESS_DISTANCE": nm_to_meter(45),
"INGRESS_ALT": feet_to_meter(20000),
"EGRESS_ALT": feet_to_meter(20000),
"PATROL_ALT_RANGE": (feet_to_meter(15000), feet_to_meter(33000)),
"PATTERN_ALTITUDE": feet_to_meter(5000),
min_patrol_altitude: int
max_patrol_altitude: int
pattern_altitude: int
"CAP_PATTERN_LENGTH": (nm_to_meter(15), nm_to_meter(40)),
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(6), nm_to_meter(15)),
"CAP_DISTANCE_FROM_CP": (nm_to_meter(10), nm_to_meter(40)),
cap_min_track_length: int
cap_max_track_length: int
cap_min_distance_from_cp: int
cap_max_distance_from_cp: int
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
}
COLDWAR_DOCTRINE = {
MODERN_DOCTRINE = Doctrine(
cap=True,
cas=True,
sead=True,
strike=True,
antiship=True,
strike_max_range=1500000,
sead_max_range=1500000,
rendezvous_altitude=feet_to_meter(25000),
join_distance=nm_to_meter(20),
split_distance=nm_to_meter(20),
ingress_egress_distance=nm_to_meter(45),
ingress_altitude=feet_to_meter(20000),
egress_altitude=feet_to_meter(20000),
min_patrol_altitude=feet_to_meter(15000),
max_patrol_altitude=feet_to_meter(33000),
pattern_altitude=feet_to_meter(5000),
cap_min_track_length=nm_to_meter(15),
cap_max_track_length=nm_to_meter(40),
cap_min_distance_from_cp=nm_to_meter(10),
cap_max_distance_from_cp=nm_to_meter(40),
)
"GENERATORS": {
"CAS": True,
"CAP": True,
"SEAD": True,
"STRIKE": True,
"ANTISHIP": True,
},
COLDWAR_DOCTRINE = Doctrine(
cap=True,
cas=True,
sead=True,
strike=True,
antiship=True,
strike_max_range=1500000,
sead_max_range=1500000,
rendezvous_altitude=feet_to_meter(22000),
join_distance=nm_to_meter(10),
split_distance=nm_to_meter(10),
ingress_egress_distance=nm_to_meter(30),
ingress_altitude=feet_to_meter(18000),
egress_altitude=feet_to_meter(18000),
min_patrol_altitude=feet_to_meter(10000),
max_patrol_altitude=feet_to_meter(24000),
pattern_altitude=feet_to_meter(5000),
cap_min_track_length=nm_to_meter(12),
cap_max_track_length=nm_to_meter(24),
cap_min_distance_from_cp=nm_to_meter(8),
cap_max_distance_from_cp=nm_to_meter(25),
)
"STRIKE_MAX_RANGE": 1500000,
"SEAD_MAX_RANGE": 1500000,
"CAP_EVERY_X_MINUTES": 20,
"CAS_EVERY_X_MINUTES": 30,
"SEAD_EVERY_X_MINUTES": 40,
"STRIKE_EVERY_X_MINUTES": 40,
"INGRESS_EGRESS_DISTANCE": nm_to_meter(30),
"INGRESS_ALT": feet_to_meter(18000),
"EGRESS_ALT": feet_to_meter(18000),
"PATROL_ALT_RANGE": (feet_to_meter(10000), feet_to_meter(24000)),
"PATTERN_ALTITUDE": feet_to_meter(5000),
"CAP_PATTERN_LENGTH": (nm_to_meter(12), nm_to_meter(24)),
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(2), nm_to_meter(8)),
"CAP_DISTANCE_FROM_CP": (nm_to_meter(8), nm_to_meter(25)),
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
}
WWII_DOCTRINE = {
"GENERATORS": {
"CAS": True,
"CAP": True,
"SEAD": False,
"STRIKE": True,
"ANTISHIP": True,
},
"STRIKE_MAX_RANGE": 1500000,
"SEAD_MAX_RANGE": 1500000,
"CAP_EVERY_X_MINUTES": 20,
"CAS_EVERY_X_MINUTES": 30,
"SEAD_EVERY_X_MINUTES": 40,
"STRIKE_EVERY_X_MINUTES": 40,
"INGRESS_EGRESS_DISTANCE": nm_to_meter(7),
"INGRESS_ALT": feet_to_meter(8000),
"EGRESS_ALT": feet_to_meter(8000),
"PATROL_ALT_RANGE": (feet_to_meter(4000), feet_to_meter(15000)),
"PATTERN_ALTITUDE": feet_to_meter(5000),
"CAP_PATTERN_LENGTH": (nm_to_meter(8), nm_to_meter(18)),
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(1), nm_to_meter(6)),
"CAP_DISTANCE_FROM_CP": (nm_to_meter(0), nm_to_meter(5)),
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
}
WWII_DOCTRINE = Doctrine(
cap=True,
cas=True,
sead=False,
strike=True,
antiship=True,
strike_max_range=1500000,
sead_max_range=1500000,
join_distance=nm_to_meter(5),
split_distance=nm_to_meter(5),
rendezvous_altitude=feet_to_meter(10000),
ingress_egress_distance=nm_to_meter(7),
ingress_altitude=feet_to_meter(8000),
egress_altitude=feet_to_meter(8000),
min_patrol_altitude=feet_to_meter(4000),
max_patrol_altitude=feet_to_meter(15000),
pattern_altitude=feet_to_meter(5000),
cap_min_track_length=nm_to_meter(8),
cap_max_track_length=nm_to_meter(18),
cap_min_distance_from_cp=nm_to_meter(0),
cap_max_distance_from_cp=nm_to_meter(5),
)

View File

@ -1,5 +1,26 @@
from dcs.ships import (
CGN_1144_2_Pyotr_Velikiy,
CG_1164_Moskva,
CVN_70_Carl_Vinson,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017,
FFG_11540_Neustrashimy,
FFL_1124_4_Grisha,
FF_1135M_Rezky,
FSG_1241_1MP_Molniya,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
Type_052B_Destroyer,
Type_052C_Destroyer,
Type_054A_Frigate,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import AirDefence
from dcs.ships import *
UNITS_WITH_RADAR = [

View File

@ -1,34 +1,177 @@
import typing
import enum
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from dcs.countries import get_by_id, country_dict
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
from dcs.task import *
from dcs.unit import *
from dcs.unittype import *
from dcs.unitgroup import *
from dcs.countries import country_dict
from dcs.helicopters import (
AH_1W,
AH_64A,
AH_64D,
HelicopterType,
Ka_50,
Mi_24V,
Mi_28N,
Mi_8MT,
OH_58D,
SA342L,
SA342M,
SA342Minigun,
SA342Mistral,
UH_1H,
UH_60A,
helicopter_map,
)
from dcs.mapping import Point
# mypy can't resolve these if they're wildcard imports for some reason.
from dcs.planes import (
AJS37,
AV8BNA,
A_10A,
A_10C,
A_10C_2,
A_20G,
A_50,
An_26B,
An_30M,
B_17G,
B_1B,
B_52H,
Bf_109K_4,
C_101CC,
C_130,
E_3A,
FA_18C_hornet,
FW_190A8,
FW_190D9,
F_14B,
F_15C,
F_15E,
F_16A,
F_16C_50,
F_4E,
F_5E_3,
F_86F_Sabre,
F_A_18C,
IL_76MD,
IL_78M,
JF_17,
J_11A,
Ju_88A4,
KC130,
KC_135,
KJ_2000,
L_39C,
L_39ZA,
MQ_9_Reaper,
M_2000C,
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_27K,
MiG_29A,
MiG_29G,
MiG_29S,
MiG_31,
Mirage_2000_5,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
PlaneType,
RQ_1A_Predator,
S_3B_Tanker,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_27,
Su_30,
Su_33,
Su_34,
Tornado_GR4,
Tornado_IDS,
WingLoong_I,
Yak_40,
plane_map,
)
from dcs.ships import (
Armed_speedboat,
Bulk_cargo_ship_Yakushev,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017,
Dry_cargo_ship_Ivanov,
LHA_1_Tarawa,
Tanker_Elnya_160,
ship_map,
)
from dcs.task import (
AWACS,
AntishipStrike,
CAP,
CAS,
CargoTransportation,
Embarking,
Escort,
GroundAttack,
Intercept,
MainTask,
Nothing,
PinpointStrike,
Reconnaissance,
Refueling,
SEAD,
Task,
Transport,
)
from dcs.terrain.terrain import Airport
from dcs.unit import Ship, Unit, Vehicle
from dcs.unitgroup import ShipGroup, StaticGroup
from dcs.unittype import FlyingType, ShipType, UnitType, VehicleType
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Carriage,
Infantry,
Unarmed,
vehicle_map,
)
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from game.factions.australia_2005 import Australia_2005
from game.factions.bluefor_coldwar import BLUEFOR_COLDWAR
from game.factions.bluefor_coldwar_a4 import BLUEFOR_COLDWAR_A4
from game.factions.bluefor_coldwar_mods import BLUEFOR_COLDWAR_MODS
from game.factions.bluefor_modern import BLUEFOR_MODERN
from game.factions.canada_2005 import Canada_2005
from game.factions.china_2010 import China_2010
from game.factions.france_1995 import France_1995
from game.factions.france_2005 import France_2005
from game.factions.france_modded import France_2005_Modded
from game.factions.germany_1944 import Germany_1944
from game.factions.germany_1944_easy import Germany_1944_Easy
from game.factions.germany_1990 import Germany_1990
from game.factions.india_2010 import India_2010
from game.factions.insurgent import Insurgent
from game.factions.insurgent_modded import Insurgent_modded
from game.factions.iran_2015 import Iran_2015
from game.factions.israel_1948 import Israel_1948
from game.factions.israel_1973 import Israel_1973, Israel_1973_NO_WW2_UNITS, Israel_1982
from game.factions.israel_1973 import (
Israel_1973,
Israel_1973_NO_WW2_UNITS,
Israel_1982,
)
from game.factions.israel_2000 import Israel_2000
from game.factions.italy_1990 import Italy_1990
from game.factions.italy_1990_mb339 import Italy_1990_MB339
@ -37,35 +180,41 @@ from game.factions.libya_2011 import Libya_2011
from game.factions.netherlands_1990 import Netherlands_1990
from game.factions.north_korea_2000 import NorthKorea_2000
from game.factions.pakistan_2015 import Pakistan_2015
from game.factions.private_miltary_companies import PMC_WESTERN_B, PMC_RUSSIAN, PMC_WESTERN_A
from game.factions.russia_1975 import Russia_1975
from game.factions.germany_1944 import Germany_1944
from game.factions.india_2010 import India_2010
from game.factions.private_miltary_companies import (
PMC_RUSSIAN,
PMC_WESTERN_A,
PMC_WESTERN_B,
)
from game.factions.russia_1955 import Russia_1955
from game.factions.russia_1965 import Russia_1965
from game.factions.russia_1975 import Russia_1975
from game.factions.russia_1990 import Russia_1990
from game.factions.russia_2010 import Russia_2010
from game.factions.spain_1990 import Spain_1990
from game.factions.sweden_1990 import Sweden_1990
from game.factions.syria import Syria_2011, Syria_1967, Syria_1967_WW2_Weapons, Syria_1973, Arab_Armies_1948, Syria_1982
from game.factions.syria import (
Arab_Armies_1948,
Syria_1967,
Syria_1967_WW2_Weapons,
Syria_1973,
Syria_1982,
Syria_2011,
)
from game.factions.turkey_2005 import Turkey_2005
from game.factions.uae_2005 import UAE_2005
from game.factions.uk_1944 import UK_1944
from game.factions.uk_1990 import UnitedKingdom_1990
from game.factions.ukraine_2010 import Ukraine_2010
from game.factions.us_aggressors import US_Aggressors
from game.factions.usa_1944 import USA_1944, ALLIES_1944
from game.factions.usa_1944 import ALLIES_1944, USA_1944
from game.factions.usa_1955 import USA_1955
from game.factions.usa_1960 import USA_1960
from game.factions.usa_1965 import USA_1965
from game.factions.usa_1990 import USA_1990
from game.factions.usa_2005 import USA_2005
from game.factions.bluefor_modern import BLUEFOR_MODERN
# PATCH pydcs data with MODS
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.mb339.mb339 import MB_339PAN
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M
plane_map["A-4E-C"] = A_4E_C
@ -793,13 +942,13 @@ SAM_CONVERT = {
"""
Units that will always be spawned in the air
"""
TAKEOFF_BAN = [
TAKEOFF_BAN: List[Type[FlyingType]] = [
]
"""
Units that will be always spawned in the air if launched from the carrier
"""
CARRIER_TAKEOFF_BAN = [
CARRIER_TAKEOFF_BAN: List[Type[FlyingType]] = [
Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned
]
@ -807,7 +956,7 @@ CARRIER_TAKEOFF_BAN = [
Units separated by country.
country : DCS Country name
"""
FACTIONS = {
FACTIONS: Dict[str, Dict[str, Any]] = {
"Bluefor Modern": BLUEFOR_MODERN,
"Bluefor Cold War 1970s": BLUEFOR_COLDWAR,
@ -934,10 +1083,11 @@ COMMON_OVERRIDE = {
PinpointStrike: "STRIKE",
SEAD: "SEAD",
AntishipStrike: "ANTISHIP",
GroundAttack: "STRIKE"
GroundAttack: "STRIKE",
Escort: "CAP",
}
PLANE_PAYLOAD_OVERRIDES = {
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
FA_18C_hornet: {
CAP: "CAP HEAVY",
@ -946,7 +1096,8 @@ PLANE_PAYLOAD_OVERRIDES = {
PinpointStrike: "STRIKE",
SEAD: "SEAD",
AntishipStrike: "ANTISHIP",
GroundAttack: "STRIKE"
GroundAttack: "STRIKE",
Escort: "CAP HEAVY",
},
F_A_18C: {
CAP: "CAP HEAVY",
@ -955,7 +1106,8 @@ PLANE_PAYLOAD_OVERRIDES = {
PinpointStrike: "STRIKE",
SEAD: "SEAD",
AntishipStrike: "ANTISHIP",
GroundAttack: "STRIKE"
GroundAttack: "STRIKE",
Escort: "CAP HEAVY",
},
A_10A: COMMON_OVERRIDE,
A_10C: COMMON_OVERRIDE,
@ -1134,17 +1286,17 @@ LHA_CAPABLE = [
---------- END OF CONFIGURATION SECTION
"""
UnitsDict = typing.Dict[UnitType, int]
PlaneDict = typing.Dict[FlyingType, int]
HeliDict = typing.Dict[HelicopterType, int]
ArmorDict = typing.Dict[VehicleType, int]
ShipDict = typing.Dict[ShipType, int]
AirDefenseDict = typing.Dict[AirDefence, int]
UnitsDict = Dict[UnitType, int]
PlaneDict = Dict[FlyingType, int]
HeliDict = Dict[HelicopterType, int]
ArmorDict = Dict[VehicleType, int]
ShipDict = Dict[ShipType, int]
AirDefenseDict = Dict[AirDefence, int]
AssignedUnitsDict = typing.Dict[typing.Type[UnitType], typing.Tuple[int, int]]
TaskForceDict = typing.Dict[typing.Type[Task], AssignedUnitsDict]
AssignedUnitsDict = Dict[Type[UnitType], Tuple[int, int]]
TaskForceDict = Dict[Type[MainTask], AssignedUnitsDict]
StartingPosition = typing.Optional[typing.Union[ShipGroup, StaticGroup, Airport, Point]]
StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
def upgrade_to_supercarrier(unit, name: str):
@ -1162,7 +1314,7 @@ def upgrade_to_supercarrier(unit, name: str):
else:
return unit
def unit_task(unit: UnitType) -> Task:
def unit_task(unit: UnitType) -> Optional[Task]:
for task, units in UNIT_BY_TASK.items():
if unit in units:
return task
@ -1173,10 +1325,10 @@ def unit_task(unit: UnitType) -> Task:
print(unit.name + " cause issue")
return None
def find_unittype(for_task: Task, country_name: str) -> typing.List[UnitType]:
def find_unittype(for_task: Task, country_name: str) -> List[UnitType]:
return [x for x in UNIT_BY_TASK[for_task] if x in FACTIONS[country_name]["units"]]
def find_infantry(country_name: str) -> typing.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.Soldier_RPG,
@ -1199,7 +1351,7 @@ def unit_type_name(unit_type) -> str:
def unit_type_name_2(unit_type) -> str:
return unit_type.name and unit_type.name or unit_type.id
def unit_type_from_name(name: str) -> UnitType:
def unit_type_from_name(name: str) -> Optional[UnitType]:
if name in vehicle_map:
return vehicle_map[name]
elif name in plane_map:
@ -1232,7 +1384,7 @@ def task_name(task) -> str:
return task.name
def choose_units(for_task: Task, factor: float, count: int, country: str) -> typing.Collection[UnitType]:
def choose_units(for_task: Task, factor: float, count: int, country: str) -> List[UnitType]:
suitable_unittypes = find_unittype(for_task, country)
suitable_unittypes = [x for x in suitable_unittypes if x not in helicopter_map.values()]
suitable_unittypes.sort(key=lambda x: PRICES[x])
@ -1258,7 +1410,7 @@ def unitdict_merge(a: UnitsDict, b: UnitsDict) -> UnitsDict:
def unitdict_split(unit_dict: UnitsDict, count: int):
buffer_dict = {}
buffer_dict: Dict[UnitType, int] = {}
for unit_type, unit_count in unit_dict.items():
for _ in range(unit_count):
unitdict_append(buffer_dict, unit_type, 1)
@ -1281,7 +1433,7 @@ def unitdict_restrict_count(unit_dict: UnitsDict, total_count: int) -> UnitsDict
return {}
def assigned_units_split(fd: AssignedUnitsDict) -> typing.Tuple[PlaneDict, PlaneDict]:
def assigned_units_split(fd: AssignedUnitsDict) -> Tuple[PlaneDict, PlaneDict]:
return {k: v1 for k, (v1, v2) in fd.items()}, {k: v2 for k, (v1, v2) in fd.items()},
@ -1290,7 +1442,7 @@ def assigned_units_from(d: PlaneDict) -> AssignedUnitsDict:
def assignedunits_split_to_count(dict: AssignedUnitsDict, count: int):
buffer_dict = {}
buffer_dict: Dict[Type[UnitType], Tuple[int, int]] = {}
for unit_type, (unit_count, client_count) in dict.items():
for _ in range(unit_count):
new_count, new_client_count = buffer_dict.get(unit_type, (0, 0))

View File

@ -1,24 +1,24 @@
import typing
from __future__ import annotations
import logging
import math
from typing import Dict, List, Optional, Type, TYPE_CHECKING
from dcs.action import Coalition
from dcs.unittype import UnitType
from dcs.task import *
from dcs.vehicles import AirDefence
from dcs.mapping import Point
from dcs.task import Task
from dcs.unittype import UnitType
from game import *
from game import db, persistency
from game.debriefing import Debriefing
from game.infos.information import Information
from theater import *
from game.operation.operation import Operation
from gen.environmentgen import EnvironmentSettings
from gen.conflictgen import Conflict
from game.db import assigned_units_from, unitdict_from
from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
from theater.start_generator import generate_airbase_defense_group
from userdata.debriefing import Debriefing
from userdata import persistency
import game.db as db
if TYPE_CHECKING:
from ..game import Game
DIFFICULTY_LOG_BASE = 1.1
EVENT_DEPARTURE_MAX_DISTANCE = 340000
@ -28,6 +28,7 @@ MINOR_DEFEAT_INFLUENCE = 0.1
DEFEAT_INFLUENCE = 0.3
STRONG_DEFEAT_INFLUENCE = 0.5
class Event:
silent = False
informational = False
@ -37,7 +38,6 @@ class Event:
game = None # type: Game
location = None # type: Point
from_cp = None # type: ControlPoint
departure_cp = None # type: ControlPoint
to_cp = None # type: ControlPoint
operation = None # type: Operation
@ -47,7 +47,7 @@ class Event:
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
self.game = game
self.departure_cp = None
self.departure_cp: Optional[ControlPoint] = None
self.from_cp = from_cp
self.to_cp = target_cp
self.location = location
@ -59,14 +59,14 @@ class Event:
return self.attacker_name == self.game.player_name
@property
def enemy_cp(self) -> ControlPoint:
def enemy_cp(self) -> Optional[ControlPoint]:
if self.attacker_name == self.game.player_name:
return self.to_cp
else:
return self.departure_cp
@property
def tasks(self) -> typing.Collection[typing.Type[Task]]:
def tasks(self) -> List[Type[Task]]:
return []
@property
@ -91,18 +91,6 @@ class Event:
def is_successfull(self, debriefing: Debriefing) -> bool:
return self.operation.is_successfull(debriefing)
def player_attacking(self, cp: ControlPoint, flights: db.TaskForceDict):
if self.is_player_attacking:
self.departure_cp = cp
else:
self.to_cp = cp
def player_defending(self, cp: ControlPoint, flights: db.TaskForceDict):
if self.is_player_attacking:
self.departure_cp = cp
else:
self.to_cp = cp
def generate(self):
self.operation.is_awacs_enabled = self.is_awacs_enabled
self.operation.ca_slots = self.ca_slots
@ -253,7 +241,7 @@ class Event:
for enemy_cp in enemy_cps:
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
delta = 0
delta = 0.0
player_won = True
ally_casualties = killed_unit_count_by_cp[cp.id]
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id]
@ -376,7 +364,6 @@ class Event:
class UnitsDeliveryEvent(Event):
informational = True
units = None # type: typing.Dict[UnitType, int]
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
super(UnitsDeliveryEvent, self).__init__(game=game,
@ -386,12 +373,12 @@ class UnitsDeliveryEvent(Event):
attacker_name=attacker_name,
defender_name=defender_name)
self.units = {}
self.units: Dict[UnitType, int] = {}
def __str__(self):
return "Pending delivery to {}".format(self.to_cp)
def deliver(self, units: typing.Dict[UnitType, int]):
def deliver(self, units: Dict[UnitType, int]):
for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v

View File

@ -1,12 +1,17 @@
from game.event import *
from typing import List, Type
from dcs.task import CAP, CAS, Task
from game import db
from game.operation.frontlineattack import FrontlineAttackOperation
from userdata.debriefing import Debriefing
from .event import Event
from ..debriefing import Debriefing
class FrontlineAttackEvent(Event):
@property
def tasks(self) -> typing.Collection[typing.Type[Task]]:
def tasks(self) -> List[Type[Task]]:
if self.is_player_attacking:
return [CAS, CAP]
else:
@ -34,6 +39,7 @@ class FrontlineAttackEvent(Event):
self.to_cp.base.affect_strength(-0.1)
def player_attacking(self, flights: db.TaskForceDict):
assert self.departure_cp is not None
op = FrontlineAttackOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
@ -41,13 +47,3 @@ class FrontlineAttackEvent(Event):
departure_cp=self.departure_cp,
to_cp=self.to_cp)
self.operation = op
def player_defending(self, flights: db.TaskForceDict):
op = FrontlineAttackOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
from_cp=self.from_cp,
departure_cp=self.departure_cp,
to_cp=self.to_cp)
self.operation = op

View File

@ -1,7 +1,27 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_1W,
UH_1H,
)
from dcs.planes import (
C_130,
E_3A,
FA_18C_hornet,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Ticonderoga_class,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Australia_2005 = {
"country": "Australia",

View File

@ -1,7 +1,30 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
SA342L,
SA342M,
UH_1H,
)
from dcs.planes import (
AJS37,
A_10A,
C_130,
E_3A,
F_14B,
F_4E,
F_5E_3,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
BLUEFOR_COLDWAR = {
"country": "Combined Joint Task Forces Blue",

View File

@ -1,7 +1,31 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
SA342L,
SA342M,
UH_1H,
)
from dcs.planes import (
AJS37,
A_10A,
C_130,
E_3A,
F_14B,
F_4E,
F_5E_3,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Ticonderoga_class,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
from pydcs_extensions.a4ec.a4ec import A_4E_C

View File

@ -1,7 +1,31 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
SA342L,
SA342M,
UH_1H,
)
from dcs.planes import (
AJS37,
A_10A,
C_130,
E_3A,
F_14B,
F_4E,
F_5E_3,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Ticonderoga_class,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.mb339.mb339 import MB_339PAN

View File

@ -1,7 +1,45 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_64D,
Ka_50,
SA342L,
SA342M,
UH_1H,
)
from dcs.planes import (
AJS37,
AV8BNA,
A_10A,
A_10C,
A_10C_2,
C_130,
E_3A,
FA_18C_hornet,
F_14B,
F_15C,
F_16C_50,
F_5E_3,
JF_17,
KC130,
KC_135,
M_2000C,
Su_25T,
Su_27,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
BLUEFOR_MODERN = {
"country": "Combined Joint Task Forces Blue",

View File

@ -1,7 +1,26 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
UH_1H,
)
from dcs.planes import (
C_130,
E_3A,
FA_18C_hornet,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Ticonderoga_class,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Canada_2005 = {
"country": "Canada",

View File

@ -1,7 +1,38 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
Mi_28N,
Mi_8MT,
)
from dcs.planes import (
An_26B,
An_30M,
IL_76MD,
IL_78M,
JF_17,
J_11A,
KJ_2000,
MiG_21Bis,
Su_30,
Su_33,
WingLoong_I,
Yak_40,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
Type_052B_Destroyer,
Type_052C_Destroyer,
Type_054A_Frigate,
Type_071_Amphibious_Transport_Dock,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
China_2010 = {
"country": "China",

View File

@ -1,7 +1,28 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
SA342L,
SA342M,
SA342Mistral,
)
from dcs.planes import (
C_130,
E_3A,
KC130,
KC_135,
M_2000C,
Mirage_2000_5,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
France_1995 = {
"country": "France",

View File

@ -1,7 +1,31 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
SA342L,
SA342M,
SA342Mistral,
)
from dcs.planes import (
C_130,
E_3A,
FA_18C_hornet,
KC130,
KC_135,
M_2000C,
Mirage_2000_5,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
France_2005 = {
"country": "France",

View File

@ -1,10 +1,33 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
SA342L,
SA342M,
SA342Mistral,
)
from dcs.planes import (
C_130,
E_3A,
KC130,
KC_135,
M_2000C,
Mirage_2000_5,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M
France_2005_Modded = {
"country": "France",

View File

@ -1,5 +1,16 @@
from dcs.planes import *
from dcs.vehicles import *
from dcs.planes import (
Bf_109K_4,
FW_190A8,
FW_190D9,
Ju_88A4,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
from game.data.building_data import WW2_GERMANY_BUILDINGS
from game.data.doctrine import WWII_DOCTRINE

View File

@ -1,5 +1,16 @@
from dcs.planes import *
from dcs.vehicles import *
from dcs.planes import (
Bf_109K_4,
FW_190A8,
FW_190D9,
Ju_88A4,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
from game.data.building_data import WW2_GERMANY_BUILDINGS
from game.data.doctrine import WWII_DOCTRINE

View File

@ -1,7 +1,28 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
SA342L,
SA342M,
UH_1H,
)
from dcs.planes import (
C_130,
E_3A,
F_4E,
KC130,
KC_135,
MiG_29G,
Tornado_IDS,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Germany_1990 = {
"country": "Germany",

View File

@ -1,7 +1,32 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_64A,
Mi_8MT,
)
from dcs.planes import (
C_130,
E_3A,
KC130,
KC_135,
M_2000C,
MiG_21Bis,
MiG_27K,
MiG_29S,
Mirage_2000_5,
Su_30,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
FSG_1241_1MP_Molniya,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
India_2010 = {
"country": "India",

View File

@ -1,7 +1,14 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Insurgent = {
"country": "Insurgents",

View File

@ -1,8 +1,21 @@
from dcs.ships import *
from dcs.vehicles import *
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
from pydcs_extensions.frenchpack.frenchpack import DIM__TOYOTA_BLUE, DIM__TOYOTA_DESERT, DIM__TOYOTA_GREEN, \
DIM__KAMIKAZE
from pydcs_extensions.frenchpack.frenchpack import (
DIM__KAMIKAZE,
DIM__TOYOTA_BLUE,
DIM__TOYOTA_DESERT,
DIM__TOYOTA_GREEN,
)
Insurgent_modded = {
"country": "Insurgents",

View File

@ -1,7 +1,35 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
Mi_24V,
Mi_28N,
)
from dcs.planes import (
A_50,
An_26B,
An_30M,
F_14B,
F_4E,
F_5E_3,
IL_76MD,
IL_78M,
MiG_21Bis,
MiG_29A,
Su_17M4,
Su_24M,
Su_25,
Yak_40,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Iran_2015 = {
"country": "Iran",

View File

@ -1,6 +1,20 @@
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.planes import (
B_17G,
Bf_109K_4,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
)
from dcs.ships import (
Armed_speedboat,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Israel_1948 = {
"country": "Israel",

View File

@ -1,7 +1,26 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_1W,
UH_1H,
)
from dcs.planes import (
C_130,
E_3A,
F_15C,
F_16A,
F_16C_50,
F_4E,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
from pydcs_extensions.a4ec.a4ec import A_4E_C

View File

@ -1,7 +1,29 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_1W,
AH_64D,
)
from dcs.planes import (
C_130,
E_3A,
F_15C,
F_15E,
F_16C_50,
F_4E,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
Israel_2000 = {
"country": "Israel",

View File

@ -1,7 +1,28 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_1W,
UH_1H,
)
from dcs.planes import (
AV8BNA,
C_130,
E_3A,
KC_135,
S_3B_Tanker,
Tornado_IDS,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Italy_1990 = {
"country": "Italy",

View File

@ -1,7 +1,28 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_1W,
UH_1H,
)
from dcs.planes import (
AV8BNA,
C_130,
E_3A,
KC_135,
S_3B_Tanker,
Tornado_IDS,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
from pydcs_extensions.mb339.mb339 import MB_339PAN

View File

@ -1,7 +1,24 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_1W,
AH_64D,
)
from dcs.planes import (
C_130,
E_3A,
F_15C,
F_16C_50,
F_4E,
KC130,
KC_135,
)
from dcs.ships import LHA_1_Tarawa, Ticonderoga_class, USS_Arleigh_Burke_IIa
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
Japan_2005 = {
"country": "Japan",

View File

@ -1,6 +1,25 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.vehicles import *
from dcs.helicopters import (
Mi_24V,
)
from dcs.planes import (
A_50,
An_26B,
An_30M,
IL_76MD,
IL_78M,
MiG_21Bis,
MiG_23MLD,
Su_17M4,
Su_24M,
Yak_40,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
Libya_2011 = {
"country": "Libya",

View File

@ -1,7 +1,25 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_64A,
)
from dcs.planes import (
C_130,
E_3A,
F_16C_50,
F_5E_3,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Netherlands_1990 = {
"country": "The Netherlands",

View File

@ -1,7 +1,33 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
Mi_24V,
Mi_8MT,
)
from dcs.planes import (
A_50,
An_26B,
An_30M,
IL_76MD,
IL_78M,
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_29A,
Su_25,
Yak_40,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
NorthKorea_2000 = {
"country": "North Korea",

View File

@ -1,7 +1,25 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_1W,
UH_1H,
)
from dcs.planes import (
E_3A,
F_16C_50,
IL_78M,
JF_17,
MiG_19P,
MiG_21Bis,
WingLoong_I,
)
from dcs.ships import (
Armed_speedboat,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Pakistan_2015 = {
"country": "Pakistan",

View File

@ -1,7 +1,25 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
Ka_50,
Mi_24V,
Mi_8MT,
OH_58D,
SA342M,
UH_1H,
)
from dcs.planes import (
C_101CC,
L_39C,
L_39ZA,
)
from dcs.ships import (
Armed_speedboat,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
from pydcs_extensions.mb339.mb339 import MB_339PAN

View File

@ -1,6 +1,11 @@
from dcs.planes import MiG_15bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40
from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
from dcs.planes import An_26B, An_30M, IL_76MD, IL_78M, MiG_15bis, Yak_40
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
)
from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed
Russia_1955 = {
"country": "Russia",

View File

@ -1,7 +1,22 @@
from dcs.helicopters import Mi_8MT
from dcs.planes import MiG_15bis, MiG_19P, MiG_21Bis, IL_76MD, IL_78M, An_26B, An_30M, Yak_40, A_50
from dcs.ships import CV_1143_5_Admiral_Kuznetsov, Bulk_cargo_ship_Yakushev, Dry_cargo_ship_Ivanov, Tanker_Elnya_160
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
from dcs.planes import (
A_50,
An_26B,
An_30M,
IL_76MD,
IL_78M,
MiG_15bis,
MiG_19P,
MiG_21Bis,
Yak_40,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
)
from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed
Russia_1965 = {
"country": "Russia",

View File

@ -1,8 +1,31 @@
from dcs.helicopters import Mi_8MT, Mi_24V
from dcs.planes import MiG_21Bis, MiG_23MLD, MiG_25PD, MiG_29A, Su_17M4, Su_24M, Su_25, IL_76MD, IL_78M, An_26B, An_30M, \
Yak_40, A_50
from dcs.ships import *
from dcs.vehicles import AirDefence, Armor, Unarmed, Infantry, Artillery
from dcs.helicopters import (
Mi_24V,
Mi_8MT,
)
from dcs.planes import (
A_50,
An_26B,
An_30M,
IL_76MD,
IL_78M,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_29A,
Su_17M4,
Su_24M,
Su_25,
Yak_40,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CGN_1144_2_Pyotr_Velikiy,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
FF_1135M_Rezky,
Tanker_Elnya_160,
)
from dcs.vehicles import AirDefence, Armor, Artillery, Infantry, Unarmed
Russia_1975 = {
"country": "Russia",

View File

@ -1,7 +1,39 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
Ka_50,
Mi_24V,
Mi_8MT,
)
from dcs.planes import (
A_50,
An_26B,
An_30M,
IL_76MD,
IL_78M,
MiG_23MLD,
MiG_25PD,
MiG_29A,
MiG_29S,
MiG_31,
Su_24M,
Su_25,
Su_27,
Yak_40,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
FF_1135M_Rezky,
FSG_1241_1MP_Molniya,
Tanker_Elnya_160,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
Russia_1990 = {
"country": "Russia",

View File

@ -1,7 +1,42 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
Ka_50,
Mi_24V,
Mi_28N,
Mi_8MT,
)
from dcs.planes import (
A_50,
An_26B,
An_30M,
IL_76MD,
IL_78M,
L_39ZA,
MiG_29S,
MiG_31,
Su_24M,
Su_25,
Su_25T,
Su_27,
Su_30,
Su_33,
Su_34,
Yak_40,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
FF_1135M_Rezky,
FSG_1241_1MP_Molniya,
Tanker_Elnya_160,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
Russia_2010 = {
"country": "Russia",

View File

@ -1,6 +1,26 @@
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.planes import (
AV8BNA,
C_101CC,
C_130,
E_3A,
FA_18C_hornet,
F_5E_3,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Spain_1990 = {
"country": "Spain",

View File

@ -1,7 +1,21 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
from dcs.helicopters import (
UH_1H,
)
from dcs.planes import (
AJS37,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Sweden_1990 = {
"country": "Sweden",

View File

@ -1,6 +1,35 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.vehicles import *
from dcs.helicopters import (
Mi_24V,
Mi_8MT,
SA342L,
SA342M,
)
from dcs.planes import (
A_50,
An_26B,
An_30M,
IL_76MD,
IL_78M,
L_39ZA,
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_29S,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_17M4,
Su_24M,
Yak_40,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
Syria_2011 = {
"country": "Syria",

View File

@ -1,7 +1,26 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_1W,
UH_1H,
)
from dcs.planes import (
C_130,
E_3A,
F_16C_50,
F_4E,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Turkey_2005 = {
"country": "Turkey",

View File

@ -1,7 +1,27 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_64D,
)
from dcs.planes import (
C_130,
E_3A,
F_16C_50,
KC130,
KC_135,
M_2000C,
Mirage_2000_5,
WingLoong_I,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
UAE_2005 = {
"country": "United Arab Emirates",

View File

@ -1,6 +1,19 @@
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.planes import (
A_20G,
B_17G,
P_47D_30,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
)
from dcs.ships import LCVP__Higgins_boat, LST_Mk_II, LS_Samuel_Chase
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
from game.data.building_data import WW2_ALLIES_BUILDINGS
from game.data.doctrine import WWII_DOCTRINE

View File

@ -1,7 +1,29 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_64A,
SA342M,
)
from dcs.planes import (
AV8BNA,
C_130,
E_3A,
F_4E,
KC130,
KC_135,
Tornado_GR4,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
UnitedKingdom_1990 = {
"country": "UK",

View File

@ -1,7 +1,33 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
Mi_24V,
Mi_8MT,
)
from dcs.planes import (
A_50,
An_26B,
An_30M,
IL_76MD,
IL_78M,
L_39ZA,
MiG_29S,
Su_24M,
Su_25,
Su_25T,
Su_27,
Yak_40,
)
from dcs.ships import (
Bulk_cargo_ship_Yakushev,
CV_1143_5_Admiral_Kuznetsov,
Dry_cargo_ship_Ivanov,
Tanker_Elnya_160,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
Ukraine_2010 = {
"country": "Ukraine",

View File

@ -1,7 +1,36 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_64D,
Ka_50,
SA342L,
SA342M,
UH_1H,
)
from dcs.planes import (
C_130,
E_3A,
FA_18C_hornet,
F_15C,
F_16C_50,
F_5E_3,
KC130,
KC_135,
Su_27,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
US_Aggressors = {
"country": "USAF Aggressors",

View File

@ -1,6 +1,20 @@
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.planes import (
A_20G,
B_17G,
P_47D_30,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
)
from dcs.ships import LCVP__Higgins_boat, LST_Mk_II, LS_Samuel_Chase
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
from game.data.building_data import WW2_ALLIES_BUILDINGS
from game.data.doctrine import WWII_DOCTRINE

View File

@ -1,7 +1,22 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
from dcs.planes import (
C_130,
E_3A,
F_86F_Sabre,
KC130,
KC_135,
P_51D,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
USA_1955 = {
"country": "USA",

View File

@ -1,7 +1,25 @@
from dcs.vehicles import *
from dcs.ships import *
from dcs.planes import *
from dcs.helicopters import *
from dcs.helicopters import (
UH_1H,
)
from dcs.planes import (
C_130,
E_3A,
F_86F_Sabre,
KC130,
KC_135,
P_51D,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
USA_1960 = {
"country": "USA",

View File

@ -1,7 +1,26 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
UH_1H,
)
from dcs.planes import (
B_52H,
C_130,
E_3A,
F_4E,
F_5E_3,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
USA_1965 = {
"country": "USA",

View File

@ -1,7 +1,34 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_64A,
UH_1H,
)
from dcs.planes import (
AV8BNA,
A_10A,
C_130,
E_3A,
FA_18C_hornet,
F_14B,
F_15C,
F_15E,
F_16C_50,
KC130,
KC_135,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Infantry,
Unarmed,
)
USA_1990 = {
"country": "USA",

View File

@ -1,7 +1,36 @@
from dcs.helicopters import *
from dcs.planes import *
from dcs.ships import *
from dcs.vehicles import *
from dcs.helicopters import (
AH_64D,
UH_1H,
)
from dcs.planes import (
AV8BNA,
A_10C,
A_10C_2,
C_130,
E_3A,
FA_18C_hornet,
F_14B,
F_15C,
F_15E,
F_16C_50,
KC130,
KC_135,
MQ_9_Reaper,
)
from dcs.ships import (
Armed_speedboat,
CVN_74_John_C__Stennis,
LHA_1_Tarawa,
Ticonderoga_class,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import (
AirDefence,
Armor,
Artillery,
Infantry,
Unarmed,
)
USA_2005 = {
"country": "USA",

View File

@ -1,10 +1,32 @@
import logging
import math
import random
import sys
from datetime import datetime, timedelta
from typing import Any, Dict, List
from game.db import REWARDS, PLAYER_BUDGET_BASE, sys
from dcs.action import Coalition
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike, Task
from dcs.unittype import UnitType
from dcs.vehicles import AirDefence
from game import db
from game.db import PLAYER_BUDGET_BASE, REWARDS
from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
from gen.flights.ai_flight_planner import FlightPlanner
from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.ai_ground_planner import GroundPlanner
from .event import *
from theater import ConflictTheater, ControlPoint
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from . import persistency
from .debriefing import Debriefing
from .event.event import Event, UnitsDeliveryEvent
from .event.frontlineattack import FrontlineAttackEvent
from .infos.information import Information
from .settings import Settings
COMMISION_UNIT_VARIETY = 4
@ -45,20 +67,11 @@ PLAYER_BUDGET_IMPORTANCE_LOG = 2
class Game:
settings = None # type: Settings
budget = PLAYER_BUDGET_INITIAL
events = None # type: typing.List[Event]
pending_transfers = None # type: typing.Dict[]
ignored_cps = None # type: typing.Collection[ControlPoint]
turn = 0
game_stats: GameStats = None
current_unit_id = 0
current_group_id = 0
def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater, start_date: datetime, settings):
def __init__(self, player_name: str, enemy_name: str,
theater: ConflictTheater, start_date: datetime,
settings: Settings):
self.settings = settings
self.events = []
self.events: List[Event] = []
self.theater = theater
self.player_name = player_name
self.player_country = db.FACTIONS[player_name]["country"]
@ -68,17 +81,25 @@ class Game:
self.date = datetime(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.game_stats.update(self)
self.planners = {}
self.ground_planners = {}
self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
self.__culling_points = self.compute_conflicts_position()
self.__frontlineData = []
self.__destroyed_units = []
self.jtacs = []
self.__destroyed_units: List[str] = []
self.savepath = ""
self.budget = PLAYER_BUDGET_INITIAL
self.current_unit_id = 0
self.current_group_id = 0
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
self.aircraft_inventory = GlobalAircraftInventory(
self.theater.controlpoints
)
self.sanitize_sides()
self.on_load()
def sanitize_sides(self):
@ -95,11 +116,11 @@ class Game:
self.enemy_country = "Russia"
@property
def player_faction(self):
def player_faction(self) -> Dict[str, Any]:
return db.FACTIONS[self.player_name]
@property
def enemy_faction(self):
def enemy_faction(self) -> Dict[str, Any]:
return db.FACTIONS[self.enemy_name]
def _roll(self, prob, mult):
@ -116,7 +137,7 @@ class Game:
for player_cp, enemy_cp in self.theater.conflicts(True):
self._generate_player_event(FrontlineAttackEvent, player_cp, enemy_cp)
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[UnitType]:
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> List[UnitType]:
importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
if for_task == AirDefence and not self.settings.sams:
@ -190,12 +211,14 @@ class Game:
def is_player_attack(self, event):
if isinstance(event, Event):
return event.attacker_name == self.player_name
return event and event.attacker_name and event.attacker_name == self.player_name
else:
return event.name == self.player_name
return event and event.name and event.name == self.player_name
def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint] = None):
def on_load(self) -> None:
ObjectiveDistanceCache.set_theater(self.theater)
def pass_turn(self, no_action=False):
logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
self.turn = self.turn + 1
@ -219,26 +242,24 @@ class Game:
if not cp.is_carrier and not cp.is_lha:
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
self.ignored_cps = []
if ignored_cps:
self.ignored_cps = ignored_cps
self.events = [] # type: typing.List[Event]
self.events = []
self._generate_events()
# Update statistics
self.game_stats.update(self)
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp)
# Plan flights & combat for next turn
self.__culling_points = self.compute_conflicts_position()
self.planners = {}
self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
CoalitionMissionPlanner(self, is_player=True).plan_missions()
CoalitionMissionPlanner(self, is_player=False).plan_missions()
for cp in self.theater.controlpoints:
if cp.has_runway():
planner = FlightPlanner(cp, self)
planner.plan_flights()
self.planners[cp.id] = planner
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
@ -408,10 +429,10 @@ class Game:
return 1
def get_player_coalition(self):
return dcs.action.Coalition.Blue
return Coalition.Blue
def get_enemy_coalition(self):
return dcs.action.Coalition.Red
return Coalition.Red
def get_player_color(self):
return "blue"

130
game/inventory.py Normal file
View File

@ -0,0 +1,130 @@
"""Inventory management APIs."""
from collections import defaultdict
from typing import Dict, Iterable, Iterator, Set, Tuple
from dcs.unittype import UnitType
from gen.flights.flight import Flight
from theater import ControlPoint
class ControlPointAircraftInventory:
"""Aircraft inventory for a single control point."""
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
self.inventory: Dict[UnitType, int] = defaultdict(int)
def add_aircraft(self, aircraft: UnitType, count: int) -> None:
"""Adds aircraft to the inventory.
Args:
aircraft: The type of aircraft to add.
count: The number of aircraft to add.
"""
self.inventory[aircraft] += count
def remove_aircraft(self, aircraft: UnitType, count: int) -> None:
"""Removes aircraft from the inventory.
Args:
aircraft: The type of aircraft to remove.
count: The number of aircraft to remove.
Raises:
ValueError: The control point cannot fulfill the requested number of
aircraft.
"""
available = self.inventory[aircraft]
if available < count:
raise ValueError(
f"Cannot remove {count} {aircraft.id} from "
f"{self.control_point.name}. Only have {available}."
)
self.inventory[aircraft] -= count
def available(self, aircraft: UnitType) -> int:
"""Returns the number of available aircraft of the given type.
Args:
aircraft: The type of aircraft to query.
"""
return self.inventory[aircraft]
@property
def types_available(self) -> Iterator[UnitType]:
"""Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items():
if count > 0:
yield aircraft
@property
def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]:
"""Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items():
if count > 0:
yield aircraft, count
@property
def total_available(self) -> int:
"""Returns the total number of aircraft available."""
# TODO: Remove?
# This probably isn't actually useful. It's used by the AI flight
# planner to determine how many flights of a given type it should
# allocate, but it should probably be making that decision based on the
# number of aircraft available to perform a particular role.
return sum(self.inventory.values())
def clear(self) -> None:
"""Clears all aircraft from the inventory."""
self.inventory.clear()
class GlobalAircraftInventory:
"""Game-wide aircraft inventory."""
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
cp: ControlPointAircraftInventory(cp) for cp in control_points
}
def reset(self) -> None:
"""Clears all control points and their inventories."""
for inventory in self.inventories.values():
inventory.clear()
def set_from_control_point(self, control_point: ControlPoint) -> None:
"""Set the control point's aircraft inventory.
If the inventory for the given control point has already been set for
the turn, it will be overwritten.
"""
inventory = self.inventories[control_point]
for aircraft, count in control_point.base.aircraft.items():
inventory.add_aircraft(aircraft, count)
def for_control_point(
self,
control_point: ControlPoint) -> ControlPointAircraftInventory:
"""Returns the inventory specific to the given control point."""
return self.inventories[control_point]
@property
def available_types_for_player(self) -> Iterator[UnitType]:
"""Iterates over all aircraft types available to the player."""
seen: Set[UnitType] = set()
for control_point, inventory in self.inventories.items():
if control_point.captured:
for aircraft in inventory.types_available:
if aircraft not in seen:
seen.add(aircraft)
yield aircraft
def claim_for_flight(self, flight: Flight) -> None:
"""Removes aircraft from the inventory for the given flight."""
inventory = self.for_control_point(flight.from_cp)
inventory.remove_aircraft(flight.unit_type, flight.count)
def return_from_flight(self, flight: Flight) -> None:
"""Returns a flight's aircraft to the inventory."""
inventory = self.for_control_point(flight.from_cp)
inventory.add_aircraft(flight.unit_type, flight.count)

View File

@ -1,3 +1,5 @@
from typing import List
class FactionTurnMetadata:
"""
Store metadata about a faction
@ -31,10 +33,8 @@ class GameStats:
Store statistics for the current game
"""
data_per_turn: [GameTurnMetadata] = []
def __init__(self):
self.data_per_turn = []
self.data_per_turn: List[GameTurnMetadata] = []
def update(self, game):
"""

View File

@ -1,7 +1,8 @@
from game.db import assigned_units_split
from .operation import *
from dcs.terrain.terrain import Terrain
from gen.conflictgen import Conflict
from .operation import Operation
from .. import db
MAX_DISTANCE_BETWEEN_GROUPS = 12000

View File

@ -1,30 +1,51 @@
from typing import Set
import logging
import os
from pathlib import Path
from typing import List, Optional, Set
from gen import *
from gen.airfields import AIRFIELD_DATA
from gen.beacons import load_beacons_for_terrain
from gen.radios import RadioRegistry
from gen.tacan import TacanRegistry
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
from dcs.coalition import Coalition
from dcs.countries import country_dict
from dcs.lua.parse import loads
from dcs.mapping import Point
from dcs.terrain.terrain import Terrain
from userdata.debriefing import *
from dcs.translation import String
from dcs.triggers import TriggerStart
from dcs.unittype import UnitType
from gen import Conflict, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo
from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator
from gen.environmentgen import EnviromentGenerator
from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator
from gen.kneeboard import KneeboardGenerator
from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from theater import ControlPoint
from .. import db
from ..debriefing import Debriefing
class Operation:
attackers_starting_position = None # type: db.StartingPosition
defenders_starting_position = None # type: db.StartingPosition
current_mission = None # type: dcs.Mission
regular_mission = None # type: dcs.Mission
quick_mission = None # type: dcs.Mission
current_mission = None # type: Mission
regular_mission = None # type: Mission
quick_mission = None # type: Mission
conflict = None # type: Conflict
armorgen = None # type: ArmorConflictGenerator
airgen = None # type: AircraftConflictGenerator
triggersgen = None # type: TriggersGenerator
airsupportgen = None # type: AirSupportConflictGenerator
visualgen = None # type: VisualGenerator
envgen = None # type: EnvironmentGenerator
envgen = None # type: EnviromentGenerator
groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
@ -43,7 +64,7 @@ class Operation:
defender_name: str,
from_cp: ControlPoint,
departure_cp: ControlPoint,
to_cp: ControlPoint = None):
to_cp: ControlPoint):
self.game = game
self.attacker_name = attacker_name
self.attacker_country = db.FACTIONS[attacker_name]["country"]
@ -55,7 +76,7 @@ class Operation:
self.to_cp = to_cp
self.is_quick = False
def units_of(self, country_name: str) -> typing.Collection[UnitType]:
def units_of(self, country_name: str) -> List[UnitType]:
return []
def is_successfull(self, debriefing: Debriefing) -> bool:
@ -68,32 +89,14 @@ class Operation:
def initialize(self, mission: Mission, conflict: Conflict):
self.current_mission = mission
self.conflict = conflict
self.radio_registry = RadioRegistry()
self.tacan_registry = TacanRegistry()
self.airgen = AircraftConflictGenerator(
mission, conflict, self.game.settings, self.game,
self.radio_registry)
self.airsupportgen = AirSupportConflictGenerator(
mission, conflict, self.game, self.radio_registry,
self.tacan_registry)
self.triggersgen = TriggersGenerator(mission, conflict, self.game)
self.visualgen = VisualGenerator(mission, conflict, self.game)
self.envgen = EnviromentGenerator(mission, conflict, self.game)
self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game)
self.groundobjectgen = GroundObjectsGenerator(
mission,
conflict,
self.game,
self.radio_registry,
self.tacan_registry
)
self.briefinggen = BriefingGenerator(mission, conflict, self.game)
self.briefinggen = BriefingGenerator(self.current_mission,
self.conflict, self.game)
def prepare(self, terrain: Terrain, is_quick: bool):
with open("resources/default_options.lua", "r") as f:
options_dict = loads(f.read())["options"]
self.current_mission = dcs.Mission(terrain)
self.current_mission = Mission(terrain)
print(self.game.player_country)
print(country_dict[db.country_id_from_name(self.game.player_country)])
@ -124,9 +127,16 @@ class Operation:
self.defenders_starting_position = None
else:
self.attackers_starting_position = self.departure_cp.at
self.defenders_starting_position = self.to_cp.at
# TODO: Is this possible?
if self.to_cp is not None:
self.defenders_starting_position = self.to_cp.at
else:
self.defenders_starting_position = None
def generate(self):
radio_registry = RadioRegistry()
tacan_registry = TacanRegistry()
# Dedup beacon/radio frequencies, since some maps have some frequencies
# used multiple times.
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
@ -138,7 +148,7 @@ class Operation:
logging.error(
f"TACAN beacon has no channel: {beacon.callsign}")
else:
self.tacan_registry.reserve(beacon.tacan_channel)
tacan_registry.reserve(beacon.tacan_channel)
for airfield, data in AIRFIELD_DATA.items():
if data.theater == self.game.theater.terrain.name:
@ -150,16 +160,26 @@ class Operation:
# beacon list.
for frequency in unique_map_frequencies:
self.radio_registry.reserve(frequency)
radio_registry.reserve(frequency)
# Generate meteo
envgen = EnviromentGenerator(self.current_mission, self.conflict,
self.game)
if self.environment_settings is None:
self.environment_settings = self.envgen.generate()
self.environment_settings = envgen.generate()
else:
self.envgen.load(self.environment_settings)
envgen.load(self.environment_settings)
# Generate ground object first
self.groundobjectgen.generate()
groundobjectgen = GroundObjectsGenerator(
self.current_mission,
self.conflict,
self.game,
radio_registry,
tacan_registry
)
groundobjectgen.generate()
# Generate destroyed units
for d in self.game.get_destroyed_units():
@ -180,24 +200,27 @@ class Operation:
dead=True,
)
# Air Support (Tanker & Awacs)
self.airsupportgen.generate(self.is_awacs_enabled)
airsupportgen = AirSupportConflictGenerator(
self.current_mission, self.conflict, self.game, radio_registry,
tacan_registry)
airsupportgen.generate(self.is_awacs_enabled)
# Generate Activity on the map
for cp in self.game.theater.controlpoints:
side = cp.captured
if side:
country = self.current_mission.country(self.game.player_country)
else:
country = self.current_mission.country(self.game.enemy_country)
if cp.id in self.game.planners.keys():
self.airgen.generate_flights(
cp,
country,
self.game.planners[cp.id],
self.groundobjectgen.runways
)
airgen = AircraftConflictGenerator(
self.current_mission, self.conflict, self.game.settings, self.game,
radio_registry)
airgen.generate_flights(
self.current_mission.country(self.game.player_country),
self.game.blue_ato,
groundobjectgen.runways
)
airgen.generate_flights(
self.current_mission.country(self.game.enemy_country),
self.game.red_ato,
groundobjectgen.runways
)
# Generate ground units on frontline everywhere
jtacs: List[JtacInfo] = []
@ -221,18 +244,20 @@ class Operation:
self.current_mission.groundControl.red_tactical_commander = self.ca_slots
# Triggers
if self.game.is_player_attack(self.conflict.attackers_country):
cp = self.conflict.from_cp
else:
cp = self.conflict.to_cp
self.triggersgen.generate()
triggersgen = TriggersGenerator(self.current_mission, self.conflict,
self.game)
triggersgen.generate()
# Options
self.forcedoptionsgen.generate()
forcedoptionsgen = ForcedOptionsGenerator(self.current_mission,
self.conflict, self.game)
forcedoptionsgen.generate()
# Generate Visuals Smoke Effects
visualgen = VisualGenerator(self.current_mission, self.conflict,
self.game)
if self.game.settings.perf_smoke_gen:
self.visualgen.generate()
visualgen.generate()
# Inject Plugins Lua Scripts
listOfPluginsScripts = []
@ -327,19 +352,20 @@ class Operation:
trigger.add_action(DoScript(String(lua)))
self.current_mission.triggerrules.triggers.append(trigger)
self.assign_channels_to_flights()
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
kneeboard_generator = KneeboardGenerator(self.current_mission)
for dynamic_runway in self.groundobjectgen.runways.values():
for dynamic_runway in groundobjectgen.runways.values():
self.briefinggen.add_dynamic_runway(dynamic_runway)
for tanker in self.airsupportgen.air_support.tankers:
for tanker in airsupportgen.air_support.tankers:
self.briefinggen.add_tanker(tanker)
kneeboard_generator.add_tanker(tanker)
if self.is_awacs_enabled:
for awacs in self.airsupportgen.air_support.awacs:
for awacs in airsupportgen.air_support.awacs:
self.briefinggen.add_awacs(awacs)
kneeboard_generator.add_awacs(awacs)
@ -347,21 +373,23 @@ class Operation:
self.briefinggen.add_jtac(jtac)
kneeboard_generator.add_jtac(jtac)
for flight in self.airgen.flights:
for flight in airgen.flights:
self.briefinggen.add_flight(flight)
kneeboard_generator.add_flight(flight)
self.briefinggen.generate()
kneeboard_generator.generate()
def assign_channels_to_flights(self) -> None:
def assign_channels_to_flights(self, flights: List[FlightData],
air_support: AirSupport) -> None:
"""Assigns preset radio channels for client flights."""
for flight in self.airgen.flights:
for flight in flights:
if not flight.client_units:
continue
self.assign_channels_to_flight(flight)
self.assign_channels_to_flight(flight, air_support)
def assign_channels_to_flight(self, flight: FlightData) -> None:
def assign_channels_to_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
@ -371,5 +399,7 @@ class Operation:
logging.warning(f"No aircraft data for {airframe.id}")
return
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, self.airsupportgen.air_support)
if aircraft_data.channel_allocator is not None:
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, air_support
)

View File

@ -2,8 +2,9 @@ import logging
import os
import pickle
import shutil
from typing import Optional
_dcs_saved_game_folder = None # type: str
_dcs_saved_game_folder: Optional[str] = None
_file_abs_path = None
def setup(user_folder: str):
@ -40,30 +41,33 @@ def restore_game():
try:
save = pickle.load(f)
return save
except:
logging.error("Invalid Save game")
except Exception:
logging.exception("Invalid Save game")
return None
def load_game(path):
with open(path, "rb") as f:
try:
save = pickle.load(f)
save.savepath = path
return save
except:
logging.error("Invalid Save game")
except Exception:
logging.exception("Invalid Save game")
return None
def save_game(game) -> bool:
try:
with open(_temporary_save_file(), "wb") as f:
pickle.dump(game, f)
shutil.copy(_temporary_save_file(), game.savepath)
return True
except Exception as e:
logging.error(e)
except Exception:
logging.exception("Could not save game")
return False
def autosave(game) -> bool:
"""
Autosave to the autosave location
@ -74,7 +78,7 @@ def autosave(game) -> bool:
with open(_autosave_path(), "wb") as f:
pickle.dump(game, f)
return True
except Exception as e:
logging.error(e)
except Exception:
logging.exception("Could not save game")
return False

View File

@ -1,4 +1,3 @@
from .aaa import *
from .aircraft import *
from .armor import *
from .airsupportgen import *
@ -12,4 +11,3 @@ from .forcedoptionsgen import *
from .kneeboard import *
from . import naming

View File

@ -1,51 +0,0 @@
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.mission import *
from .conflictgen import *
from .naming import *
DISTANCE_FACTOR = 0.5, 1
EXTRA_AA_MIN_DISTANCE = 50000
EXTRA_AA_MAX_DISTANCE = 150000
EXTRA_AA_POSITION_FROM_CP = 550
class ExtraAAConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game, player_country: Country, enemy_country: Country):
self.mission = mission
self.game = game
self.conflict = conflict
self.player_country = player_country
self.enemy_country = enemy_country
def generate(self):
for cp in self.game.theater.controlpoints:
if cp.is_global:
continue
if cp.position.distance_to_point(self.conflict.position) < EXTRA_AA_MIN_DISTANCE:
continue
if cp.position.distance_to_point(self.conflict.from_cp.position) < EXTRA_AA_MIN_DISTANCE:
continue
if cp.position.distance_to_point(self.conflict.to_cp.position) < EXTRA_AA_MIN_DISTANCE:
continue
if cp.position.distance_to_point(self.conflict.position) > EXTRA_AA_MAX_DISTANCE:
continue
country_name = cp.captured and self.player_country or self.enemy_country
position = cp.position.point_from_heading(0, EXTRA_AA_POSITION_FROM_CP)
self.mission.vehicle_group(
country=self.mission.country(country_name),
name=namegen.next_basedefense_name(),
_type=db.EXTRA_AA[country_name],
position=position,
group_size=1
)

View File

@ -1,30 +1,84 @@
import logging
import random
from dataclasses import dataclass
from typing import Type
from typing import Dict, List, Optional, Tuple, Type, Union
from dcs import helicopters
from dcs.action import ActivateGroup, AITaskPush, MessageToAll
from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone
from dcs.action import AITaskPush, ActivateGroup, MessageToAll
from dcs.condition import CoalitionHasAirdrome, PartOfCoalitionInZone, TimeAfter
from dcs.country import Country
from dcs.flyingunit import FlyingUnit
from dcs.helicopters import helicopter_map, UH_1H
from dcs.helicopters import UH_1H, helicopter_map
from dcs.mapping import Point
from dcs.mission import Mission, StartType
from dcs.planes import (
AJS37,
B_17G,
Bf_109K_4,
FW_190A8,
FW_190D9,
F_14B,
I_16,
JF_17,
Ju_88A4,
P_47D_30,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_33,
)
from dcs.point import PointAction
from dcs.task import (
AntishipStrike,
AttackGroup,
Bombing,
CAP,
CAS,
ControlledTask,
EPLRS,
EngageTargets,
Escort,
GroundAttack,
MainTask,
NoTask,
OptROE,
OptRTBOnBingoFuel,
OptRTBOnOutOfAmmo,
OptReactOnThreat,
OptRestrictAfterburner,
OptRestrictJettison,
OrbitAction,
PinpointStrike,
SEAD,
StartCommand,
Targets,
Task,
)
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import TriggerOnce, Event
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce
from dcs.unitgroup import FlyingGroup, Group, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType
from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
from game.utils import nm_to_meter
from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder
from gen.callsigns import create_group_callsign_from_unit
from gen.flights.ai_flight_planner import FlightPlanner
from gen.flights.flight import (
Flight,
FlightType,
FlightWaypoint,
FlightWaypointType,
)
from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry
from .conflictgen import *
from .naming import *
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from theater.controlpoint import ControlPoint, ControlPointType
from .naming import namegen
from .conflictgen import Conflict
WARM_START_HELI_AIRSPEED = 120
WARM_START_HELI_ALT = 500
@ -264,8 +318,12 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel)
if self.intra_flight_radio_index is not None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel)
if self.inter_flight_radio_index is None:
return
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
@ -335,8 +393,10 @@ class ViggenRadioChannelAllocator(RadioChannelAllocator):
# the guard channel.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
flight.assign_channel(radio_id, 4, flight.departure.atc)
flight.assign_channel(radio_id, 5, flight.arrival.atc)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 4, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 5, flight.arrival.atc)
# TODO: Assign divert to 6 when we support divert airfields.
@ -348,8 +408,10 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
air_support: AirSupport) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
flight.assign_channel(radio_id, 2, flight.departure.atc)
flight.assign_channel(radio_id, 3, flight.arrival.atc)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 2, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@ -471,8 +533,6 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
class AircraftConflictGenerator:
escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]]
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
game, radio_registry: RadioRegistry):
self.m = mission
@ -480,7 +540,7 @@ class AircraftConflictGenerator:
self.settings = settings
self.conflict = conflict
self.radio_registry = radio_registry
self.escort_targets = []
self.escort_targets: List[Tuple[FlyingGroup, int]] = []
self.flights: List[FlightData] = []
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
@ -502,33 +562,23 @@ class AircraftConflictGenerator:
def _start_type(self) -> StartType:
return self.settings.cold_start and StartType.Cold or StartType.Warm
def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task],
def _setup_group(self, group: FlyingGroup, for_task: Type[Task],
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
did_load_loadout = False
unit_type = group.units[0].unit_type
if unit_type in db.PLANE_PAYLOAD_OVERRIDES:
override_loadout = db.PLANE_PAYLOAD_OVERRIDES[unit_type]
if type(override_loadout) == dict:
# Clear pylons
for p in group.units:
p.pylons.clear()
# Clear pylons
for p in group.units:
p.pylons.clear()
# Now load loadout
if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task]
group.load_loadout(payload_name)
did_load_loadout = True
logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
elif "*" in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type]["*"]
group.load_loadout(payload_name)
did_load_loadout = True
logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
elif issubclass(override_loadout, MainTask):
group.load_task_default_loadout(override_loadout)
# Now load loadout
if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task]
group.load_loadout(payload_name)
did_load_loadout = True
logging.info("Loaded overridden payload for {} - {} for task {}".format(unit_type, payload_name, for_task))
if not did_load_loadout:
group.load_task_default_loadout(for_task)
@ -590,7 +640,7 @@ class AircraftConflictGenerator:
# Special case so Su 33 carrier take off
if unit_type is Su_33:
if task is not CAP:
if flight.flight_type is not CAP:
for unit in group.units:
unit.fuel = Su_33.fuel_max / 2.2
else:
@ -613,9 +663,12 @@ class AircraftConflictGenerator:
# so just use the first runway.
return runways[0]
def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None, start_type = None) -> FlyingGroup:
def _generate_at_airport(self, name: str, side: Country,
unit_type: FlyingType, count: int,
client_count: int,
airport: Optional[Airport] = None,
start_type=None) -> FlyingGroup:
assert count > 0
assert unit is not None
if start_type is None:
start_type = self._start_type()
@ -633,7 +686,6 @@ class AircraftConflictGenerator:
def _generate_inflight(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: Point) -> FlyingGroup:
assert count > 0
assert unit is not None
if unit_type in helicopters.helicopter_map.values():
alt = WARM_START_HELI_ALT
@ -660,9 +712,11 @@ class AircraftConflictGenerator:
group.points[0].alt_type = "RADIO"
return group
def _generate_at_group(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, at: typing.Union[ShipGroup, StaticGroup], start_type=None) -> FlyingGroup:
def _generate_at_group(self, name: str, side: Country,
unit_type: FlyingType, count: int, client_count: int,
at: Union[ShipGroup, StaticGroup],
start_type=None) -> FlyingGroup:
assert count > 0
assert unit is not None
if start_type is None:
start_type = self._start_type()
@ -688,7 +742,7 @@ class AircraftConflictGenerator:
return self._generate_at_group(name, side, unit_type, count, client_count, at)
else:
return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
elif issubclass(at, Airport):
elif isinstance(at, Airport):
takeoff_ban = unit_type in db.TAKEOFF_BAN
ai_ban = client_count == 0 and self.settings.only_player_takeoff
@ -707,8 +761,9 @@ class AircraftConflictGenerator:
point.alt_type = "RADIO"
return point
def _rtb_for(self, group: FlyingGroup, cp: ControlPoint, at: db.StartingPosition = None):
if not at:
def _rtb_for(self, group: FlyingGroup, cp: ControlPoint,
at: Optional[db.StartingPosition] = None):
if at is None:
at = cp.at
position = at if isinstance(at, Point) else at.position
@ -751,31 +806,28 @@ class AircraftConflictGenerator:
else:
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
def generate_flights(self, cp, country, flight_planner: FlightPlanner,
dynamic_runways: Dict[str, RunwayData]):
# Clear pydcs parking slots
if cp.airport is not None:
logging.info("CLEARING SLOTS @ " + cp.airport.name)
logging.info("===============")
def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints:
if cp.airport is not None:
for ps in cp.airport.parking_slots:
logging.info("SLOT : " + str(ps.unit_id))
ps.unit_id = None
logging.info("----------------")
logging.info("===============")
for parking_slot in cp.airport.parking_slots:
parking_slot.unit_id = None
for flight in flight_planner.flights:
if flight.client_count == 0 and self.game.position_culled(flight.from_cp.position):
logging.info("Flight not generated : culled")
continue
logging.info("Generating flight : " + str(flight.unit_type))
group = self.generate_planned_flight(cp, country, flight)
self.setup_flight_group(group, flight, flight.flight_type,
dynamic_runways)
self.setup_group_activation_trigger(flight, group)
def generate_flights(self, country, ato: AirTaskingOrder,
dynamic_runways: Dict[str, RunwayData]) -> None:
self.clear_parking_slots()
for package in ato.packages:
for flight in package.flights:
culled = self.game.position_culled(flight.from_cp.position)
if flight.client_count == 0 and culled:
logging.info("Flight not generated: culled")
continue
logging.info(f"Generating flight: {flight.unit_type}")
group = self.generate_planned_flight(flight.from_cp, country,
flight)
self.setup_flight_group(group, flight, flight.flight_type,
dynamic_runways)
self.setup_group_activation_trigger(flight, group)
def setup_group_activation_trigger(self, flight, group):
if flight.scheduled_in > 0 and flight.client_count == 0:
@ -932,6 +984,14 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
group.points[0].tasks.append(OptRestrictJettison(True))
elif flight_type == FlightType.ESCORT:
group.task = Escort.name
self._setup_group(group, Escort, flight, dynamic_runways)
# TODO: Cleanup duplication...
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
group.points[0].tasks.append(OptRestrictJettison(True))
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
group.points[0].tasks.append(OptRestrictAfterburner(True))
@ -952,6 +1012,7 @@ class AircraftConflictGenerator:
# pt.tasks.append(engagetgt)
elif point.waypoint_type == FlightWaypointType.LANDING_POINT:
pt.type = "Land"
pt.action = PointAction.Landing
elif point.waypoint_type == FlightWaypointType.INGRESS_STRIKE:
if group.units[0].unit_type == B_17G:

View File

@ -195,10 +195,12 @@ AIRFIELD_DATA = {
runway_length=8623,
atc=AtcData(MHz(3, 750), MHz(121, 0), MHz(38, 400), MHz(250, 0)),
outer_ndb={
"22": ("AP", MHz(443, 0)), "4": "443.00 (AN)"
"22": ("AP", MHz(443, 0)),
"04": ("AN", MHz(443)),
},
inner_ndb={
"22": ("P", MHz(215, 0)), "4": "215.00 (N)"
"22": ("P", MHz(215, 0)),
"04": ("N", MHz(215)),
},
),

View File

@ -1,8 +1,21 @@
from dataclasses import dataclass, field
from typing import List, Type
from dcs.mission import Mission, StartType
from dcs.planes import IL_78M
from dcs.task import (
AWACS,
ActivateBeaconCommand,
MainTask,
Refueling,
SetImmortalCommand,
SetInvisibleCommand,
)
from game import db
from .naming import namegen
from .callsigns import callsign_for_support_unit
from .conflictgen import *
from .naming import *
from .conflictgen import Conflict
from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry
@ -49,7 +62,7 @@ class AirSupportConflictGenerator:
self.tacan_registry = tacan_registry
@classmethod
def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]:
def support_tasks(cls) -> List[Type[MainTask]]:
return [Refueling, AWACS]
def generate(self, is_awacs_enabled):
@ -76,6 +89,7 @@ class AirSupportConflictGenerator:
speed=574,
tacanchannel=str(tacan),
)
tanker_group.set_frequency(freq.mhz)
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
@ -118,6 +132,8 @@ class AirSupportConflictGenerator:
frequency=freq.mhz,
start_type=StartType.Warm,
)
awacs_flight.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))

View File

@ -1,13 +1,39 @@
import logging
import random
from dataclasses import dataclass
from typing import List
from dcs import Mission
from dcs.action import AITaskPush
from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess
from dcs.triggers import TriggerOnce, Event
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
from dcs.country import Country
from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction
from dcs.task import (
AttackGroup,
ControlledTask,
EPLRS,
FireAtPoint,
GoToWaypoint,
Hold,
OrbitAction,
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle
from dcs.unittype import VehicleType
from gen import namegen
from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE
from game import db
from .naming import namegen
from gen.ground_forces.ai_ground_planner import (
CombatGroupRole,
DISTANCE_FROM_FRONTLINE,
)
from .callsigns import callsign_for_support_unit
from .conflictgen import *
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
@ -48,7 +74,7 @@ class GroundConflictGenerator:
self.jtacs: List[JtacInfo] = []
def _group_point(self, point) -> Point:
distance = randint(
distance = random.randint(
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]),
)
@ -165,7 +191,7 @@ class GroundConflictGenerator:
heading=forward_heading,
move_formation=PointAction.OffRoad)
for i in range(randint(3, 10)):
for i in range(random.randint(3, 10)):
u = random.choice(possible_infantry_units)
position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group(
@ -183,6 +209,11 @@ class GroundConflictGenerator:
return
for dcs_group, group in ally_groups:
if hasattr(group.units[0], 'eplrs'):
if group.units[0].eplrs:
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.role == CombatGroupRole.ARTILLERY:
# Fire on any ennemy in range
if self.game.settings.perf_artillery:

136
gen/ato.py Normal file
View File

@ -0,0 +1,136 @@
"""Air Tasking Orders.
The classes of the Air Tasking Order (ATO) define all of the missions that have
been planned, and which aircraft have been assigned to them. Each planned
mission, or "package" is composed of individual flights. The package may contain
dissimilar aircraft performing different roles, but all for the same goal. For
example, the package to strike an enemy airfield may contain an escort flight,
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
the single CAP flight.
"""
from collections import defaultdict
from dataclasses import dataclass, field
import logging
from typing import Dict, Iterator, List, Optional
from dcs.mapping import Point
from .flights.flight import Flight, FlightType
from theater.missiontarget import MissionTarget
@dataclass(frozen=True)
class Task:
"""The main task of a flight or package."""
#: The type of task.
task_type: FlightType
#: The location of the objective.
location: str
@dataclass
class Package:
"""A mission package."""
#: The mission target. Currently can be either a ControlPoint or a
#: TheaterGroundObject (non-ControlPoint map objectives).
target: MissionTarget
#: The set of flights in the package.
flights: List[Flight] = field(default_factory=list)
delay: int = field(default=0)
join_point: Optional[Point] = field(default=None, init=False, hash=False)
split_point: Optional[Point] = field(default=None, init=False, hash=False)
ingress_point: Optional[Point] = field(default=None, init=False, hash=False)
egress_point: Optional[Point] = field(default=None, init=False, hash=False)
def add_flight(self, flight: Flight) -> None:
"""Adds a flight to the package."""
self.flights.append(flight)
def remove_flight(self, flight: Flight) -> None:
"""Removes a flight from the package."""
self.flights.remove(flight)
if not self.flights:
self.ingress_point = None
self.egress_point = None
@property
def primary_task(self) -> Optional[FlightType]:
if not self.flights:
return None
flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0)
for flight in self.flights:
flight_counts[flight.flight_type] += 1
# The package will contain a mix of mission types, but in general we can
# determine the goal of the mission because some mission types are more
# likely to be the main task than others. For example, a package with
# only CAP flights is a CAP package, a flight with CAP and strike is a
# strike package, a flight with CAP and DEAD is a DEAD package, and a
# flight with strike and SEAD is an OCA/Strike package. The type of
# package is determined by the highest priority flight in the package.
task_priorities = [
FlightType.CAS,
FlightType.STRIKE,
FlightType.ANTISHIP,
FlightType.BAI,
FlightType.EVAC,
FlightType.TROOP_TRANSPORT,
FlightType.RECON,
FlightType.ELINT,
FlightType.DEAD,
FlightType.SEAD,
FlightType.LOGISTICS,
FlightType.INTERCEPTION,
FlightType.TARCAP,
FlightType.CAP,
FlightType.BARCAP,
FlightType.EWAR,
FlightType.ESCORT,
]
for task in task_priorities:
if flight_counts[task]:
return task
# If we get here, our task_priorities list above is incomplete. Log the
# issue and return the type of *any* flight in the package.
some_mission = next(iter(self.flights)).flight_type
logging.warning(f"Unhandled mission type: {some_mission}")
return some_mission
@property
def package_description(self) -> str:
"""Generates a package description based on flight composition."""
task = self.primary_task
if task is None:
return "No mission"
return task.name
def __hash__(self) -> int:
# TODO: Far from perfect. Number packages?
return hash(self.target.name)
@dataclass
class AirTaskingOrder:
"""The entire ATO for one coalition."""
#: The set of all planned packages in the ATO.
packages: List[Package] = field(default_factory=list)
def add_package(self, package: Package) -> None:
"""Adds a package to the ATO."""
self.packages.append(package)
def remove_package(self, package: Package) -> None:
"""Removes a package from the ATO."""
self.packages.remove(package)
def clear(self) -> None:
"""Removes all packages from the ATO."""
self.packages.clear()

View File

@ -106,7 +106,7 @@ class BriefingGenerator(MissionInfoGenerator):
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
self.description += "-" * 50 + "\n"
self.description += f"{flight_unit_name} x {flight.size + 2}\n\n"
self.description += f"{flight_unit_name} x {flight.size}\n\n"
for i, wpt in enumerate(flight.waypoints):
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"

View File

@ -1,21 +1,11 @@
import logging
import typing
import pdb
import dcs
import random
from typing import Tuple
from random import randint
from dcs import Mission
from dcs.country import Country
from dcs.mapping import Point
from dcs.mission import *
from dcs.vehicles import *
from dcs.unitgroup import *
from dcs.unittype import *
from dcs.mapping import *
from dcs.point import *
from dcs.task import *
from dcs.country import *
from theater import *
from theater import ConflictTheater, ControlPoint
AIR_DISTANCE = 40000
@ -65,24 +55,6 @@ def _heading_sum(h, a) -> int:
class Conflict:
attackers_side = None # type: str
defenders_side = None # type: str
attackers_country = None # type: Country
defenders_country = None # type: Country
from_cp = None # type: ControlPoint
to_cp = None # type: ControlPoint
position = None # type: Point
size = None # type: int
radials = None # type: typing.List[int]
heading = None # type: int
distance = None # type: int
ground_attackers_location = None # type: Point
ground_defenders_location = None # type: Point
air_attackers_location = None # type: Point
air_defenders_location = None # type: Point
def __init__(self,
theater: ConflictTheater,
from_cp: ControlPoint,
@ -155,7 +127,7 @@ class Conflict:
else:
return self.position
def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> typing.Optional[Point]:
def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point:
return Conflict._find_ground_position(at, max_distance, heading, self.theater)
@classmethod
@ -163,7 +135,7 @@ class Conflict:
return from_cp.has_frontline and to_cp.has_frontline
@classmethod
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> typing.Optional[typing.Tuple[Point, int]]:
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
attack_heading = from_cp.position.heading_between_point(to_cp.position)
attack_distance = from_cp.position.distance_to_point(to_cp.position)
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2)
@ -174,9 +146,7 @@ class Conflict:
@classmethod
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> typing.Optional[typing.Tuple[Point, int, int]]:
initial, heading = cls.frontline_position(theater, from_cp, to_cp)
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
"""
probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH)
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
@ -193,9 +163,6 @@ class Conflict:
return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length
"""
frontline = cls.frontline_position(theater, from_cp, to_cp)
if not frontline:
return None
center_position, heading = frontline
left_position, right_position = None, None
@ -243,7 +210,7 @@ class Conflict:
"""
@classmethod
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> typing.Optional[Point]:
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
pos = initial
for _ in range(0, int(max_distance), 500):
if theater.is_on_land(pos):
@ -302,10 +269,14 @@ class Conflict:
distance = to_cp.size * GROUND_DISTANCE_FACTOR
attackers_location = position.point_from_heading(attack_heading, distance)
attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, _heading_sum(attack_heading, 180), theater)
attackers_location = Conflict._find_ground_position(
attackers_location, int(distance * 2),
_heading_sum(attack_heading, 180), theater)
defenders_location = position.point_from_heading(defense_heading, distance)
defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater)
defenders_location = Conflict._find_ground_position(
defenders_location, int(distance * 2),
_heading_sum(defense_heading, 180), theater)
return cls(
position=position,
@ -429,7 +400,7 @@ class Conflict:
assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
attack_position = position.point_from_heading(heading, randint(0, int(distance)))
attack_position = position.point_from_heading(heading, random.randint(0, int(distance)))
attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE)
defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE))
@ -456,7 +427,9 @@ class Conflict:
distance = to_cp.size * GROUND_DISTANCE_FACTOR
defenders_location = position.point_from_heading(defense_heading, distance)
defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater)
defenders_location = Conflict._find_ground_position(
defenders_location, int(distance * 2),
_heading_sum(defense_heading, 180), theater)
return cls(
position=position,

View File

@ -1,20 +1,11 @@
import logging
import typing
import random
from datetime import datetime, timedelta, time
from datetime import timedelta
from dcs.mission import Mission
from dcs.triggers import *
from dcs.condition import *
from dcs.action import *
from dcs.unit import Skill
from dcs.point import MovingPoint, PointProperties
from dcs.action import *
from dcs.weather import *
from dcs.weather import Weather, Wind
from game import db
from theater import *
from gen import *
from .conflictgen import Conflict
WEATHER_CLOUD_BASE = 2000, 3000
WEATHER_CLOUD_DENSITY = 1, 8

View File

@ -28,13 +28,13 @@ SHIP_MAP = {
}
def generate_ship_group(game, ground_object, faction:str):
def generate_ship_group(game, ground_object, faction_name: str):
"""
This generate a ship group
:return: Nothing, but put the group reference inside the ground object
"""
faction = db.FACTIONS[faction]
if "boat" in faction.keys():
faction = db.FACTIONS[faction_name]
if "boat" in faction:
generators = faction["boat"]
if len(generators) > 0:
gen = random.choice(generators)

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,78 @@
from dcs.planes import *
from dcs.helicopters import *
from dcs.helicopters import (
AH_1W,
AH_64A,
AH_64D,
Ka_50,
Mi_24V,
Mi_28N,
Mi_8MT,
OH_58D,
SA342L,
SA342M,
UH_1H,
)
from dcs.planes import (
AJS37,
AV8BNA,
A_10A,
A_10C,
A_10C_2,
A_20G,
B_17G,
Bf_109K_4,
C_101CC,
FA_18C_hornet,
FW_190A8,
FW_190D9,
F_14B,
F_15C,
F_15E,
F_16A,
F_16C_50,
F_4E,
F_5E_3,
F_86F_Sabre,
F_A_18C,
JF_17,
J_11A,
Ju_88A4,
L_39ZA,
MQ_9_Reaper,
M_2000C,
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_27K,
MiG_29A,
MiG_29G,
MiG_29K,
MiG_29S,
MiG_31,
Mirage_2000_5,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
RQ_1A_Predator,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_27,
Su_30,
Su_33,
Su_34,
Tornado_GR4,
Tornado_IDS,
WingLoong_I,
)
# Interceptor are the aircraft prioritized for interception tasks
# If none is available, the AI will use regular CAP-capable aircraft instead
@ -7,6 +80,10 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M
# TODO: These lists really ought to be era (faction) dependent.
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
# factions that also have F-4s should not.
INTERCEPT_CAPABLE = [
MiG_21Bis,
MiG_25PD,
@ -77,6 +154,42 @@ CAP_CAPABLE = [
Rafale_M,
]
CAP_PREFERRED = [
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_29A,
MiG_29G,
MiG_29S,
MiG_31,
Su_27,
J_11A,
Su_30,
Su_33,
M_2000C,
Mirage_2000_5,
F_86F_Sabre,
F_14B,
F_15C,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
Rafale_M,
]
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [
@ -155,6 +268,59 @@ CAS_CAPABLE = [
RQ_1A_Predator
]
CAS_PREFERRED = [
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_34,
JF_17,
A_10A,
A_10C,
A_10C_2,
AV8BNA,
F_15E,
Tornado_GR4,
C_101CC,
MB_339PAN,
L_39ZA,
AJS37,
SA342M,
SA342L,
OH_58D,
AH_64A,
AH_64D,
AH_1W,
UH_1H,
Mi_8MT,
Mi_28N,
Mi_24V,
Ka_50,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
A_4E_C,
Rafale_A_S,
WingLoong_I,
MQ_9_Reaper,
RQ_1A_Predator
]
# Aircraft used for SEAD / DEAD tasks
SEAD_CAPABLE = [
F_4E,
@ -179,6 +345,12 @@ SEAD_CAPABLE = [
Rafale_A_S
]
SEAD_PREFERRED = [
F_4E,
Su_25T,
Tornado_IDS,
]
# Aircraft used for Strike mission
STRIKE_CAPABLE = [
MiG_15bis,
@ -236,6 +408,15 @@ STRIKE_CAPABLE = [
]
STRIKE_PREFERRED = [
AJS37,
F_15E,
Tornado_GR4,
A_20G,
B_17G,
]
ANTISHIP_CAPABLE = [
Su_24M,
Su_17M4,

View File

@ -0,0 +1,51 @@
"""Objective adjacency lists."""
from typing import Dict, Iterator, List, Optional
from theater import ConflictTheater, ControlPoint, MissionTarget
class ClosestAirfields:
"""Precalculates which control points are closes to the given target."""
def __init__(self, target: MissionTarget,
all_control_points: List[ControlPoint]) -> None:
self.target = target
self.closest_airfields: List[ControlPoint] = sorted(
all_control_points, key=lambda c: self.target.distance_to(c)
)
def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
"""Iterates over all airfields within the given range of the target.
Note that this iterates over *all* airfields, not just friendly
airfields.
"""
for cp in self.closest_airfields:
if cp.distance_to(self.target) < meters:
yield cp
else:
break
class ObjectiveDistanceCache:
theater: Optional[ConflictTheater] = None
closest_airfields: Dict[str, ClosestAirfields] = {}
@classmethod
def set_theater(cls, theater: ConflictTheater) -> None:
if cls.theater is not None:
cls.closest_airfields = {}
cls.theater = theater
@classmethod
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
if cls.theater is None:
raise RuntimeError(
"Call ObjectiveDistanceCache.set_theater before using"
)
if location.name not in cls.closest_airfields:
cls.closest_airfields[location.name] = ClosestAirfields(
location, cls.theater.controlpoints
)
return cls.closest_airfields[location.name]

View File

@ -1,10 +1,10 @@
from enum import Enum
from typing import List
from typing import Dict, Optional
from game import db
from dcs.unittype import UnitType
from dcs.point import MovingPoint, PointAction
from theater.controlpoint import ControlPoint
from theater.controlpoint import ControlPoint, MissionTarget
class FlightType(Enum):
@ -47,6 +47,8 @@ class FlightWaypointType(Enum):
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16
SPLIT = 17
class PredefinedWaypointCategory(Enum):
@ -71,8 +73,8 @@ class FlightWaypoint:
self.alt_type = "BARO"
self.name = ""
self.description = ""
self.targets = []
self.targetGroup = None
self.targets: List[MissionTarget] = []
self.targetGroup: Optional[MissionTarget] = None
self.obj_name = ""
self.pretty_name = ""
self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED
@ -108,15 +110,9 @@ class FlightWaypoint:
class Flight:
unit_type: UnitType = None
from_cp = None
points: List[FlightWaypoint] = []
flight_type: FlightType = None
count: int = 0
client_count: int = 0
targets = []
use_custom_loadout = False
loadout = {}
preset_loadout_name = ""
start_type = "Runway"
group = False # Contains DCS Mission group data after mission has been generated
@ -124,14 +120,14 @@ class Flight:
# How long before this flight should take off
scheduled_in = 0
def __init__(self, unit_type: UnitType, count: int, from_cp, flight_type: FlightType):
def __init__(self, unit_type: UnitType, count: int, from_cp: ControlPoint, flight_type: FlightType):
self.unit_type = unit_type
self.count = count
self.from_cp = from_cp
self.flight_type = flight_type
self.points = []
self.targets = []
self.loadout = {}
self.points: List[FlightWaypoint] = []
self.targets: List[MissionTarget] = []
self.loadout: Dict[str, str] = {}
self.start_type = "Runway"
def __repr__(self):
@ -141,10 +137,10 @@ class Flight:
# Test
if __name__ == '__main__':
from pydcs.dcs.planes import A_10C
from dcs.planes import A_10C
from theater import ControlPoint, Point, List
from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0)
from_cp = ControlPoint(0, "AA", Point(0, 0), Point(0, 0), [], 0, 0)
f = Flight(A_10C(), 4, from_cp, FlightType.CAS)
f.scheduled_in = 50
print(f)

442
gen/flights/flightplan.py Normal file
View File

@ -0,0 +1,442 @@
"""Flight plan generation.
Flights are first planned generically by either the player or by the
MissionPlanner. Those only plan basic information like the objective, aircraft
type, and the size of the flight. The FlightPlanBuilder is responsible for
generating the waypoints for the mission.
"""
from __future__ import annotations
import logging
import random
from typing import List, Optional, TYPE_CHECKING
from dcs.mapping import Point
from dcs.unit import Unit
from game.data.doctrine import Doctrine, MODERN_DOCTRINE
from game.utils import nm_to_meter
from gen.ato import Package
from theater import ControlPoint, FrontLine, MissionTarget, TheaterGroundObject
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .waypointbuilder import WaypointBuilder
from ..conflictgen import Conflict
if TYPE_CHECKING:
from game import Game
class InvalidObjectiveLocation(RuntimeError):
"""Raised when the objective location is invalid for the mission type."""
def __init__(self, task: FlightType, location: MissionTarget) -> None:
super().__init__(
f"{location.name} is not valid for {task.name} missions."
)
class FlightPlanBuilder:
"""Generates flight plans for flights."""
def __init__(self, game: Game, package: Package, is_player: bool) -> None:
self.game = game
self.package = package
self.is_player = is_player
if is_player:
faction = self.game.player_faction
else:
faction = self.game.enemy_faction
self.doctrine: Doctrine = faction.get("doctrine", MODERN_DOCTRINE)
def populate_flight_plan(
self, flight: Flight,
# TODO: Custom targets should be an attribute of the flight.
custom_targets: Optional[List[Unit]] = None) -> None:
"""Creates a default flight plan for the given mission."""
if flight not in self.package.flights:
raise RuntimeError("Flight must be a part of the package")
self.generate_missing_package_waypoints()
# TODO: Flesh out mission types.
try:
task = flight.flight_type
if task == FlightType.ANTISHIP:
logging.error(
"Anti-ship flight plan generation not implemented"
)
elif task == FlightType.BAI:
logging.error("BAI flight plan generation not implemented")
elif task == FlightType.BARCAP:
self.generate_barcap(flight)
elif task == FlightType.CAP:
self.generate_barcap(flight)
elif task == FlightType.CAS:
self.generate_cas(flight)
elif task == FlightType.DEAD:
self.generate_sead(flight, custom_targets)
elif task == FlightType.ELINT:
logging.error("ELINT flight plan generation not implemented")
elif task == FlightType.ESCORT:
self.generate_escort(flight)
elif task == FlightType.EVAC:
logging.error("Evac flight plan generation not implemented")
elif task == FlightType.EWAR:
logging.error("EWar flight plan generation not implemented")
elif task == FlightType.INTERCEPTION:
logging.error(
"Intercept flight plan generation not implemented"
)
elif task == FlightType.LOGISTICS:
logging.error(
"Logistics flight plan generation not implemented"
)
elif task == FlightType.RECON:
logging.error("Recon flight plan generation not implemented")
elif task == FlightType.SEAD:
self.generate_sead(flight, custom_targets)
elif task == FlightType.STRIKE:
self.generate_strike(flight)
elif task == FlightType.TARCAP:
self.generate_frontline_cap(flight)
elif task == FlightType.TROOP_TRANSPORT:
logging.error(
"Troop transport flight plan generation not implemented"
)
except InvalidObjectiveLocation as ex:
logging.error(f"Could not create flight plan: {ex}")
def generate_missing_package_waypoints(self) -> None:
if self.package.ingress_point is None:
self.package.ingress_point = self._ingress_point()
if self.package.egress_point is None:
self.package.egress_point = self._egress_point()
if self.package.join_point is None:
self.package.join_point = self._join_point()
if self.package.split_point is None:
self.package.split_point = self._split_point()
def generate_strike(self, flight: Flight) -> None:
"""Generates a strike flight plan.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
# TODO: Support airfield strikes.
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.join(self.package.join_point)
builder.ingress_strike(self.package.ingress_point, location)
if len(location.groups) > 0 and location.dcs_identifier == "AA":
# TODO: Replace with DEAD?
# Strike missions on SEAD targets target units.
for g in location.groups:
for j, u in enumerate(g.units):
builder.strike_point(u, f"{u.type} #{j}", location)
else:
# TODO: Does this actually happen?
# ConflictTheater is built with the belief that multiple ground
# objects have the same name. If that's the case,
# TheaterGroundObject needs some refactoring because it behaves very
# differently for SAM sites than it does for strike targets.
buildings = self.game.theater.find_ground_objects_by_obj_name(
location.obj_name
)
for building in buildings:
if building.is_dead:
continue
builder.strike_point(
building,
f"{building.obj_name} {building.category}",
location
)
builder.egress(self.package.egress_point, location)
builder.split(self.package.split_point)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_barcap(self, flight: Flight) -> None:
"""Generate a BARCAP flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
if isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
patrol_alt = random.randint(
self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude
)
closest_cache = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in closest_cache.closest_airfields:
if airfield.captured != self.is_player:
closest_airfield = airfield
break
else:
logging.error("Could not find any enemy airfields")
return
heading = location.position.heading_between_point(
closest_airfield.position
)
end = location.position.point_from_heading(
heading,
random.randint(self.doctrine.cap_min_distance_from_cp,
self.doctrine.cap_max_distance_from_cp)
)
diameter = random.randint(
self.doctrine.cap_min_track_length,
self.doctrine.cap_max_track_length
)
start = end.point_from_heading(heading - 180, diameter)
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.race_track(start, end, patrol_alt)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_frontline_cap(self, flight: Flight) -> None:
"""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
ingress, heading, distance = Conflict.frontline_vector(
ally_cp, enemy_cp, self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2)
orbit_center = center.point_from_heading(
heading - 90, random.randint(nm_to_meter(6), nm_to_meter(15))
)
combat_width = distance / 2
if combat_width > 500000:
combat_width = 500000
if combat_width < 35000:
combat_width = 35000
radius = combat_width*1.25
orbit0p = orbit_center.point_from_heading(heading, radius)
orbit1p = orbit_center.point_from_heading(heading + 180, radius)
# Create points
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.race_track(orbit0p, orbit1p, patrol_alt)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_sead(self, flight: Flight,
custom_targets: Optional[List[Unit]]) -> None:
"""Generate a SEAD/DEAD flight at a given location.
Args:
flight: The flight to generate the flight plan for.
custom_targets: Specific radar equipped units selected by the user.
"""
location = self.package.target
if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
if custom_targets is None:
custom_targets = []
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.join(self.package.join_point)
builder.ingress_sead(self.package.ingress_point, location)
# TODO: Unify these.
# There doesn't seem to be any reason to treat the UI fragged missions
# different from the automatic missions.
if custom_targets:
for target in custom_targets:
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
target.position.x,
target.position.y,
0
)
point.alt_type = "RADIO"
if flight.flight_type == FlightType.DEAD:
builder.dead_point(target, location.name, location)
else:
builder.sead_point(target, location.name, location)
else:
if flight.flight_type == FlightType.DEAD:
builder.dead_area(location)
else:
builder.sead_area(location)
builder.egress(self.package.egress_point, location)
builder.split(self.package.split_point)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_escort(self, flight: Flight) -> None:
# TODO: Decide common waypoints for the package ahead of time.
# Packages should determine some common points like push, ingress,
# egress, and split points ahead of time so they can be shared by all
# flights.
patrol_alt = random.randint(
self.doctrine.min_patrol_altitude,
self.doctrine.max_patrol_altitude
)
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp)
builder.join(self.package.join_point)
builder.race_track(
self.package.ingress_point,
self.package.egress_point,
patrol_alt
)
builder.split(self.package.split_point)
builder.rtb(flight.from_cp)
flight.points = builder.build()
def generate_cas(self, flight: Flight) -> None:
"""Generate a CAS flight plan for the given target.
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)
is_helo = getattr(flight.unit_type, "helicopter", False)
cap_alt = 500 if is_helo else 1000
ingress, heading, distance = Conflict.frontline_vector(
location.control_points[0], location.control_points[1],
self.game.theater
)
center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance)
builder = WaypointBuilder(self.doctrine)
builder.ascent(flight.from_cp, is_helo)
builder.join(self.package.join_point)
builder.ingress_cas(ingress, location)
builder.cas(center, cap_alt)
builder.egress(egress, location)
builder.split(self.package.split_point)
builder.rtb(flight.from_cp, is_helo)
flight.points = builder.build()
# TODO: Make a model for the waypoint builder and use that in the UI.
def generate_ascend_point(self, departure: ControlPoint) -> FlightWaypoint:
"""Generate ascend point.
Args:
departure: Departure airfield or carrier.
"""
builder = WaypointBuilder(self.doctrine)
builder.ascent(departure)
return builder.build()[0]
def generate_descend_point(self, arrival: ControlPoint) -> FlightWaypoint:
"""Generate approach/descend point.
Args:
arrival: Arrival airfield or carrier.
"""
builder = WaypointBuilder(self.doctrine)
builder.descent(arrival)
return builder.build()[0]
def generate_rtb_waypoint(self, arrival: ControlPoint) -> FlightWaypoint:
"""Generate RTB landing point.
Args:
arrival: Arrival airfield or carrier.
"""
builder = WaypointBuilder(self.doctrine)
builder.land(arrival)
return builder.build()[0]
def _join_point(self) -> Point:
ingress_point = self.package.ingress_point
assert ingress_point is not None
heading = self._heading_to_package_airfield(ingress_point)
return ingress_point.point_from_heading(heading,
-self.doctrine.join_distance)
def _split_point(self) -> Point:
egress_point = self.package.egress_point
assert egress_point is not None
heading = self._heading_to_package_airfield(egress_point)
return egress_point.point_from_heading(heading,
-self.doctrine.split_distance)
def _ingress_point(self) -> Point:
heading = self._target_heading_to_package_airfield()
return self.package.target.position.point_from_heading(
heading - 180 + 25, self.doctrine.ingress_egress_distance
)
def _egress_point(self) -> Point:
heading = self._target_heading_to_package_airfield()
return self.package.target.position.point_from_heading(
heading - 180 - 25, self.doctrine.ingress_egress_distance
)
def _target_heading_to_package_airfield(self) -> int:
return self._heading_to_package_airfield(self.package.target.position)
def _heading_to_package_airfield(self, point: Point) -> int:
return self.package_airfield().position.heading_between_point(point)
# TODO: Set ingress/egress/join/split points in the Package.
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.
if not self.package.flights:
raise RuntimeError(
"Cannot determine source airfield for package with no flights"
)
# The package airfield is either the flight's airfield (when there is no
# package) or the closest airfield to the objective that is the
# departure airfield for some flight in the package.
cache = ObjectiveDistanceCache.get_closest_airfields(
self.package.target
)
for airfield in cache.closest_airfields:
for flight in self.package.flights:
if flight.from_cp == airfield:
return airfield
raise RuntimeError(
"Could not find any airfield assigned to this package"
)

View File

@ -0,0 +1,298 @@
from __future__ import annotations
from typing import List, Optional, Union
from dcs.mapping import Point
from dcs.unit import Unit
from game.data.doctrine import Doctrine
from game.utils import nm_to_meter
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import FlightWaypoint, FlightWaypointType
class WaypointBuilder:
def __init__(self, doctrine: Doctrine) -> None:
self.doctrine = doctrine
self.waypoints: List[FlightWaypoint] = []
self.ingress_point: Optional[FlightWaypoint] = None
def build(self) -> List[FlightWaypoint]:
return self.waypoints
def ascent(self, departure: ControlPoint, is_helo: bool = False) -> None:
"""Create ascent waypoint for the given departure airfield or carrier.
Args:
departure: Departure airfield or carrier.
"""
# TODO: Pick runway based on wind direction.
heading = departure.heading
position = departure.position.point_from_heading(
heading, nm_to_meter(5)
)
waypoint = FlightWaypoint(
FlightWaypointType.ASCEND_POINT,
position.x,
position.y,
500 if is_helo else self.doctrine.pattern_altitude
)
waypoint.name = "ASCEND"
waypoint.alt_type = "RADIO"
waypoint.description = "Ascend"
waypoint.pretty_name = "Ascend"
self.waypoints.append(waypoint)
def descent(self, arrival: ControlPoint, is_helo: bool = False) -> None:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
# TODO: Pick runway based on wind direction.
# ControlPoint.heading is the departure heading.
heading = (arrival.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 is_helo else self.doctrine.pattern_altitude
)
waypoint.name = "DESCEND"
waypoint.alt_type = "RADIO"
waypoint.description = "Descend to pattern altitude"
waypoint.pretty_name = "Ascend"
self.waypoints.append(waypoint)
def land(self, arrival: ControlPoint) -> None:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
position = arrival.position
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
0
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
self.waypoints.append(waypoint)
def join(self, position: Point) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.JOIN,
position.x,
position.y,
self.doctrine.ingress_altitude
)
waypoint.pretty_name = "Join"
waypoint.description = "Rendezvous with package"
waypoint.name = "JOIN"
self.waypoints.append(waypoint)
def split(self, position: Point) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.SPLIT,
position.x,
position.y,
self.doctrine.ingress_altitude
)
waypoint.pretty_name = "Split"
waypoint.description = "Depart from package"
waypoint.name = "SPLIT"
self.waypoints.append(waypoint)
def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
def ingress_strike(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective)
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> None:
if self.ingress_point is not None:
raise RuntimeError("A flight plan can have only one ingress point.")
waypoint = FlightWaypoint(
ingress_type,
position.x,
position.y,
self.doctrine.ingress_altitude
)
waypoint.pretty_name = "INGRESS on " + objective.name
waypoint.description = "INGRESS on " + objective.name
waypoint.name = "INGRESS"
self.waypoints.append(waypoint)
self.ingress_point = waypoint
def egress(self, position: Point, target: MissionTarget) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
position.x,
position.y,
self.doctrine.ingress_altitude
)
waypoint.pretty_name = "EGRESS from " + target.name
waypoint.description = "EGRESS from " + target.name
waypoint.name = "EGRESS"
self.waypoints.append(waypoint)
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
location)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = location
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
location)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = location
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE [{location.name}]: {name}",
location)
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
description: str, location: MissionTarget) -> None:
if self.ingress_point is None:
raise RuntimeError(
"An ingress point must be added before target points."
)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
target.position.x,
target.position.y,
0
)
waypoint.description = description
waypoint.pretty_name = description
waypoint.name = name
waypoint.only_for_player = True
self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before.
self.ingress_point.targets.append(location)
def sead_area(self, target: MissionTarget) -> None:
self._target_area(f"SEAD on {target.name}", target)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = target
def dead_area(self, target: MissionTarget) -> None:
self._target_area(f"DEAD on {target.name}", target)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = target
def _target_area(self, name: str, location: MissionTarget) -> None:
if self.ingress_point is None:
raise RuntimeError(
"An ingress point must be added before target points."
)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
0
)
waypoint.description = name
waypoint.pretty_name = name
waypoint.name = name
waypoint.only_for_player = True
self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before.
self.ingress_point.targets.append(location)
def cas(self, position: Point, altitude: int) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.CAS,
position.x,
position.y,
altitude
)
waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS"
waypoint.name = "CAS"
waypoint.pretty_name = "CAS"
self.waypoints.append(waypoint)
def race_track_start(self, position: Point, altitude: int) -> None:
"""Creates a racetrack start waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the racetrack in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
position.x,
position.y,
altitude
)
waypoint.name = "RACETRACK START"
waypoint.description = "Orbit between this point and the next point"
waypoint.pretty_name = "Race-track start"
self.waypoints.append(waypoint)
# TODO: Does this actually do anything?
# orbit0.targets.append(location)
# Note: Targets of PATROL TRACK waypoints are the points to be defended.
# orbit0.targets.append(flight.from_cp)
# orbit0.targets.append(center)
def race_track_end(self, position: Point, altitude: int) -> None:
"""Creates a racetrack end waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the racetrack in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PATROL,
position.x,
position.y,
altitude
)
waypoint.name = "RACETRACK END"
waypoint.description = "Orbit between this point and the previous point"
waypoint.pretty_name = "Race-track end"
self.waypoints.append(waypoint)
def race_track(self, start: Point, end: Point, altitude: int) -> None:
"""Creates two waypoint for a racetrack orbit.
Args:
start: The beginning racetrack waypoint.
end: The ending racetrack waypoint.
altitude: The racetrack altitude.
"""
self.race_track_start(start, altitude)
self.race_track_end(end, altitude)
def rtb(self, arrival: ControlPoint, is_helo: bool = False) -> None:
"""Creates descent ant landing waypoints for the given control point.
Args:
arrival: Arrival airfield or carrier.
"""
self.descent(arrival, is_helo)
self.land(arrival)

View File

@ -1,13 +1,13 @@
import random
from enum import Enum
from typing import Dict, List
from dcs.vehicles import *
from gen import Conflict
from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
from dcs.unittype import VehicleType
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
TYPE_TANKS = [
Armor.MBT_T_55,
@ -207,8 +207,8 @@ GROUP_SIZES_BY_COMBAT_STANCE = {
class CombatGroup:
def __init__(self, role:CombatGroupRole):
self.units = []
def __init__(self, role: CombatGroupRole):
self.units: List[VehicleType] = []
self.role = role
self.assigned_enemy_cp = None
self.start_position = None
@ -222,33 +222,22 @@ class CombatGroup:
class GroundPlanner:
cp = None
combat_groups_dict = {}
connected_enemy_cp = []
tank_groups = []
apc_group = []
ifv_group = []
art_group = []
shorad_groups = []
logi_groups = []
def __init__(self, cp:ControlPoint, game):
self.cp = cp
self.game = game
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured]
self.tank_groups = []
self.apc_group = []
self.ifv_group = []
self.art_group = []
self.atgm_group = []
self.logi_groups = []
self.shorad_groups = []
self.tank_groups: List[CombatGroup] = []
self.apc_group: List[CombatGroup] = []
self.ifv_group: List[CombatGroup] = []
self.art_group: List[CombatGroup] = []
self.atgm_group: List[CombatGroup] = []
self.logi_groups: List[CombatGroup] = []
self.shorad_groups: List[CombatGroup] = []
self.units_per_cp = {}
self.units_per_cp: Dict[int, List[CombatGroup]] = {}
for cp in self.connected_enemy_cp:
self.units_per_cp[cp.id] = []
self.reserve = []
self.reserve: List[CombatGroup] = []
def plan_groundwar(self):

View File

@ -1,11 +1,23 @@
from dcs.statics import *
from dcs.unit import Ship, Vehicle
import logging
import random
from typing import Dict, Iterator
from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS
from dcs import Mission
from dcs.statics import fortification_map, warehouse_map
from dcs.task import (
ActivateBeaconCommand,
ActivateICLSCommand,
EPLRS,
OptAlarmState,
)
from dcs.unit import Ship, Vehicle
from dcs.unitgroup import StaticGroup
from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name
from .airfields import RunwayData
from .conflictgen import *
from .naming import *
from .conflictgen import Conflict
from .radios import RadioRegistry
from .tacan import TacanBand, TacanRegistry
@ -26,7 +38,7 @@ class GroundObjectsGenerator:
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]:
def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]:
if self.conflict.is_vector:
center = self.conflict.center
heading = self.conflict.heading - 90
@ -80,6 +92,10 @@ class GroundObjectsGenerator:
vehicle.heading = u.heading
vehicle.player_can_drive = True
vg.add_unit(vehicle)
if hasattr(utype, 'eplrs'):
if utype.eplrs:
vg.points[0].tasks.append(EPLRS(vg.id))
else:
vg = self.m.ship_group(side, g.name, utype, position=g.position,
heading=g.units[0].heading)

View File

@ -82,6 +82,8 @@ class KneeboardPageWriter:
def table(self, cells: List[List[str]],
headers: Optional[List[str]] = None) -> None:
if headers is None:
headers = []
table = tabulate(cells, headers=headers, numalign="right")
self.text(table, font=self.table_font)
@ -136,7 +138,7 @@ class FlightPlanBuilder:
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
self.rows.append([
waypoint.number,
str(waypoint.number),
waypoint.waypoint.pretty_name,
str(int(units.meters_to_feet(waypoint.waypoint.alt)))
])
@ -194,7 +196,7 @@ class BriefingPage(KneeboardPage):
tankers.append([
tanker.callsign,
tanker.variant,
tanker.tacan,
str(tanker.tacan),
self.format_frequency(tanker.freq),
])
writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"])
@ -225,12 +227,22 @@ class BriefingPage(KneeboardPage):
atc = ""
if runway.atc is not None:
atc = self.format_frequency(runway.atc)
if runway.tacan is None:
tacan = ""
else:
tacan = str(runway.tacan)
if runway.ils is not None:
ils = str(runway.ils)
elif runway.icls is not None:
ils = str(runway.icls)
else:
ils = ""
return [
row_title,
runway.airfield_name,
atc,
runway.tacan or "",
runway.ils or runway.icls or "",
tacan,
ils,
runway.runway_name,
]

View File

@ -8,13 +8,13 @@ MISSILES_MAP = {
}
def generate_missile_group(game, ground_object, faction:str):
def generate_missile_group(game, ground_object, faction_name: str):
"""
This generate a ship group
:return: Nothing, but put the group reference inside the ground object
"""
faction = db.FACTIONS[faction]
if "missiles" in faction.keys():
faction = db.FACTIONS[faction_name]
if "missiles" in faction:
generators = faction["missiles"]
if len(generators) > 0:
gen = random.choice(generators)

View File

@ -1,19 +1,12 @@
import typing
import random
from datetime import datetime, timedelta, time
from dcs.action import MarkToAll
from dcs.condition import TimeAfter
from dcs.mission import Mission
from dcs.triggers import *
from dcs.condition import *
from dcs.action import *
from dcs.task import Option
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce
from dcs.unit import Skill
from dcs.point import MovingPoint, PointProperties
from dcs.action import *
from game import db
from theater import *
from gen.airsupportgen import AirSupportConflictGenerator
from gen import *
from .conflictgen import Conflict
PUSH_TRIGGER_SIZE = 3000
PUSH_TRIGGER_ACTIVATION_AGL = 25

View File

@ -1,18 +1,20 @@
import typing
from __future__ import annotations
import random
from datetime import datetime, timedelta
from typing import TYPE_CHECKING
from dcs.mapping import Point
from dcs.mission import Mission
from dcs.statics import *
from dcs.unit import Static
from dcs.unittype import StaticType
from theater import *
from .conflictgen import *
#from game.game import Game
from game import db
if TYPE_CHECKING:
from game import Game
from .conflictgen import Conflict, FRONTLINE_LENGTH
class MarkerSmoke(unittype.StaticType):
class MarkerSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
@ -20,7 +22,7 @@ class MarkerSmoke(unittype.StaticType):
rate = 0.1
class Smoke(unittype.StaticType):
class Smoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
@ -28,7 +30,7 @@ class Smoke(unittype.StaticType):
rate = 1
class BigSmoke(unittype.StaticType):
class BigSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
@ -36,7 +38,7 @@ class BigSmoke(unittype.StaticType):
rate = 1
class MassiveSmoke(unittype.StaticType):
class MassiveSmoke(StaticType):
id = "big_smoke"
category = "Effects"
name = "big_smoke"
@ -44,7 +46,7 @@ class MassiveSmoke(unittype.StaticType):
rate = 1
class Outpost(unittype.StaticType):
class Outpost(StaticType):
id = "outpost"
name = "outpost"
category = "Fortifications"
@ -90,9 +92,7 @@ def turn_heading(heading, fac):
class VisualGenerator:
game = None # type: Game
def __init__(self, mission: Mission, conflict: Conflict, game):
def __init__(self, mission: Mission, conflict: Conflict, game: Game):
self.mission = mission
self.conflict = conflict
self.game = game

12
mypy.ini Normal file
View File

@ -0,0 +1,12 @@
[mypy]
namespace_packages = True
[mypy-dcs.*]
follow_imports=silent
ignore_missing_imports = True
[mypy-PIL.*]
ignore_missing_imports = True
[mypy-winreg.*]
ignore_missing_imports = True

2
pydcs

@ -1 +1 @@
Subproject commit ceea62a8e0731c21b3e1a3e90682aa0affc168f1
Subproject commit c203e5a1b8d5eb42d559dab074e668bf37fa5158

View File

@ -25,7 +25,7 @@ class ERC_90(unittype.VehicleType):
detection_range = 0
threat_range = 4000
air_weapon_dist = 4000
eprls = True
eplrs = True
class VAB__50(unittype.VehicleType):
@ -34,7 +34,7 @@ class VAB__50(unittype.VehicleType):
detection_range = 0
threat_range = 1200
air_weapon_dist = 1200
eprls = True
eplrs = True
class VAB_T20_13(unittype.VehicleType):
@ -43,7 +43,7 @@ class VAB_T20_13(unittype.VehicleType):
detection_range = 0
threat_range = 2000
air_weapon_dist = 2000
eprls = True
eplrs = True
class VAB_MEPHISTO(unittype.VehicleType):
@ -52,7 +52,7 @@ class VAB_MEPHISTO(unittype.VehicleType):
detection_range = 0
threat_range = 4000
air_weapon_dist = 4000
eprls = True
eplrs = True
class VBL__50(unittype.VehicleType):
@ -61,7 +61,7 @@ class VBL__50(unittype.VehicleType):
detection_range = 0
threat_range = 1200
air_weapon_dist = 1200
eprls = True
eplrs = True
class VBL_AANF1(unittype.VehicleType):
@ -70,7 +70,7 @@ class VBL_AANF1(unittype.VehicleType):
detection_range = 0
threat_range = 1000
air_weapon_dist = 1000
eprls = True
eplrs = True
class VBAE_CRAB(unittype.VehicleType):
@ -79,7 +79,7 @@ class VBAE_CRAB(unittype.VehicleType):
detection_range = 0
threat_range = 3500
air_weapon_dist = 3500
eprls = True
eplrs = True
class VBAE_CRAB_MMP(unittype.VehicleType):
@ -88,7 +88,7 @@ class VBAE_CRAB_MMP(unittype.VehicleType):
detection_range = 0
threat_range = 3500
air_weapon_dist = 3500
eprls = True
eplrs = True
class AMX_30B2(unittype.VehicleType):
@ -121,7 +121,7 @@ class DIM__TOYOTA_BLUE(unittype.VehicleType):
detection_range = 0
threat_range = 1200
air_weapon_dist = 1200
eprls = True
eplrs = True
class DIM__TOYOTA_GREEN(unittype.VehicleType):
@ -130,7 +130,7 @@ class DIM__TOYOTA_GREEN(unittype.VehicleType):
detection_range = 0
threat_range = 1200
air_weapon_dist = 1200
eprls = True
eplrs = True
class DIM__TOYOTA_DESERT(unittype.VehicleType):
@ -139,7 +139,7 @@ class DIM__TOYOTA_DESERT(unittype.VehicleType):
detection_range = 0
threat_range = 1200
air_weapon_dist = 1200
eprls = True
eplrs = True
class DIM__KAMIKAZE(unittype.VehicleType):
@ -148,7 +148,7 @@ class DIM__KAMIKAZE(unittype.VehicleType):
detection_range = 0
threat_range = 50
air_weapon_dist = 50
eprls = True
eplrs = True
## FORTIFICATION
@ -187,7 +187,7 @@ class TRM_2000(unittype.VehicleType):
detection_range = 3500
threat_range = 0
air_weapon_dist = 0
eprls = True
eplrs = True
class TRM_2000_Fuel(unittype.VehicleType):
id = "TRM2000_Citerne"
@ -195,7 +195,7 @@ class TRM_2000_Fuel(unittype.VehicleType):
detection_range = 3500
threat_range = 0
air_weapon_dist = 0
eprls = True
eplrs = True
class VAB_MEDICAL(unittype.VehicleType):
id = "VABH"
@ -203,7 +203,7 @@ class VAB_MEDICAL(unittype.VehicleType):
detection_range = 0
threat_range = 0
air_weapon_dist = 0
eprls = True
eplrs = True
class VAB(unittype.VehicleType):
id = "VAB_RADIO"
@ -211,7 +211,7 @@ class VAB(unittype.VehicleType):
detection_range = 0
threat_range = 0
air_weapon_dist = 0
eprls = True
eplrs = True
class VBL(unittype.VehicleType):
id = "VBL-Radio"
@ -219,7 +219,7 @@ class VBL(unittype.VehicleType):
detection_range = 0
threat_range = 0
air_weapon_dist = 0
eprls = True
eplrs = True
class Tracma_TD_1500(unittype.VehicleType):
id = "Tracma"
@ -236,7 +236,7 @@ class SMOKE_SAM_IR(unittype.VehicleType):
detection_range = 20000
threat_range = 20000
air_weapon_dist = 20000
eprls = True
eplrs = True
class _53T2(unittype.VehicleType):
id = "AA20"
@ -251,7 +251,7 @@ class TRM_2000_53T2(unittype.VehicleType):
detection_range = 6000
threat_range = 2000
air_weapon_dist = 2000
eprls = True
eplrs = True
class TRM_2000_PAMELA(unittype.VehicleType):
id = "TRMMISTRAL"
@ -259,7 +259,7 @@ class TRM_2000_PAMELA(unittype.VehicleType):
detection_range = 8000
threat_range = 10000
air_weapon_dist = 10000
eprls = True
eplrs = True
## INFANTRY
@ -285,4 +285,4 @@ class VAB_MORTIER(unittype.VehicleType):
detection_range = 0
threat_range = 15000
air_weapon_dist = 15000
eprls = True
eplrs = True

65
qt_ui/dialogs.py Normal file
View File

@ -0,0 +1,65 @@
"""Application-wide dialog management."""
from typing import Optional
from gen.flights.flight import Flight
from theater.missiontarget import MissionTarget
from .models import GameModel, PackageModel
from .windows.mission.QEditFlightDialog import QEditFlightDialog
from .windows.mission.QPackageDialog import (
QEditPackageDialog,
QNewPackageDialog,
)
class Dialog:
"""Dialog management singleton.
Opens dialogs and keeps references to dialog windows so that their creators
do not need to worry about the lifetime of the dialog object, and can open
dialogs without needing to have their own reference to common data like the
game model.
"""
#: The game model. Is only None before initialization, as the game model
#: itself is responsible for handling the case where no game is loaded.
game_model: Optional[GameModel] = None
new_package_dialog: Optional[QNewPackageDialog] = None
edit_package_dialog: Optional[QEditPackageDialog] = None
edit_flight_dialog: Optional[QEditFlightDialog] = None
@classmethod
def set_game(cls, game_model: GameModel) -> None:
"""Sets the game model."""
cls.game_model = game_model
@classmethod
def open_new_package_dialog(cls, mission_target: MissionTarget):
"""Opens the dialog to create a new package with the given target."""
cls.new_package_dialog = QNewPackageDialog(
cls.game_model.game,
cls.game_model.ato_model,
mission_target
)
cls.new_package_dialog.show()
@classmethod
def open_edit_package_dialog(cls, package_model: PackageModel):
"""Opens the dialog to edit the given package."""
cls.edit_package_dialog = QEditPackageDialog(
cls.game_model.game,
cls.game_model.ato_model,
package_model
)
cls.edit_package_dialog.show()
@classmethod
def open_edit_flight_dialog(cls, package_model: PackageModel,
flight: Flight) -> None:
"""Opens the dialog to edit the given flight."""
cls.edit_flight_dialog = QEditFlightDialog(
cls.game_model.game,
package_model.package,
flight
)
cls.edit_flight_dialog.show()

View File

@ -4,13 +4,14 @@ from shutil import copyfile
import dcs
from userdata import persistency
from game import persistency
global __dcs_saved_game_directory
global __dcs_installation_directory
PREFERENCES_FILE_PATH = "liberation_preferences.json"
def init():
global __dcs_saved_game_directory
global __dcs_installation_directory

View File

@ -1,7 +1,7 @@
import json
import logging
import os
import qt_ui.uiconstants as CONST
from typing import Dict
global __theme_index
@ -10,29 +10,44 @@ THEME_PREFERENCES_FILE_PATH = "liberation_theme.json"
DEFAULT_THEME_INDEX = 1
# new themes can be added here
THEMES: Dict[int, Dict[str, str]] = {
0: {'themeName': 'Vanilla',
'themeFile': 'windows-style.css',
'themeIcons': 'medium',
},
1: {'themeName': 'DCS World',
'themeFile': 'style-dcs.css',
'themeIcons': 'light',
},
}
def init():
global __theme_index
__theme_index = DEFAULT_THEME_INDEX
print("init setting theme index to " + str(__theme_index))
if os.path.isfile(THEME_PREFERENCES_FILE_PATH):
try:
with(open(THEME_PREFERENCES_FILE_PATH)) as prefs:
pref_data = json.loads(prefs.read())
__theme_index = pref_data["theme_index"]
print(__theme_index)
set_theme_index(__theme_index)
save_theme_config()
print("file setting theme index to " + str(__theme_index))
except:
# is this necessary?
set_theme_index(DEFAULT_THEME_INDEX)
print("except setting theme index to " + str(__theme_index))
logging.exception("Unable to change theme")
else:
# is this necessary?
set_theme_index(DEFAULT_THEME_INDEX)
print("else setting theme index to " + str(__theme_index))
logging.error(
f"Using default theme because {THEME_PREFERENCES_FILE_PATH} "
"does not exist"
)
# set theme index then use save_theme_config to save to file
@ -49,19 +64,19 @@ def get_theme_index():
# get theme name based on current index
def get_theme_name():
theme_name = CONST.THEMES[get_theme_index()]['themeName']
theme_name = THEMES[get_theme_index()]['themeName']
return theme_name
# get theme icon sub-folder name based on current index
def get_theme_icons():
theme_icons = CONST.THEMES[get_theme_index()]['themeIcons']
theme_icons = THEMES[get_theme_index()]['themeIcons']
return str(theme_icons)
# get theme stylesheet css based on current index
def get_theme_css_file():
theme_file = CONST.THEMES[get_theme_index()]['themeFile']
theme_file = THEMES[get_theme_index()]['themeFile']
return str(theme_file)

22
qt_ui/logging_config.py Normal file
View File

@ -0,0 +1,22 @@
"""Logging APIs."""
import logging
import os
from logging.handlers import RotatingFileHandler
def init_logging(version: str) -> None:
"""Initializes the logging configuration."""
if not os.path.isdir("./logs"):
os.mkdir("logs")
fmt = "%(asctime)s :: %(levelname)s :: %(message)s"
logging.basicConfig(level=logging.DEBUG, format=fmt)
logger = logging.getLogger()
handler = RotatingFileHandler('./logs/liberation.log', 'a', 5000000, 1)
handler.setLevel(logging.INFO)
handler.setFormatter(logging.Formatter(fmt))
logger.addHandler(handler)
logger.info(f"DCS Liberation {version}")

View File

@ -1,5 +1,3 @@
from userdata import logging_config
import logging
import os
import sys
@ -9,11 +7,17 @@ from PySide2 import QtWidgets
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen
from qt_ui import uiconstants
from game import persistency
from qt_ui import (
liberation_install,
liberation_theme,
logging_config,
uiconstants,
)
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QLiberationWindow import QLiberationWindow
from qt_ui.windows.preferences.QLiberationFirstStartWindow import QLiberationFirstStartWindow
from userdata import liberation_install, persistency, liberation_theme
from qt_ui.windows.preferences.QLiberationFirstStartWindow import \
QLiberationFirstStartWindow
# Logging setup
logging_config.init_logging(uiconstants.VERSION_STRING)

268
qt_ui/models.py Normal file
View File

@ -0,0 +1,268 @@
"""Qt data models for game objects."""
from typing import Any, Callable, Dict, Iterator, TypeVar, Optional
from PySide2.QtCore import (
QAbstractListModel,
QModelIndex,
Qt,
Signal,
)
from PySide2.QtGui import QIcon
from game import db
from game.game import Game
from gen.ato import AirTaskingOrder, Package
from gen.flights.flight import Flight
from qt_ui.uiconstants import AIRCRAFT_ICONS
from theater.missiontarget import MissionTarget
class DeletableChildModelManager:
"""Manages lifetimes for child models.
Qt's data models don't have a good way of modeling related data aside from
lists, tables, or trees of similar objects. We could build one monolithic
GameModel that tracks all of the data in the game and use the parent/child
relationships of that model to index down into the ATO, packages, flights,
etc, but doing so is error prone because it requires us to manually manage
that relationship tree and keep our own mappings from row/column into
specific members.
However, creating child models outside of the tree means that removing an
item from the parent will not signal the child's deletion to any views, so
we must track this explicitly.
Any model which has child data types should use this class to track the
deletion of child models. All child model types must define a signal named
`deleted`. This signal will be emitted when the child model is being
deleted. Any views displaying such data should subscribe to those events and
update their display accordingly.
"""
#: The type of data owned by models created by this class.
DataType = TypeVar("DataType")
#: The type of model managed by this class.
ModelType = TypeVar("ModelType")
ModelDict = Dict[DataType, ModelType]
def __init__(self, create_model: Callable[[DataType], ModelType]) -> None:
self.create_model = create_model
self.models: DeletableChildModelManager.ModelDict = {}
def acquire(self, data: DataType) -> ModelType:
"""Returns a model for the given child data.
If a model has already been created for the given data, it will be
returned. The data type must be hashable.
"""
if data in self.models:
return self.models[data]
model = self.create_model(data)
self.models[data] = model
return model
def release(self, data: DataType) -> None:
"""Releases the model matching the given data, if one exists.
If the given data has had a model created for it, that model will be
deleted and its `deleted` signal will be emitted.
"""
if data in self.models:
model = self.models[data]
del self.models[data]
model.deleted.emit()
def clear(self) -> None:
"""Deletes all managed models."""
for data in list(self.models.keys()):
self.release(data)
class NullListModel(QAbstractListModel):
"""Generic empty list model."""
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return 0
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
return None
class PackageModel(QAbstractListModel):
"""The model for an ATO package."""
#: Emitted when this package is being deleted from the ATO.
deleted = Signal()
def __init__(self, package: Package) -> None:
super().__init__()
self.package = package
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.package.flights)
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
return None
flight = self.flight_at_index(index)
if role == Qt.DisplayRole:
return self.text_for_flight(flight)
if role == Qt.DecorationRole:
return self.icon_for_flight(flight)
return None
@staticmethod
def text_for_flight(flight: Flight) -> str:
"""Returns the text that should be displayed for the flight."""
task = flight.flight_type.name
count = flight.count
name = db.unit_type_name(flight.unit_type)
delay = flight.scheduled_in
origin = flight.from_cp.name
return f"[{task}] {count} x {name} from {origin} in {delay} minutes"
@staticmethod
def icon_for_flight(flight: Flight) -> Optional[QIcon]:
"""Returns the icon that should be displayed for the flight."""
name = db.unit_type_name(flight.unit_type)
if name in AIRCRAFT_ICONS:
return QIcon(AIRCRAFT_ICONS[name])
return None
def add_flight(self, flight: Flight) -> None:
"""Adds the given flight to the package."""
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.package.add_flight(flight)
self.endInsertRows()
def delete_flight_at_index(self, index: QModelIndex) -> None:
"""Removes the flight at the given index from the package."""
self.delete_flight(self.flight_at_index(index))
def delete_flight(self, flight: Flight) -> None:
"""Removes the given flight from the package.
If the flight is using claimed inventory, the caller is responsible for
returning that inventory.
"""
index = self.package.flights.index(flight)
self.beginRemoveRows(QModelIndex(), index, index)
self.package.remove_flight(flight)
self.endRemoveRows()
def flight_at_index(self, index: QModelIndex) -> Flight:
"""Returns the flight located at the given index."""
return self.package.flights[index.row()]
@property
def mission_target(self) -> MissionTarget:
"""Returns the mission target of the package."""
package = self.package
target = package.target
return target
@property
def description(self) -> str:
"""Returns the description of the package."""
return self.package.package_description
@property
def flights(self) -> Iterator[Flight]:
"""Iterates over the flights in the package."""
for flight in self.package.flights:
yield flight
class AtoModel(QAbstractListModel):
"""The model for an AirTaskingOrder."""
def __init__(self, game: Optional[Game], ato: AirTaskingOrder) -> None:
super().__init__()
self.game = game
self.ato = ato
self.package_models = DeletableChildModelManager(PackageModel)
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self.ato.packages)
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid():
return None
if role == Qt.DisplayRole:
package = self.ato.packages[index.row()]
return f"{package.package_description} {package.target.name}"
return None
def add_package(self, package: Package) -> None:
"""Adds a package to the ATO."""
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.ato.add_package(package)
self.endInsertRows()
def delete_package_at_index(self, index: QModelIndex) -> None:
"""Removes the package at the given index from the ATO."""
self.delete_package(self.package_at_index(index))
def delete_package(self, package: Package) -> None:
"""Removes the given package from the ATO."""
self.package_models.release(package)
index = self.ato.packages.index(package)
self.beginRemoveRows(QModelIndex(), index, index)
self.ato.remove_package(package)
for flight in package.flights:
self.game.aircraft_inventory.return_from_flight(flight)
self.endRemoveRows()
def package_at_index(self, index: QModelIndex) -> Package:
"""Returns the package at the given index."""
return self.ato.packages[index.row()]
def replace_from_game(self, game: Optional[Game]) -> None:
"""Updates the ATO object to match the updated game object.
If the game is None (as is the case when no game has been loaded), an
empty ATO will be used.
"""
self.beginResetModel()
self.game = game
self.package_models.clear()
if self.game is not None:
self.ato = game.blue_ato
else:
self.ato = AirTaskingOrder()
self.endResetModel()
def get_package_model(self, index: QModelIndex) -> PackageModel:
"""Returns a model for the package at the given index."""
return self.package_models.acquire(self.package_at_index(index))
@property
def packages(self) -> Iterator[PackageModel]:
"""Iterates over all the packages in the ATO."""
for package in self.ato.packages:
yield self.package_models.acquire(package)
class GameModel:
"""A model for the Game object.
This isn't a real Qt data model, but simplifies management of the game and
its ATO objects.
"""
def __init__(self) -> None:
self.game: Optional[Game] = None
# TODO: Add red ATO model, add cheat option to show red flight plan.
self.ato_model = AtoModel(self.game, AirTaskingOrder())
def set(self, game: Optional[Game]) -> None:
"""Updates the managed Game object.
The argument will be None when no game has been loaded. In this state,
much of the UI is still visible and needs to handle that behavior. To
simplify that case, the AtoModel will model an empty ATO when no game is
loaded.
"""
self.game = game
self.ato_model.replace_from_game(self.game)

View File

@ -1,14 +1,12 @@
# URL for UI links
import os
from typing import Dict
from PySide2.QtGui import QColor, QFont, QPixmap
from game.event import UnitsDeliveryEvent, FrontlineAttackEvent
from theater.theatergroundobject import CATEGORY_MAP
from userdata.liberation_theme import get_theme_icons
from .liberation_theme import get_theme_icons
VERSION_STRING = "2.1.3"
VERSION_STRING = "2.1.4"
URLS : Dict[str, str] = {
"Manual": "https://github.com/khopa/dcs_liberation/wiki",
@ -28,20 +26,6 @@ FONT_PRIMARY_I = QFont(FONT_NAME, FONT_SIZE, weight=5, italic=True)
FONT_PRIMARY_B = QFont(FONT_NAME, FONT_SIZE, weight=75, italic=False)
FONT_MAP = QFont(FONT_NAME, 10, weight=75, italic=False)
# new themes can be added here
THEMES: Dict[int, Dict[str, str]] = {
0: {'themeName': 'Vanilla',
'themeFile': 'windows-style.css',
'themeIcons': 'medium',
},
1: {'themeName': 'DCS World',
'themeFile': 'style-dcs.css',
'themeIcons': 'light',
},
}
COLORS: Dict[str, QColor] = {
"white": QColor(255, 255, 255),
"white_transparent": QColor(255, 255, 255, 35),
@ -85,10 +69,10 @@ def load_icons():
ICONS["Hangar"] = QPixmap("./resources/ui/misc/hangar.png")
ICONS["Terrain_Caucasus"] = QPixmap("./resources/ui/terrain_caucasus.gif")
ICONS["Terrain_Persian_Gulf"] = QPixmap("./resources/ui/terrain_pg.gif")
ICONS["Terrain_PersianGulf"] = QPixmap("./resources/ui/terrain_pg.gif")
ICONS["Terrain_Nevada"] = QPixmap("./resources/ui/terrain_nevada.gif")
ICONS["Terrain_Normandy"] = QPixmap("./resources/ui/terrain_normandy.gif")
ICONS["Terrain_Channel"] = QPixmap("./resources/ui/terrain_channel.gif")
ICONS["Terrain_TheChannel"] = QPixmap("./resources/ui/terrain_channel.gif")
ICONS["Terrain_Syria"] = QPixmap("./resources/ui/terrain_syria.gif")
ICONS["Dawn"] = QPixmap("./resources/ui/daytime/dawn.png")
@ -127,15 +111,12 @@ EVENT_ICONS: Dict[str, QPixmap] = {}
def load_event_icons():
for image in os.listdir("./resources/ui/events/"):
print(image)
if image.endswith(".PNG"):
EVENT_ICONS[image[:-4]] = QPixmap(os.path.join("./resources/ui/events/", image))
def load_aircraft_icons():
for aircraft in os.listdir("./resources/ui/units/aircrafts/"):
print(aircraft)
if aircraft.endswith(".jpg"):
print(aircraft[:-7] + " : " + os.path.join("./resources/ui/units/aircrafts/", aircraft) + " ")
AIRCRAFT_ICONS[aircraft[:-7]] = QPixmap(os.path.join("./resources/ui/units/aircrafts/", aircraft))
AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"]
AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"]
@ -144,7 +125,5 @@ def load_aircraft_icons():
def load_vehicle_icons():
for vehicle in os.listdir("./resources/ui/units/vehicles/"):
print(vehicle)
if vehicle.endswith(".jpg"):
print(vehicle[:-7] + " : " + os.path.join("./resources/ui/units/vehicles/", vehicle) + " ")
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(os.path.join("./resources/ui/units/vehicles/", vehicle))

View File

@ -0,0 +1,13 @@
"""Spin box for selecting the number of aircraft in a flight."""
from PySide2.QtWidgets import QSpinBox
class QFlightSizeSpinner(QSpinBox):
"""Spin box for selecting the number of aircraft in a flight."""
def __init__(self, min_size: int = 1, max_size: int = 4,
default_size: int = 2) -> None:
super().__init__()
self.setMinimum(min_size)
self.setMaximum(max_size)
self.setValue(default_size)

View File

@ -0,0 +1,17 @@
"""A layout containing a widget with an associated label."""
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QHBoxLayout, QLabel, QWidget
class QLabeledWidget(QHBoxLayout):
"""A layout containing a widget with an associated label.
Best used for vertical forms, where the given widget is the input and the
label is used to name the input.
"""
def __init__(self, text: str, widget: QWidget) -> None:
super().__init__()
self.addWidget(QLabel(text))
self.addStretch()
self.addWidget(widget, alignment=Qt.AlignRight)

View File

@ -1,16 +1,17 @@
from PySide2.QtWidgets import QFrame, QHBoxLayout, QPushButton, QVBoxLayout, QGroupBox
from typing import Optional
from game import Game
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
from qt_ui.windows.finances.QFinancesMenu import QFinancesMenu
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
from qt_ui.widgets.QTurnCounter import QTurnCounter
from PySide2.QtWidgets import QFrame, QGroupBox, QHBoxLayout, QPushButton
import qt_ui.uiconstants as CONST
from game import Game
from game.event import CAP, CAS, FrontlineAttackEvent
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
from qt_ui.widgets.QTurnCounter import QTurnCounter
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.mission.QMissionPlanning import QMissionPlanning
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
class QTopPanel(QFrame):
@ -33,10 +34,10 @@ class QTopPanel(QFrame):
self.passTurnButton.setProperty("style", "btn-primary")
self.passTurnButton.clicked.connect(self.passTurn)
self.proceedButton = QPushButton("Mission Planning")
self.proceedButton = QPushButton("Take off")
self.proceedButton.setIcon(CONST.ICONS["Proceed"])
self.proceedButton.setProperty("style", "btn-success")
self.proceedButton.clicked.connect(self.proceed)
self.proceedButton.setProperty("style", "start-button")
self.proceedButton.clicked.connect(self.launch_mission)
if self.game and self.game.turn == 0:
self.proceedButton.setEnabled(False)
@ -75,7 +76,7 @@ class QTopPanel(QFrame):
self.layout.setContentsMargins(0,0,0,0)
self.setLayout(self.layout)
def setGame(self, game:Game):
def setGame(self, game: Optional[Game]):
self.game = game
if game is not None:
self.turnCounter.setCurrentTurn(self.game.turn, self.game.current_day)
@ -100,9 +101,31 @@ class QTopPanel(QFrame):
GameUpdateSignal.get_instance().updateGame(self.game)
self.proceedButton.setEnabled(True)
def proceed(self):
self.subwindow = QMissionPlanning(self.game)
self.subwindow.show()
def launch_mission(self):
"""Finishes planning and waits for mission completion."""
# TODO: Refactor this nonsense.
game_event = None
for event in self.game.events:
if isinstance(event,
FrontlineAttackEvent) and event.is_player_attacking:
game_event = event
if game_event is None:
game_event = FrontlineAttackEvent(
self.game,
self.game.theater.controlpoints[0],
self.game.theater.controlpoints[0],
self.game.theater.controlpoints[0].position,
self.game.player_name,
self.game.enemy_name)
game_event.is_awacs_enabled = True
game_event.ca_slots = 1
game_event.departure_cp = self.game.theater.controlpoints[0]
game_event.player_attacking({CAS: {}, CAP: {}})
game_event.depart_from = self.game.theater.controlpoints[0]
self.game.initiate_event(game_event)
waiting = QWaitingForMissionResultWindow(game_event, self.game)
waiting.show()
def budget_update(self, game:Game):
self.budgetBox.setGame(game)

252
qt_ui/widgets/ato.py Normal file
View File

@ -0,0 +1,252 @@
"""Widgets for displaying air tasking orders."""
import logging
from typing import Optional
from PySide2.QtCore import QItemSelectionModel, QModelIndex, QSize, Qt
from PySide2.QtWidgets import (
QAbstractItemView,
QGroupBox,
QHBoxLayout,
QListView,
QPushButton,
QSplitter,
QVBoxLayout,
)
from gen.ato import Package
from gen.flights.flight import Flight
from ..models import AtoModel, GameModel, NullListModel, PackageModel
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
class QFlightList(QListView):
"""List view for displaying the flights of a package."""
def __init__(self, model: Optional[PackageModel]) -> None:
super().__init__()
self.package_model = model
self.set_package(model)
self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
def set_package(self, model: Optional[PackageModel]) -> None:
"""Sets the package model to display."""
if model is None:
self.disconnect_model()
else:
self.package_model = model
self.setModel(model)
# noinspection PyUnresolvedReferences
model.deleted.connect(self.disconnect_model)
self.selectionModel().setCurrentIndex(
model.index(0, 0, QModelIndex()),
QItemSelectionModel.Select
)
def disconnect_model(self) -> None:
"""Clears the listview of any model attachments.
Displays an empty list until set_package is called with a valid model.
"""
model = self.model()
if model is not None and isinstance(model, PackageModel):
model.deleted.disconnect(self.disconnect_model)
self.setModel(NullListModel())
@property
def selected_item(self) -> Optional[Flight]:
"""Returns the selected flight, if any."""
index = self.currentIndex()
if not index.isValid():
return None
return self.package_model.flight_at_index(index)
class QFlightPanel(QGroupBox):
"""The flight display portion of the ATO panel.
Displays the flights assigned to the selected package, and includes edit and
delete buttons for flight management.
"""
def __init__(self, game_model: GameModel,
package_model: Optional[PackageModel] = None) -> None:
super().__init__("Flights")
self.game_model = game_model
self.package_model = package_model
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
self.flight_list = QFlightList(package_model)
self.vbox.addWidget(self.flight_list)
self.button_row = QHBoxLayout()
self.vbox.addLayout(self.button_row)
self.edit_button = QPushButton("Edit")
self.edit_button.clicked.connect(self.on_edit)
self.button_row.addWidget(self.edit_button)
self.delete_button = QPushButton("Delete")
# noinspection PyTypeChecker
self.delete_button.setProperty("style", "btn-danger")
self.delete_button.clicked.connect(self.on_delete)
self.button_row.addWidget(self.delete_button)
self.selection_changed.connect(self.on_selection_changed)
self.on_selection_changed()
def set_package(self, model: Optional[PackageModel]) -> None:
"""Sets the package model to display."""
self.package_model = model
self.flight_list.set_package(model)
self.on_selection_changed()
@property
def selection_changed(self):
"""Returns the signal emitted when the flight selection changes."""
return self.flight_list.selectionModel().selectionChanged
def on_selection_changed(self) -> None:
"""Updates the status of the edit and delete buttons."""
index = self.flight_list.currentIndex()
enabled = index.isValid()
self.edit_button.setEnabled(enabled)
self.delete_button.setEnabled(enabled)
def on_edit(self) -> None:
"""Opens the flight edit dialog."""
index = self.flight_list.currentIndex()
if not index.isValid():
logging.error(f"Cannot edit flight when no flight is selected.")
return
from qt_ui.dialogs import Dialog
Dialog.open_edit_flight_dialog(
self.package_model, self.package_model.flight_at_index(index)
)
def on_delete(self) -> None:
"""Removes the selected flight from the package."""
index = self.flight_list.currentIndex()
if not index.isValid():
logging.error(f"Cannot delete flight when no flight is selected.")
return
self.game_model.game.aircraft_inventory.return_from_flight(
self.flight_list.selected_item)
self.package_model.delete_flight_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
class QPackageList(QListView):
"""List view for displaying the packages of an ATO."""
def __init__(self, model: AtoModel) -> None:
super().__init__()
self.ato_model = model
self.setModel(model)
self.setIconSize(QSize(91, 24))
self.setSelectionBehavior(QAbstractItemView.SelectItems)
@property
def selected_item(self) -> Optional[Package]:
"""Returns the selected package, if any."""
index = self.currentIndex()
if not index.isValid():
return None
return self.ato_model.package_at_index(index)
class QPackagePanel(QGroupBox):
"""The package display portion of the ATO panel.
Displays the package assigned to the player's ATO, and includes edit and
delete buttons for package management.
"""
def __init__(self, model: AtoModel) -> None:
super().__init__("Packages")
self.ato_model = model
self.ato_model.layoutChanged.connect(self.on_selection_changed)
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
self.package_list = QPackageList(self.ato_model)
self.vbox.addWidget(self.package_list)
self.button_row = QHBoxLayout()
self.vbox.addLayout(self.button_row)
self.edit_button = QPushButton("Edit")
self.edit_button.clicked.connect(self.on_edit)
self.button_row.addWidget(self.edit_button)
self.delete_button = QPushButton("Delete")
# noinspection PyTypeChecker
self.delete_button.setProperty("style", "btn-danger")
self.delete_button.clicked.connect(self.on_delete)
self.button_row.addWidget(self.delete_button)
self.selection_changed.connect(self.on_selection_changed)
self.on_selection_changed()
@property
def selection_changed(self):
"""Returns the signal emitted when the flight selection changes."""
return self.package_list.selectionModel().selectionChanged
def on_selection_changed(self) -> None:
"""Updates the status of the edit and delete buttons."""
index = self.package_list.currentIndex()
enabled = index.isValid()
self.edit_button.setEnabled(enabled)
self.delete_button.setEnabled(enabled)
def on_edit(self) -> None:
"""Opens the package edit dialog."""
index = self.package_list.currentIndex()
if not index.isValid():
logging.error(f"Cannot edit package when no package is selected.")
return
from qt_ui.dialogs import Dialog
Dialog.open_edit_package_dialog(self.ato_model.get_package_model(index))
def on_delete(self) -> None:
"""Removes the package from the ATO."""
index = self.package_list.currentIndex()
if not index.isValid():
logging.error(f"Cannot delete package when no package is selected.")
return
self.ato_model.delete_package_at_index(index)
GameUpdateSignal.get_instance().redraw_flight_paths()
class QAirTaskingOrderPanel(QSplitter):
"""A split panel for displaying the packages and flights of an ATO.
Used as the left-bar of the main UI. The top half of the panel displays the
packages of the player's ATO, and the bottom half displays the flights of
the selected package.
"""
def __init__(self, game_model: GameModel) -> None:
super().__init__(Qt.Vertical)
self.ato_model = game_model.ato_model
self.package_panel = QPackagePanel(self.ato_model)
self.package_panel.selection_changed.connect(self.on_package_change)
self.ato_model.rowsInserted.connect(self.on_package_change)
self.addWidget(self.package_panel)
self.flight_panel = QFlightPanel(game_model)
self.addWidget(self.flight_panel)
def on_package_change(self) -> None:
"""Sets the newly selected flight for display in the bottom panel."""
index = self.package_panel.package_list.currentIndex()
if index.isValid():
self.flight_panel.set_package(
self.ato_model.get_package_model(index)
)
else:
self.flight_panel.set_package(None)

View File

@ -0,0 +1,16 @@
"""Combo box for selecting aircraft types."""
from typing import Iterable
from PySide2.QtWidgets import QComboBox
from dcs.planes import PlaneType
class QAircraftTypeSelector(QComboBox):
"""Combo box for selecting among the given aircraft types."""
def __init__(self, aircraft_types: Iterable[PlaneType]) -> None:
super().__init__()
for aircraft in aircraft_types:
self.addItem(f"{aircraft.id}", userData=aircraft)
self.model().sort(0)

Some files were not shown because too many files have changed in this diff Show More