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
commit f31861441b
55 changed files with 345 additions and 132 deletions

View File

@ -9,6 +9,8 @@ assignees: ''
Before filing, please search the issue tracker to see if the issue has already been reported.
If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
**Describe the bug**
A clear and concise description of what the bug is.

View File

@ -9,6 +9,8 @@ assignees: ''
Before filing, please search the issue tracker to see if this feature has already been requested.
If requesting a DCS AI feature, check If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@ -22,6 +22,14 @@ Latest release is available here : https://github.com/dcs-liberation/dcs_liberat
To download preview builds of the next version of DCS Liberation, see https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds.
## DCS bugs
These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
_don't_ spam them with comments):
* [A2A and SEAD escorts don't escort](https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior/?tab=comments#comment-4668033)
* [DEAD can't use mixed loadouts effectively](https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/)
## Bugs and feature requests
If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/dcs-liberation/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed.

View File

@ -7,10 +7,13 @@ Saves from 2.5 are not compatible with 3.0.
* **[Campaign]** Ground units can now be transferred by road, airlift, and cargo ship. See https://github.com/dcs-liberation/dcs_liberation/wiki/Unit-Transfers for more information.
* **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them.
* **[Campaign]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn.
* **[Campaign]** Non-control point FOBs will no longer spawn.
* **[Campaign AI]** Every 30 minutes the AI will plan a CAP, so players can customize their mission better.
* **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions.
* **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's).
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups.
* **[Flight Planner]** Flight plans now include bullseye waypoints.
* **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows.
* **[UI]** Added new web based map UI. This is mostly functional but many of the old display options are a WIP. Revert to the old map with --old-map.
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.

View File

@ -8,7 +8,6 @@ DEFAULT_AVAILABLE_BUILDINGS = [
"oil",
"ware",
"farp",
"fob",
"power",
"derrick",
]
@ -21,7 +20,6 @@ WW2_GERMANY_BUILDINGS = [
"ww2bunker",
"allycamp",
"allycamp",
"fob",
]
WW2_ALLIES_BUILDINGS = [
"fuel",
@ -30,7 +28,6 @@ WW2_ALLIES_BUILDINGS = [
"allycamp",
"allycamp",
"allycamp",
"fob",
]
FORTIFICATION_BUILDINGS = [

View File

@ -145,20 +145,26 @@ _WEAPON_FALLBACKS = [
Weapons.LAU_117_with_AGM_65E___Maverick_E__Laser_ASM___Lg_Whd_,
), # internal pylons harrier
# AGM-154 JSOW
(Weapons.AGM_154A___JSOW_CEB__CBU_type_, Weapons.GBU_12),
(
Weapons.AGM_154A___JSOW_CEB__CBU_type_,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
),
(
Weapons.BRU_55_with_2_x_AGM_154A___JSOW_CEB__CBU_type_,
Weapons.BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
),
(
Weapons.BRU_57_with_2_x_AGM_154A___JSOW_CEB__CBU_type_,
None,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
), # doesn't exist on any aircraft yet
(Weapons.AGM_154B___JSOW_Anti_Armour, Weapons.CBU_105___10_x_SFW__CBU_with_WCMD),
(Weapons.AGM_154C___JSOW_Unitary_BROACH, Weapons.GBU_12),
(
Weapons.AGM_154C___JSOW_Unitary_BROACH,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
),
(
Weapons.BRU_55_with_2_x_AGM_154C___JSOW_Unitary_BROACH,
Weapons.BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
),
# AGM-45 Shrike
(Weapons.AGM_45A_Shrike_ARM, None),

View File

@ -498,12 +498,14 @@ PRICES = {
Armor.IFV_BMP_1: 14,
Armor.IFV_BMP_2: 16,
Armor.IFV_BMP_3: 18,
Armor.LT_PT_76: 9,
Armor.ZBD_04A: 12,
Armor.ZTZ_96B: 30,
Armor.Scout_Cobra: 4,
Armor.APC_M113: 6,
Armor.Scout_HMMWV: 2,
Armor.ATGM_HMMWV: 8,
Armor.ATGM_VAB_Mephisto: 12,
Armor.IFV_M2A2_Bradley: 12,
Armor.IFV_M1126_Stryker_ICV: 10,
Armor.SPG_Stryker_MGS: 14,
@ -519,6 +521,7 @@ PRICES = {
Armor.MBT_Merkava_IV: 25,
Armor.APC_TPz_Fuchs: 5,
Armor.MBT_Challenger_II: 25,
Armor.MBT_Chieftain_Mk_3: 20,
Armor.IFV_Marder: 10,
Armor.IFV_Warrior: 10,
Armor.IFV_LAV_25: 7,
@ -534,6 +537,7 @@ PRICES = {
Artillery.Mortar_2B11_120mm: 4,
Artillery.SPH_Dana_vz77_152mm: 26,
Artillery.PLZ_05: 25,
Artillery.SPH_T155_Firtina_155mm: 28,
Unarmed.LUV_UAZ_469_Jeep: 3,
Unarmed.Truck_Ural_375: 3,
Infantry.Infantry_M4: 1,
@ -885,6 +889,7 @@ UNIT_BY_TASK = {
Armor.IFV_BMP_3,
Armor.IFV_BMP_3,
Armor.IFV_BMD_1,
Armor.LT_PT_76,
Armor.ZBD_04A,
Armor.ZBD_04A,
Armor.ZBD_04A,
@ -913,6 +918,8 @@ UNIT_BY_TASK = {
Armor.APC_TPz_Fuchs,
Armor.ATGM_HMMWV,
Armor.ATGM_HMMWV,
Armor.ATGM_VAB_Mephisto,
Armor.ATGM_VAB_Mephisto,
Armor.Scout_HMMWV,
Armor.Scout_HMMWV,
Armor.IFV_M2A2_Bradley,
@ -941,6 +948,7 @@ UNIT_BY_TASK = {
Armor.MBT_Leclerc,
Armor.MBT_Leopard_2A6M,
Armor.MBT_Challenger_II,
Armor.MBT_Chieftain_Mk_3,
Armor.MBT_Merkava_IV,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.Tk_PzIV_H,
@ -1002,6 +1010,7 @@ UNIT_BY_TASK = {
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
Artillery.MLRS_9A52_Smerch_HE_300mm,
Artillery.SPH_Dana_vz77_152mm,
Artillery.SPH_T155_Firtina_155mm,
Artillery.PLZ_05,
Artillery.SPG_M12_GMC_155mm,
Armor.SPG_Sturmpanzer_IV_Brummbar,
@ -1241,7 +1250,6 @@ REWARDS = {
"fuel": 2,
"ammo": 2,
"farp": 1,
"fob": 1,
# TODO: Should generate no cash once they generate units.
"factory": 10,
"comms": 10,

View File

@ -34,6 +34,7 @@ from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration
from .settings import Settings
from .theater import ConflictTheater
from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .threatzones import ThreatZones
from .transfers import PendingTransfers
@ -130,6 +131,9 @@ class Game:
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
self.blue_bullseye = Bullseye(Point(0, 0))
self.red_bullseye = Bullseye(Point(0, 0))
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.transfers = PendingTransfers(self)
@ -201,6 +205,11 @@ class Game:
return self.player_faction
return self.enemy_faction
def bullseye_for(self, player: bool) -> Bullseye:
if player:
return self.blue_bullseye
return self.red_bullseye
def _roll(self, prob, mult):
if self.settings.version == "dev":
# always generate all events for dev
@ -337,10 +346,17 @@ class Game:
return TurnState.CONTINUE
def set_bullseye(self) -> None:
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
self.blue_bullseye = Bullseye(enemy_cp.position)
self.red_bullseye = Bullseye(player_cp.position)
def initialize_turn(self) -> None:
self.events = []
self._generate_events()
self.set_bullseye()
# Update statistics
self.game_stats.update(self)

View File

@ -108,8 +108,12 @@ class Operation:
@classmethod
def _setup_mission_coalitions(cls):
cls.current_mission.coalition["blue"] = Coalition("blue")
cls.current_mission.coalition["red"] = Coalition("red")
cls.current_mission.coalition["blue"] = Coalition(
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
)
cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red_bullseye.to_pydcs()
)
p_country = cls.game.player_country
e_country = cls.game.enemy_country
@ -171,13 +175,16 @@ class Operation:
gen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers:
gen.add_tanker(tanker)
if tanker.blue:
gen.add_tanker(tanker)
for aewc in airsupportgen.air_support.awacs:
gen.add_awacs(aewc)
if aewc.blue:
gen.add_awacs(aewc)
for jtac in jtacs:
gen.add_jtac(jtac)
if jtac.blue:
gen.add_jtac(jtac)
for flight in airgen.flights:
gen.add_flight(flight)
@ -317,13 +324,8 @@ class Operation:
# Setup combined arms parameters
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
if cls.game.player_country in [
country.name
for country in cls.current_mission.coalition["blue"].countries.values()
]:
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
else:
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
cls.current_mission.groundControl.blue_observer = 1
# Options
forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
@ -453,7 +455,7 @@ class Operation:
for tanker in airsupportgen.air_support.tankers:
luaData["Tankers"][tanker.callsign] = {
"dcsGroupName": tanker.dcsGroupName,
"dcsGroupName": tanker.group_name,
"callsign": tanker.callsign,
"variant": tanker.variant,
"radio": tanker.freq.mhz,
@ -463,14 +465,14 @@ class Operation:
if airsupportgen.air_support.awacs:
for awacs in airsupportgen.air_support.awacs:
luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.dcsGroupName,
"dcsGroupName": awacs.group_name,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz,
}
for jtac in jtacs:
luaData["JTACs"][jtac.callsign] = {
"dcsGroupName": jtac.dcsGroupName,
"dcsGroupName": jtac.group_name,
"callsign": jtac.callsign,
"zone": jtac.region,
"dcsUnit": jtac.unit_name,

26
game/theater/bullseye.py Normal file
View File

@ -0,0 +1,26 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, TYPE_CHECKING
from dcs import Point
from game.theater import LatLon
if TYPE_CHECKING:
from game.theater import ConflictTheater
@dataclass
class Bullseye:
position: Point
@classmethod
def from_pydcs(cls, bulls: Dict[str, float]) -> Bullseye:
return cls(Point(bulls["x"], bulls["y"]))
def to_pydcs(self) -> Dict[str, float]:
return {"x": self.position.x, "y": self.position.y}
def to_lat_lon(self, theater: ConflictTheater) -> LatLon:
return theater.point_to_ll(self.position)

View File

@ -41,6 +41,7 @@ from dcs.unitgroup import (
)
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from .latlon import LatLon
from ..helipad import Helipad
from ..scenery_group import SceneryGroup
from pyproj import CRS, Transformer
@ -583,15 +584,6 @@ class ReferencePoint:
image_coordinates: Point
@dataclass(frozen=True)
class LatLon:
latitude: float
longitude: float
def as_list(self) -> List[float]:
return [self.latitude, self.longitude]
class ConflictTheater:
terrain: Terrain

11
game/theater/latlon.py Normal file
View File

@ -0,0 +1,11 @@
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class LatLon:
latitude: float
longitude: float
def as_list(self) -> List[float]:
return [self.latitude, self.longitude]

View File

@ -845,12 +845,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
return True
def generate_fob(self) -> None:
try:
category = self.faction.building_set[self.faction.building_set.index("fob")]
except IndexError:
logging.exception("Faction has no fob buildings defined")
return
category = "fob"
obj_name = self.control_point.name
template = random.choice(list(self.templates[category].values()))
point = self.control_point.position

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()

2
pydcs

@ -1 +1 @@
Subproject commit dec648d27f74c394dd6e85e83cc09e4cd823653d
Subproject commit 4972988c978f2057e7aa06919c4de71ee9a06ea5

View File

@ -375,6 +375,7 @@ class WaypointJs(QObject):
timingChanged = Signal()
isTakeoffChanged = Signal()
isDivertChanged = Signal()
isBullseyeChanged = Signal()
def __init__(
self,
@ -439,6 +440,10 @@ class WaypointJs(QObject):
def isDivert(self) -> bool:
return self.waypoint.waypoint_type is FlightWaypointType.DIVERT
@Property(bool, notify=isBullseyeChanged)
def isBullseye(self) -> bool:
return self.waypoint.waypoint_type is FlightWaypointType.BULLSEYE
@Slot(list, result=str)
def setPosition(self, position: LeafletLatLon) -> str:
point = self.theater.ll_to_point(LatLon(*position))

View File

@ -47,7 +47,8 @@
"MBT_Challenger_II",
"MBT_M60A3_Patton",
"SPG_Stryker_MGS",
"SAM_Avenger__Stinger"
"SAM_Avenger__Stinger",
"ATGM_VAB_Mephisto"
],
"artillery_units": [
"MLRS_M270_227mm",

View File

@ -46,7 +46,8 @@
"Scout_HMMWV",
"ATGM_HMMWV",
"SAM_Linebacker___Bradley_M6",
"SAM_Avenger__Stinger"
"SAM_Avenger__Stinger",
"ATGM_VAB_Mephisto"
],
"artillery_units": [
"MLRS_M270_227mm",

View File

@ -23,13 +23,10 @@
"ERC_90",
"TRM_2000_PAMELA",
"VAB__50",
"VAB_MEPHISTO",
"VAB_T20_13",
"ATGM_VAB_Mephisto",
"VAB_T20_13",
"VBL__50",
"VBL_AANF1",
"VBAE_CRAB",
"VBAE_CRAB_MMP",
"AMX_30B2",
"SAM_Roland_ADS"
],

View File

@ -21,10 +21,8 @@
"MBT_Leclerc",
"APC_TPz_Fuchs",
"Scout_Cobra",
"ATGM_Stryker",
"IFV_LAV_25",
"Scout_HMMWV",
"ATGM_HMMWV",
"ATGM_VAB_Mephisto",
"SAM_Roland_ADS"
],
"artillery_units": [

View File

@ -24,7 +24,6 @@
"ERC_90",
"TRM_2000_PAMELA",
"VAB__50",
"VAB_MEPHISTO",
"VAB_T20_13",
"VAB_T20_13",
"VBL__50",
@ -33,7 +32,8 @@
"VBAE_CRAB_MMP",
"AMX_30B2",
"Leclerc_Serie_XXI",
"SAM_Roland_ADS"
"SAM_Roland_ADS",
"ATGM_VAB_Mephisto"
],
"artillery_units": [
"MLRS_M270_227mm",

View File

@ -45,6 +45,9 @@
"ZU23Generator",
"ZU23UralGenerator"
],
"navy_generators": [
"LaCombattanteIIGroupGenerator"
],
"requirements": {},
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"

View File

@ -61,7 +61,8 @@
"helicopter_carrier_names": [
],
"navy_generators": [
"OliverHazardPerryGroupGenerator"
"OliverHazardPerryGroupGenerator",
"LaCombattanteIIGroupGenerator"
],
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"

View File

@ -46,6 +46,9 @@
"HawkEwrGenerator",
"FlatFaceGenerator"
],
"navy_generators": [
"LaCombattanteIIGroupGenerator"
],
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"
}

View File

@ -9,6 +9,7 @@
"Scout_Cobra",
"APC_MTLB",
"Scout_BRDM_2",
"LT_PT_76",
"SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375"
],
"artillery_units": [

View File

@ -11,6 +11,7 @@
"Scout_BRDM_2",
"APC_BTR_80",
"APC_BTR_RD",
"LT_PT_76",
"IFV_BMP_1",
"MBT_T_55",
"SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375",

View File

@ -20,6 +20,7 @@
"APC_M113",
"APC_BTR_80",
"MBT_M60A3_Patton",
"MBT_Chieftain_Mk_3",
"IFV_BMP_1",
"SPAAA_ZSU_23_4_Shilka_Gun_Dish",
"SPAAA_ZSU_57_2",
@ -75,7 +76,7 @@
],
"coastal_group_count": 2,
"navy_generators": [
"GrishaGroupGenerator",
"LaCombattanteIIGroupGenerator",
"MolniyaGroupGenerator"
],
"has_jtac": true,

View File

@ -26,6 +26,7 @@
"APC_M113",
"APC_BTR_80",
"MBT_M60A3_Patton",
"MBT_Chieftain_Mk_3",
"IFV_BMP_1",
"MBT_T_72B",
"SPAAA_ZSU_23_4_Shilka_Gun_Dish",
@ -85,7 +86,8 @@
"coastal_group_count": 3,
"navy_generators": [
"GrishaGroupGenerator",
"MolniyaGroupGenerator"
"MolniyaGroupGenerator",
"LaCombattanteIIGroupGenerator"
],
"has_jtac": true,
"jtac_unit": "MQ_9_Reaper"

View File

@ -29,8 +29,10 @@
"APC_MTLB",
"MBT_T_55",
"MBT_T_72B",
"MBT_Chieftain_Mk_3",
"APC_BTR_80",
"Scout_BRDM_2",
"LT_PT_76",
"SPH_2S1_Gvozdika_122mm",
"SPAAA_ZSU_57_2",
"SPAAA_ZSU_23_4_Shilka_Gun_Dish"

View File

@ -21,6 +21,7 @@
"Scout_BRDM_2",
"MBT_T_72B",
"MBT_T_55",
"LT_PT_76",
"SPAAA_ZSU_23_4_Shilka_Gun_Dish",
"SAM_SA_8_Osa_Gecko_TEL"
],
@ -71,6 +72,6 @@
"carrier_names": [
],
"navy_generators": [
"GrishaGroupGenerator", "MolniyaGroupGenerator"
"GrishaGroupGenerator", "MolniyaGroupGenerator", "LaCombattanteIIGroupGenerator"
]
}

View File

@ -25,6 +25,7 @@
"MBT_T_55",
"MBT_T_72B",
"MBT_T_80U",
"LT_PT_76",
"SPAAA_ZSU_57_2",
"SAM_SA_9_Strela_1_Gaskin_TEL"
],

View File

@ -17,6 +17,7 @@
"Grad_MRL_FDDM__FC",
"APC_MTLB",
"MBT_T_55",
"LT_PT_76",
"SPAAA_ZU_23_2_Mounted_Ural_375",
"AAA_8_8cm_Flak_18",
"AAA_S_60_57mm"

View File

@ -22,6 +22,7 @@
"APC_BTR_RD",
"IFV_BMD_1",
"IFV_BMP_1",
"LT_PT_76",
"MBT_T_55",
"SPAAA_ZU_23_2_Mounted_Ural_375",
"SPAAA_ZSU_57_2",

View File

@ -14,6 +14,7 @@
"APC_BTR_80",
"IFV_BMD_1",
"IFV_BMP_1",
"LT_PT_76",
"MBT_T_55",
"SPAAA_ZSU_57_2"
],

View File

@ -27,6 +27,7 @@
"APC_BTR_80",
"IFV_BMD_1",
"IFV_BMP_1",
"LT_PT_76",
"MBT_T_55",
"SAM_SA_8_Osa_Gecko_TEL"
],

View File

@ -20,6 +20,7 @@
"Scout_BRDM_2",
"Tk_PzIV_H",
"MBT_T_55",
"LT_PT_76",
"SPAAA_ZU_23_2_Mounted_Ural_375",
"SPAAA_ZSU_57_2",
"AAA_S_60_57mm"

View File

@ -20,6 +20,7 @@
"frontline_units": [
"Scout_BRDM_2",
"MBT_T_55",
"LT_PT_76",
"Tk_PzIV_H",
"SPG_StuG_III_Ausf__G",
"SPG_Jagdpanzer_IV",

View File

@ -20,6 +20,7 @@
"IFV_BMP_1",
"APC_MTLB",
"MBT_T_55",
"LT_PT_76",
"SPAAA_ZU_23_2_Mounted_Ural_375",
"SPAAA_ZSU_57_2",
"AAA_S_60_57mm"

View File

@ -21,6 +21,7 @@
"IFV_BMP_1",
"APC_MTLB",
"MBT_T_55",
"LT_PT_76",
"MBT_T_72B",
"SPAAA_ZU_23_2_Mounted_Ural_375",
"SPAAA_ZSU_57_2",

View File

@ -27,6 +27,7 @@
"IFV_BMP_2",
"APC_BTR_80",
"Scout_BRDM_2",
"LT_PT_76",
"APC_MTLB",
"Scout_Cobra",
"MBT_T_55",

View File

@ -25,7 +25,7 @@
"SAM_Avenger__Stinger"
],
"artillery_units": [
"SPH_M109_Paladin_155mm"
"SPH_T155_Firtina_155mm"
],
"logistics_units": [
"Truck_M818_6x6"

View File

@ -19,6 +19,7 @@
],
"frontline_units": [
"MBT_Challenger_II",
"MBT_Chieftain_Mk_3",
"IFV_Warrior",
"Scout_HMMWV",
"ATGM_HMMWV",

View File

@ -570,7 +570,7 @@ class Waypoint {
}
includeInPath() {
return !this.waypoint.isDivert;
return !this.waypoint.isDivert && !this.waypoint.isBullseye;
}
}