Remove LGBs from degraded loadouts without TGPs.

This only takes effect for default loadouts. Custom loadouts set from
the UI will allow LGBs. In the default case there will not be buddy-lase
coordination so we should take iron bombs instead.

Also adds single/double Mk 83 and Mk 82 weapon data to accomodate this.
This commit is contained in:
Dan Albert 2021-07-16 17:43:37 -07:00
parent 11c2d4ab25
commit 8e977f994f
17 changed files with 162 additions and 21 deletions

View File

@ -5,6 +5,7 @@ Saves from 3.x are not compatible with 5.0.
## Features/Improvements
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
* **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts.
* **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions.
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
* **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points.

View File

@ -4,6 +4,7 @@ import datetime
import inspect
import logging
from dataclasses import dataclass, field
from enum import unique, Enum
from functools import cached_property
from pathlib import Path
from typing import Iterator, Optional, Any, ClassVar
@ -61,7 +62,7 @@ class Weapon:
duplicate = cls._by_clsid[weapon.clsid]
raise ValueError(
"Weapon CLSID used in more than one weapon type: "
f"{duplicate.name} and {weapon.name}"
f"{duplicate.name} and {weapon.name}: {weapon.clsid}"
)
cls._by_clsid[weapon.clsid] = weapon
@ -91,6 +92,13 @@ class Weapon:
fallback = fallback.fallback
@unique
class WeaponType(Enum):
LGB = "LGB"
TGP = "TGP"
UNKNOWN = "unknown"
@dataclass(frozen=True)
class WeaponGroup:
"""Group of "identical" weapons loaded from resources/weapons.
@ -101,7 +109,10 @@ class WeaponGroup:
"""
#: The name of the weapon group in the resource file.
name: str = field(compare=False)
name: str
#: The type of the weapon group.
type: WeaponType = field(compare=False)
#: The year of introduction.
introduction_year: Optional[int] = field(compare=False)
@ -152,9 +163,13 @@ class WeaponGroup:
with group_file_path.open(encoding="utf8") as group_file:
data = yaml.safe_load(group_file)
name = data["name"]
try:
weapon_type = WeaponType(data["type"])
except KeyError:
weapon_type = WeaponType.UNKNOWN
year = data.get("year")
fallback_name = data.get("fallback")
group = WeaponGroup(name, year, fallback_name)
group = WeaponGroup(name, weapon_type, year, fallback_name)
for clsid in data["clsids"]:
weapon = Weapon(clsid, group)
Weapon.register(weapon)
@ -163,7 +178,12 @@ class WeaponGroup:
@classmethod
def register_clean_pylon(cls) -> None:
group = WeaponGroup("Clean pylon", introduction_year=None, fallback_name=None)
group = WeaponGroup(
"Clean pylon",
type=WeaponType.UNKNOWN,
introduction_year=None,
fallback_name=None,
)
cls.register(group)
weapon = Weapon("<CLEAN>", group)
Weapon.register(weapon)
@ -172,7 +192,12 @@ class WeaponGroup:
@classmethod
def register_unknown_weapons(cls, seen_clsids: set[str]) -> None:
unknown_weapons = set(weapon_ids.keys()) - seen_clsids
group = WeaponGroup("Unknown", introduction_year=None, fallback_name=None)
group = WeaponGroup(
"Unknown",
type=WeaponType.UNKNOWN,
introduction_year=None,
fallback_name=None,
)
cls.register(group)
for clsid in unknown_weapons:
weapon = Weapon(clsid, group)

View File

@ -1,9 +1,10 @@
from __future__ import annotations
import datetime
from typing import Optional, List, Iterator, TYPE_CHECKING, Mapping
from collections import Iterable
from typing import Optional, Iterator, TYPE_CHECKING, Mapping
from game.data.weapons import Weapon, Pylon
from game.data.weapons import Weapon, Pylon, WeaponType
from game.dcs.aircrafttype import AircraftType
if TYPE_CHECKING:
@ -30,9 +31,28 @@ class Loadout:
def derive_custom(self, name: str) -> Loadout:
return Loadout(name, self.pylons, self.date, is_custom=True)
@staticmethod
def _fallback_for(
weapon: Weapon,
pylon: Pylon,
date: datetime.date,
skip_types: Optional[Iterable[WeaponType]] = None,
) -> Optional[Weapon]:
if skip_types is None:
skip_types = set()
for fallback in weapon.fallbacks:
if not pylon.can_equip(fallback):
continue
if not fallback.available_on(date):
continue
if fallback.weapon_group.type in skip_types:
continue
return fallback
return None
def degrade_for_date(self, unit_type: AircraftType, date: datetime.date) -> Loadout:
if self.date is not None and self.date <= date:
return Loadout(self.name, self.pylons, self.date)
return Loadout(self.name, self.pylons, self.date, self.is_custom)
new_pylons = dict(self.pylons)
for pylon_number, weapon in self.pylons.items():
@ -41,16 +61,41 @@ class Loadout:
continue
if not weapon.available_on(date):
pylon = Pylon.for_aircraft(unit_type, pylon_number)
for fallback in weapon.fallbacks:
if not pylon.can_equip(fallback):
continue
if not fallback.available_on(date):
continue
new_pylons[pylon_number] = fallback
break
else:
fallback = self._fallback_for(weapon, pylon, date)
if fallback is None:
del new_pylons[pylon_number]
return Loadout(f"{self.name} ({date.year})", new_pylons, date)
else:
new_pylons[pylon_number] = fallback
loadout = Loadout(self.name, new_pylons, date, self.is_custom)
# If this is not a custom loadout, we should replace any LGBs with iron bombs if
# the loadout lost its TGP.
#
# If the loadout was chosen explicitly by the user, assume they know what
# they're doing. They may be coordinating buddy-lase.
if not loadout.is_custom:
loadout.replace_lgbs_if_no_tgp(unit_type, date)
return loadout
def replace_lgbs_if_no_tgp(
self, unit_type: AircraftType, date: datetime.date
) -> None:
for weapon in self.pylons.values():
if weapon is not None and weapon.weapon_group.type is WeaponType.TGP:
# Have a TGP. Nothing to do.
return
new_pylons = dict(self.pylons)
for pylon_number, weapon in self.pylons.items():
if weapon is not None and weapon.weapon_group.type is WeaponType.LGB:
pylon = Pylon.for_aircraft(unit_type, pylon_number)
fallback = self._fallback_for(
weapon, pylon, date, skip_types={WeaponType.LGB}
)
if fallback is None:
del new_pylons[pylon_number]
else:
new_pylons[pylon_number] = fallback
self.pylons = new_pylons
@classmethod
def iter_for(cls, flight: Flight) -> Iterator[Loadout]:
@ -72,10 +117,6 @@ class Loadout:
date=None,
)
@classmethod
def all_for(cls, flight: Flight) -> List[Loadout]:
return list(cls.iter_for(flight))
@classmethod
def default_loadout_names_for(cls, flight: Flight) -> Iterator[str]:
from gen.flights.flight import FlightType

View File

@ -1,4 +1,6 @@
name: 2xGBU-10
type: LGB
year: 1976
fallback: 2xMk 84
clsids:
- "{62BE78B1-9258-48AE-B882-279534C0D278}"

View File

@ -1,5 +1,7 @@
name: GBU-10
type: LGB
year: 1976
fallback: Mk 84
clsids:
- "DIS_GBU_10"
- "{BRU-32 GBU-10}"

View File

@ -1,5 +1,7 @@
name: 2xGBU-12
type: LGB
year: 1976
fallback: 2xMk 82
clsids:
- "{M2KC_RAFAUT_GBU12}"
- "{BRU33_2X_GBU-12}"

View File

@ -1,5 +1,7 @@
name: GBU-12
type: LGB
year: 1976
fallback: Mk 82
clsids:
- "DIS_GBU_12"
- "{BRU-32 GBU-12}"

View File

@ -1,5 +1,7 @@
name: 2xGBU-16
type: LGB
year: 1976
fallback: 2xMk 83
clsids:
- "{BRU33_2X_GBU-16}"
- "{BRU-42_2*GBU-16_LEFT}"

View File

@ -1,5 +1,7 @@
name: GBU-16
type: LGB
year: 1976
fallback: Mk 83
clsids:
- "DIS_GBU_16"
- "{BRU-32 GBU-16}"

View File

@ -1,4 +1,5 @@
name: GBU-24
type: LGB
year: 1986
fallback: GBU-10
clsids:

View File

@ -0,0 +1,18 @@
name: 2xMk 82
fallback: Mk 82
clsids:
- "{M2KC_RAFAUT_MK82}"
- "{BRU33_2X_MK-82}"
- "DIS_MK_82_DUAL_GDJ_II19_L"
- "DIS_MK_82_DUAL_GDJ_II19_R"
- "{D5D51E24-348C-4702-96AF-97A714E72697}"
- "{TER_9A_2L*MK-82}"
- "{TER_9A_2R*MK-82}"
- "{BRU-42_2*Mk-82_LEFT}"
- "{BRU-42_2*Mk-82_RIGHT}"
- "{BRU42_2*MK82 RS}"
- "{BRU3242_2*MK82 RS}"
- "{PHXBRU3242_2*MK82 RS}"
- "{BRU42_2*MK82 LS}"
- "{BRU3242_2*MK82 LS}"
- "{PHXBRU3242_2*MK82 LS}"

View File

@ -0,0 +1,11 @@
name: Mk 82
clsids:
- "{BRU-32 MK-82}"
- "{Mk_82B}"
- "{Mk_82BT}"
- "{Mk_82P}"
- "{Mk_82PT}"
- "{Mk_82SB}"
- "{Mk_82SP}"
- "{Mk_82YT}"
- "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}"

View File

@ -0,0 +1,7 @@
name: 2xMk 83
fallback: Mk 83
clsids:
- "{BRU33_2X_MK-83}"
- "{18617C93-78E7-4359-A8CE-D754103EDF63}"
- "{BRU-42_2*Mk-83_LEFT}"
- "{BRU-42_2*Mk-83_RIGHT}"

View File

@ -0,0 +1,16 @@
name: Mk 83
clsids:
- "{MAK79_MK83 1R}"
- "{MAK79_MK83 1L}"
- "{BRU-32 MK-83}"
- "{Mk_83BT}"
- "{Mk_83CT}"
- "{Mk_83P}"
- "{Mk_83PT}"
- "{BRU42_MK83 RS}"
- "{BRU3242_MK83 RS}"
- "{PHXBRU3242_MK83 RS}"
- "{7A44FF09-527C-4B7E-B42B-3F111CFE50FB}"
- "{BRU42_MK83 LS}"
- "{BRU3242_MK83 LS}"
- "{PHXBRU3242_MK83 LS}"

View File

@ -1,4 +1,5 @@
name: AN/ASQ-228 ATFLIR
type: TGP
year: 2003
# A bit of a hack, but fixes the common case where the Hornet cheek station is
# empty because no TGP is available.

View File

@ -0,0 +1,7 @@
name: AN/AAQ-14 LANTIRN
type: TGP
year: 1990
clsids:
- "{F14-LANTIRN-TP}"
- "{CAAC1CFD-6745-416B-AFA4-CB57414856D0}"
- "{D1744B93-2A8A-4C4D-B004-7A09CD8C8F3F}"

View File

@ -1,4 +1,5 @@
name: AN/AAQ-28 LITENING
type: TGP
year: 1999
# A bit of a hack, but fixes the common case where the Hornet cheek station is
# empty because no TGP is available. For the Viper this will have no effect