Merge branch 'develop' into helipads

# Conflicts:
#	game/data/weapons.py
#	game/db.py
#	game/theater/conflicttheater.py
#	resources/factions/france_1995.json
#	resources/factions/insurgents.json
#	resources/factions/iraq_1991.json
#	resources/factions/syria_1967_with_ww2_weapons.json
#	resources/factions/syria_2011.json
This commit is contained in:
Khopa
2021-05-21 13:58:22 +02:00
55 changed files with 345 additions and 132 deletions

View File

@@ -815,12 +815,13 @@ class AircraftConflictGenerator:
self.air_support.awacs.append(
AwacsInfo(
dcsGroupName=str(group.name),
group_name=str(group.name),
callsign=callsign,
freq=channel,
depature_location=flight.departure.name,
end_time=flight.flight_plan.mission_departure_time,
start_time=flight.flight_plan.mission_start_time,
blue=flight.departure.captured,
)
)
@@ -1275,7 +1276,7 @@ class AircraftConflictGenerator:
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
rtb_winchester=OptRTBOnOutOfAmmo.Values.All,
restrict_jettison=True,
)
@@ -1410,9 +1411,6 @@ class AircraftConflictGenerator:
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
# Escort groups are actually given the CAP task so they can perform the
# Search Then Engage task, which we have to use instead of the Escort
# task for the reasons explained in JoinPointBuilder.
group.task = Transport.name
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
@@ -1761,7 +1759,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_name)
if tgroup is not None:
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Guided)
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
task.params["expend"] = "All"
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
@@ -1866,12 +1864,11 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
center.y += target.position.y
center.x /= len(targets)
center.y /= len(targets)
bombing = Bombing(center)
bombing = Bombing(center, weapon_type=WeaponType.Bombs)
bombing.params["expend"] = "All"
bombing.params["attackQtyLimit"] = False
bombing.params["directionEnabled"] = False
bombing.params["altitudeEnabled"] = False
bombing.params["weaponType"] = WeaponType.Bombs.value
bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing)
return waypoint
@@ -1879,11 +1876,10 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
def build_strike(self) -> MovingPoint:
waypoint = super().build()
for target in self.waypoint.targets:
bombing = Bombing(target.position)
bombing = Bombing(target.position, weapon_type=WeaponType.Auto)
# If there is only one target, drop all ordnance in one pass.
if len(self.waypoint.targets) == 1:
bombing.params["expend"] = "All"
bombing.params["weaponType"] = WeaponType.Auto.value
bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing)
@@ -1954,7 +1950,10 @@ class JoinPointBuilder(PydcsWaypointBuilder):
EngageTargets(
# TODO: From doctrine.
max_distance=int(nautical_miles(30).meters),
targets=[Targets.All.Air.Planes.Fighters],
targets=[
Targets.All.Air.Planes.Fighters,
Targets.All.Air.Planes.MultiroleFighters,
],
)
)
)

View File

@@ -35,23 +35,25 @@ AWACS_ALT = 13000
class AwacsInfo:
"""AWACS information for the kneeboard."""
dcsGroupName: str
group_name: str
callsign: str
freq: RadioFrequency
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
dcsGroupName: str
group_name: str
callsign: str
variant: str
freq: RadioFrequency
tacan: TacanChannel
blue: bool
@dataclass
@@ -165,7 +167,14 @@ class AirSupportConflictGenerator:
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)
TankerInfo(
str(tanker_group.name),
callsign,
variant,
freq,
tacan,
blue=True,
)
)
if not self.game.settings.disable_legacy_aewc:
@@ -196,12 +205,13 @@ class AirSupportConflictGenerator:
self.air_support.awacs.append(
AwacsInfo(
dcsGroupName=str(awacs_flight.name),
group_name=str(awacs_flight.name),
callsign=callsign_for_support_unit(awacs_flight),
freq=freq,
depature_location=None,
start_time=None,
end_time=None,
blue=True,
)
)
else:

View File

@@ -68,11 +68,12 @@ INFANTRY_GROUP_SIZE = 5
class JtacInfo:
"""JTAC information."""
dcsGroupName: str
group_name: str
unit_name: str
callsign: str
region: str
code: str
blue: bool
# TODO: Radio info? Type?
@@ -196,7 +197,14 @@ class GroundConflictGenerator:
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
self.jtacs.append(
JtacInfo(str(jtac.name), n, callsign, frontline, str(code))
JtacInfo(
str(jtac.name),
n,
callsign,
frontline,
str(code),
blue=True,
)
)
def gen_infantry_group_for_group(

View File

@@ -0,0 +1,12 @@
from dcs.ships import FAC_La_Combattante_IIa
from game.factions.faction import Faction
from game.theater import TheaterGroundObject
from gen.fleet.dd_group import DDGroupGenerator
class LaCombattanteIIGroupGenerator(DDGroupGenerator):
def __init__(self, game, ground_object: TheaterGroundObject, faction: Faction):
super(LaCombattanteIIGroupGenerator, self).__init__(
game, ground_object, faction, FAC_La_Combattante_IIa
)

View File

@@ -8,6 +8,7 @@ from gen.fleet.dd_group import (
ArleighBurkeGroupGenerator,
OliverHazardPerryGroupGenerator,
)
from gen.fleet.lacombattanteII import LaCombattanteIIGroupGenerator
from gen.fleet.lha_group import LHAGroupGenerator
from gen.fleet.ru_dd_group import (
RussianNavyGroupGenerator,
@@ -34,6 +35,7 @@ SHIP_MAP = {
"KiloSubGroupGenerator": KiloSubGroupGenerator,
"TangoSubGroupGenerator": TangoSubGroupGenerator,
"Type54GroupGenerator": Type54GroupGenerator,
"LaCombattanteIIGroupGenerator": LaCombattanteIIGroupGenerator,
}

View File

@@ -132,7 +132,6 @@ CAP_CAPABLE = [
MiG_29A,
F_16C_50,
FA_18C_hornet,
F_15E,
F_16A,
F_4E,
JF_17,
@@ -140,6 +139,7 @@ CAP_CAPABLE = [
MiG_21Bis,
Mirage_2000_5,
M_2000C,
F_15E,
F_5E_3,
MiG_19P,
A_4E_C,

View File

@@ -79,6 +79,7 @@ class FlightWaypointType(Enum):
INGRESS_OCA_AIRCRAFT = 25
PICKUP = 26
DROP_OFF = 27
BULLSEYE = 28
class FlightWaypoint:

View File

@@ -426,6 +426,7 @@ class BarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
@@ -438,6 +439,7 @@ class BarCapFlightPlan(PatrollingFlightPlan):
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@dataclass(frozen=True)
@@ -446,6 +448,7 @@ class CasFlightPlan(PatrollingFlightPlan):
target: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
@@ -459,6 +462,7 @@ class CasFlightPlan(PatrollingFlightPlan):
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
def request_escort_at(self) -> Optional[FlightWaypoint]:
return self.patrol_start
@@ -472,6 +476,7 @@ class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
lead_time: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
@@ -485,6 +490,7 @@ class TarCapFlightPlan(PatrollingFlightPlan):
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def tot_offset(self) -> timedelta:
@@ -523,6 +529,7 @@ class StrikeFlightPlan(FormationFlightPlan):
nav_from: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
@@ -537,6 +544,7 @@ class StrikeFlightPlan(FormationFlightPlan):
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def package_speed_waypoints(self) -> Set[FlightWaypoint]:
@@ -641,6 +649,7 @@ class SweepFlightPlan(LoiterFlightPlan):
nav_from: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
lead_time: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
@@ -653,6 +662,7 @@ class SweepFlightPlan(LoiterFlightPlan):
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
@@ -704,6 +714,7 @@ class AwacsFlightPlan(LoiterFlightPlan):
nav_from: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
@@ -713,6 +724,7 @@ class AwacsFlightPlan(LoiterFlightPlan):
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def mission_start_time(self) -> Optional[timedelta]:
@@ -746,6 +758,7 @@ class AirliftFlightPlan(FlightPlan):
nav_to_home: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
@@ -758,6 +771,7 @@ class AirliftFlightPlan(FlightPlan):
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
@@ -1053,6 +1067,7 @@ class FlightPlanBuilder:
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
hold=start,
hold_duration=timedelta(hours=4),
)
@@ -1151,6 +1166,7 @@ class FlightPlanBuilder:
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
@@ -1187,6 +1203,7 @@ class FlightPlanBuilder:
sweep_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def generate_transport(self, flight: Flight) -> AirliftFlightPlan:
@@ -1238,6 +1255,7 @@ class FlightPlanBuilder:
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def racetrack_for_objective(
@@ -1389,6 +1407,7 @@ class FlightPlanBuilder:
patrol_end=end,
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def generate_dead(
@@ -1517,6 +1536,7 @@ class FlightPlanBuilder:
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def generate_cas(self, flight: Flight) -> CasFlightPlan:
@@ -1562,6 +1582,7 @@ class FlightPlanBuilder:
patrol_end=builder.egress(egress, location),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
@staticmethod
@@ -1696,6 +1717,7 @@ class FlightPlanBuilder:
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:

View File

@@ -35,6 +35,9 @@ class Loadout:
new_pylons = dict(self.pylons)
for pylon_number, weapon in self.pylons.items():
if weapon is None:
del new_pylons[pylon_number]
continue
if not weapon.available_on(date):
pylon = Pylon.for_aircraft(unit_type, pylon_number)
for fallback in weapon.fallbacks:
@@ -44,6 +47,8 @@ class Loadout:
continue
new_pylons[pylon_number] = fallback
break
else:
del new_pylons[pylon_number]
return Loadout(f"{self.name} ({date.year})", new_pylons, date)
@classmethod

View File

@@ -50,6 +50,7 @@ class WaypointBuilder:
self.threat_zones = game.threat_zone_for(not player)
self.navmesh = game.navmesh_for(player)
self.targets = targets
self._bullseye = game.bullseye_for(player)
@property
def is_helo(self) -> bool:
@@ -145,6 +146,19 @@ class WaypointBuilder:
waypoint.only_for_player = True
return waypoint
def bullseye(self) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.BULLSEYE,
self._bullseye.position.x,
self._bullseye.position.y,
meters(0),
)
waypoint.pretty_name = "Bullseye"
waypoint.description = "Bullseye"
waypoint.name = "BULLSEYE"
waypoint.only_for_player = True
return waypoint
def hold(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.LOITER,

View File

@@ -15,10 +15,12 @@ TYPE_TANKS = [
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_Chieftain_Mk_3,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_IV,
Armor.ZTZ_96B,
Armor.LT_PT_76,
# WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.Tk_PzIV_H,
@@ -45,6 +47,7 @@ TYPE_TANKS = [
TYPE_ATGM = [
Armor.ATGM_HMMWV,
Armor.ATGM_VAB_Mephisto,
Armor.ATGM_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
@@ -114,6 +117,7 @@ TYPE_ARTILLERY = [
Artillery.MLRS_M270_227mm,
Artillery.SPM_2S9_Nona_120mm_M,
Artillery.SPH_Dana_vz77_152mm,
Artillery.SPH_T155_Firtina_155mm,
Artillery.PLZ_05,
Artillery.SPH_2S19_Msta_152mm,
Artillery.MLRS_9A52_Smerch_CM_300mm,

View File

@@ -23,6 +23,7 @@ only be added per airframe, so PvP missions where each side have the same
aircraft will be able to see the enemy's kneeboard for the same airframe.
"""
import datetime
import textwrap
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
@@ -37,6 +38,7 @@ from tabulate import tabulate
from game.data.alic import AlicCodes
from game.db import find_unittype, unit_type_from_name
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
from game.theater.bullseye import Bullseye
from game.utils import meters
from .aircraft import AIRCRAFT_DATA, FlightData
from .airsupportgen import AwacsInfo, TankerInfo
@@ -257,21 +259,16 @@ class BriefingPage(KneeboardPage):
def __init__(
self,
flight: FlightData,
comms: List[CommInfo],
awacs: List[AwacsInfo],
tankers: List[TankerInfo],
jtacs: List[JtacInfo],
bullseye: Bullseye,
theater: ConflictTheater,
start_time: datetime.datetime,
dark_kneeboard: bool,
) -> None:
self.flight = flight
self.comms = list(comms)
self.awacs = awacs
self.tankers = tankers
self.jtacs = jtacs
self.bullseye = bullseye
self.theater = theater
self.start_time = start_time
self.dark_kneeboard = dark_kneeboard
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
@@ -301,6 +298,10 @@ class BriefingPage(KneeboardPage):
headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
)
writer.text(
f"Bullseye: {self.format_ll(self.bullseye.to_lat_lon(self.theater))}"
)
writer.table(
[
[
@@ -311,6 +312,86 @@ class BriefingPage(KneeboardPage):
["Bingo", "Joker"],
)
writer.write(path)
def airfield_info_row(
self, row_title: str, runway: Optional[RunwayData]
) -> List[str]:
"""Creates a table row for a given airfield.
Args:
row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or
"Divert".
runway: The runway described by this row.
Returns:
A list of strings to be used as a row of the airfield table.
"""
if runway is None:
return [row_title, "", "", "", "", ""]
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,
"\n".join(textwrap.wrap(runway.airfield_name, width=24)),
atc,
tacan,
ils,
runway.runway_name,
]
def format_frequency(self, frequency: RadioFrequency) -> str:
channel = self.flight.channel_for(frequency)
if channel is None:
return str(frequency)
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
channel_name = namer.channel_name(channel.radio_id, channel.channel)
return f"{channel_name}\n{frequency}"
class SupportPage(KneeboardPage):
"""A kneeboard page containing information about support units."""
def __init__(
self,
flight: FlightData,
comms: List[CommInfo],
awacs: List[AwacsInfo],
tankers: List[TankerInfo],
jtacs: List[JtacInfo],
start_time: datetime.datetime,
dark_kneeboard: bool,
) -> None:
self.flight = flight
self.comms = list(comms)
self.awacs = awacs
self.tankers = tankers
self.jtacs = jtacs
self.start_time = start_time
self.dark_kneeboard = dark_kneeboard
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
if self.flight.custom_name is not None:
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
else:
custom_name_title = ""
writer.title(f"{self.flight.callsign} Support Info{custom_name_title}")
# AEW&C
writer.heading("AEW&C")
aewc_ladder = []
@@ -368,44 +449,6 @@ class BriefingPage(KneeboardPage):
writer.write(path)
def airfield_info_row(
self, row_title: str, runway: Optional[RunwayData]
) -> List[str]:
"""Creates a table row for a given airfield.
Args:
row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or
"Divert".
runway: The runway described by this row.
Returns:
A list of strings to be used as a row of the airfield table.
"""
if runway is None:
return [row_title, "", "", "", "", ""]
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,
tacan,
ils,
runway.runway_name,
]
def format_frequency(self, frequency: RadioFrequency) -> str:
channel = self.flight.channel_for(frequency)
if channel is None:
@@ -558,6 +601,13 @@ class KneeboardGenerator(MissionInfoGenerator):
"""Returns a list of kneeboard pages for the given flight."""
pages: List[KneeboardPage] = [
BriefingPage(
flight,
self.game.bullseye_for(flight.friendly),
self.game.theater,
self.mission.start_time,
self.dark_kneeboard,
),
SupportPage(
flight,
self.comms,
self.awacs,

View File

@@ -209,16 +209,6 @@ class TriggersGenerator:
player_coalition = "blue"
enemy_coalition = "red"
player_cp, enemy_cp = self.game.theater.closest_opposing_control_points()
self.mission.coalition["blue"].bullseye = {
"x": enemy_cp.position.x,
"y": enemy_cp.position.y,
}
self.mission.coalition["red"].bullseye = {
"x": player_cp.position.x,
"y": player_cp.position.y,
}
self._set_skill(player_coalition, enemy_coalition)
self._set_allegiances(player_coalition, enemy_coalition)
self._gen_markers()