mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
This issue addresses Issue #3204 by allowing factions to set a different weapon introduction date.
293 lines
11 KiB
Python
293 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Any, Optional, TYPE_CHECKING
|
|
|
|
from dcs import Mission, Point
|
|
from dcs.flyingunit import FlyingUnit
|
|
from dcs.unit import Skill
|
|
from dcs.unitgroup import FlyingGroup
|
|
|
|
from game.ato import Flight, FlightType
|
|
from game.callsigns.callsign import callsign_for_support_unit
|
|
from game.callsigns.callsigngenerator import Callsign, FlightCallsignGenerator
|
|
from game.data.weapons import Pylon
|
|
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
|
|
from game.missiongenerator.missiondata import AwacsInfo, MissionData, TankerInfo
|
|
from game.radio.radios import RadioFrequency, RadioRegistry
|
|
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
|
|
from game.runways import RunwayData
|
|
from game.squadrons import Pilot
|
|
from game.unitmap import UnitMap
|
|
from .aircraftbehavior import AircraftBehavior
|
|
from .aircraftpainter import AircraftPainter
|
|
from .bingoestimator import BingoEstimator
|
|
from .flightdata import FlightData
|
|
from .waypoints import WaypointGenerator
|
|
from ...ato.flightmember import FlightMember
|
|
|
|
if TYPE_CHECKING:
|
|
from game import Game
|
|
|
|
|
|
class FlightGroupConfigurator:
|
|
def __init__(
|
|
self,
|
|
flight: Flight,
|
|
group: FlyingGroup[Any],
|
|
game: Game,
|
|
mission: Mission,
|
|
time: datetime,
|
|
radio_registry: RadioRegistry,
|
|
tacan_registry: TacanRegistry,
|
|
mission_data: MissionData,
|
|
dynamic_runways: dict[str, RunwayData],
|
|
use_client: bool,
|
|
unit_map: UnitMap,
|
|
) -> None:
|
|
self.flight = flight
|
|
self.group = group
|
|
self.game = game
|
|
self.mission = mission
|
|
self.time = time
|
|
self.radio_registry = radio_registry
|
|
self.tacan_registry = tacan_registry
|
|
self.mission_data = mission_data
|
|
self.dynamic_runways = dynamic_runways
|
|
self.use_client = use_client
|
|
self.unit_map = unit_map
|
|
|
|
def configure(self) -> FlightData:
|
|
AircraftBehavior(self.flight.flight_type, self.game.settings).apply_to(
|
|
self.flight, self.group
|
|
)
|
|
AircraftPainter(self.flight, self.group).apply_livery()
|
|
self.setup_props()
|
|
self.setup_payloads()
|
|
self.setup_fuel()
|
|
flight_channel = self.setup_radios()
|
|
|
|
laser_codes: list[Optional[int]] = []
|
|
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
|
self.configure_flight_member(unit, member, laser_codes)
|
|
|
|
divert = None
|
|
if self.flight.divert is not None:
|
|
divert = self.flight.divert.active_runway(
|
|
self.game.theater, self.game.conditions, self.dynamic_runways
|
|
)
|
|
|
|
if self.flight.flight_type in [
|
|
FlightType.TRANSPORT,
|
|
FlightType.AIR_ASSAULT,
|
|
] and self.game.lua_plugin_manager.is_plugin_enabled("ctld"):
|
|
transfer = None
|
|
if self.flight.flight_type == FlightType.TRANSPORT:
|
|
coalition = self.game.coalition_for(player=self.flight.blue)
|
|
transfer = coalition.transfers.transfer_for_flight(self.flight)
|
|
self.mission_data.logistics.append(
|
|
LogisticsGenerator(
|
|
self.flight,
|
|
self.group,
|
|
self.mission,
|
|
self.game.lua_plugin_manager,
|
|
transfer,
|
|
).generate_logistics()
|
|
)
|
|
|
|
mission_start_time, waypoints = WaypointGenerator(
|
|
self.flight,
|
|
self.group,
|
|
self.mission,
|
|
self.time,
|
|
self.game.settings,
|
|
self.mission_data,
|
|
self.unit_map,
|
|
).create_waypoints()
|
|
|
|
divert_position: Point | None = None
|
|
if self.flight.divert is not None:
|
|
divert_position = self.flight.divert.position
|
|
bingo_estimator = BingoEstimator(
|
|
self.flight.unit_type.fuel_consumption,
|
|
self.flight.arrival.position,
|
|
divert_position,
|
|
self.flight.flight_plan.waypoints,
|
|
)
|
|
|
|
self.set_callsigns()
|
|
|
|
return FlightData(
|
|
package=self.flight.package,
|
|
aircraft_type=self.flight.unit_type,
|
|
flight_type=self.flight.flight_type,
|
|
units=self.group.units,
|
|
size=len(self.group.units),
|
|
friendly=self.flight.departure.captured,
|
|
departure_delay=mission_start_time,
|
|
departure=self.flight.departure.active_runway(
|
|
self.game.theater, self.game.conditions, self.dynamic_runways
|
|
),
|
|
arrival=self.flight.arrival.active_runway(
|
|
self.game.theater, self.game.conditions, self.dynamic_runways
|
|
),
|
|
divert=divert,
|
|
waypoints=waypoints,
|
|
intra_flight_channel=flight_channel,
|
|
bingo_fuel=bingo_estimator.estimate_bingo(),
|
|
joker_fuel=bingo_estimator.estimate_joker(),
|
|
custom_name=self.flight.custom_name,
|
|
laser_codes=laser_codes,
|
|
)
|
|
|
|
def configure_flight_member(
|
|
self, unit: FlyingUnit, member: FlightMember, laser_codes: list[Optional[int]]
|
|
) -> None:
|
|
self.set_skill(unit, member)
|
|
if (code := member.tgp_laser_code) is not None:
|
|
laser_codes.append(code.code)
|
|
else:
|
|
laser_codes.append(None)
|
|
|
|
def setup_radios(self) -> RadioFrequency:
|
|
if self.flight.flight_type in {FlightType.AEWC, FlightType.REFUELING}:
|
|
channel = self.radio_registry.alloc_uhf()
|
|
self.register_air_support(channel)
|
|
else:
|
|
channel = self.flight.unit_type.alloc_flight_radio(self.radio_registry)
|
|
|
|
self.group.set_frequency(channel.mhz)
|
|
return channel
|
|
|
|
def register_air_support(self, channel: RadioFrequency) -> None:
|
|
callsign = callsign_for_support_unit(self.group)
|
|
if self.flight.flight_type is FlightType.AEWC:
|
|
self.mission_data.awacs.append(
|
|
AwacsInfo(
|
|
group_name=str(self.group.name),
|
|
callsign=callsign,
|
|
freq=channel,
|
|
depature_location=self.flight.departure.name,
|
|
end_time=self.flight.flight_plan.mission_departure_time,
|
|
start_time=self.flight.flight_plan.takeoff_time(),
|
|
blue=self.flight.departure.captured,
|
|
)
|
|
)
|
|
elif self.flight.flight_type is FlightType.REFUELING:
|
|
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
|
|
self.mission_data.tankers.append(
|
|
TankerInfo(
|
|
group_name=str(self.group.name),
|
|
callsign=callsign,
|
|
variant=self.flight.unit_type.display_name,
|
|
freq=channel,
|
|
tacan=tacan,
|
|
start_time=self.flight.flight_plan.mission_begin_on_station_time,
|
|
end_time=self.flight.flight_plan.mission_departure_time,
|
|
blue=self.flight.departure.captured,
|
|
)
|
|
)
|
|
|
|
def set_skill(self, unit: FlyingUnit, member: FlightMember) -> None:
|
|
if not member.is_player:
|
|
unit.skill = self.skill_level_for(unit, member.pilot)
|
|
return
|
|
|
|
if self.use_client:
|
|
unit.set_client()
|
|
else:
|
|
unit.set_player()
|
|
|
|
def skill_level_for(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> Skill:
|
|
if self.flight.squadron.player:
|
|
base_skill = Skill(self.game.settings.player_skill)
|
|
else:
|
|
base_skill = Skill(self.game.settings.enemy_skill)
|
|
|
|
if pilot is None:
|
|
logging.error(f"Cannot determine skill level: {unit.name} has not pilot")
|
|
return base_skill
|
|
|
|
levels = [
|
|
Skill.Average,
|
|
Skill.Good,
|
|
Skill.High,
|
|
Skill.Excellent,
|
|
]
|
|
current_level = levels.index(base_skill)
|
|
missions_for_skill_increase = 4
|
|
increase = pilot.record.missions_flown // missions_for_skill_increase
|
|
capped_increase = min(current_level + increase, len(levels) - 1)
|
|
|
|
if self.game.settings.ai_pilot_levelling:
|
|
new_level = capped_increase
|
|
else:
|
|
new_level = current_level
|
|
|
|
return levels[new_level]
|
|
|
|
def setup_props(self) -> None:
|
|
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
|
props = dict(member.properties)
|
|
if (code := member.weapon_laser_code) is not None:
|
|
for laser_code_config in self.flight.unit_type.laser_code_configs:
|
|
props.update(laser_code_config.property_dict_for_code(code.code))
|
|
for prop_id, value in props.items():
|
|
unit.set_property(prop_id, value)
|
|
|
|
def setup_payloads(self) -> None:
|
|
for unit, member in zip(self.group.units, self.flight.iter_members()):
|
|
self.setup_payload(unit, member)
|
|
|
|
def setup_payload(self, unit: FlyingUnit, member: FlightMember) -> None:
|
|
unit.pylons.clear()
|
|
|
|
loadout = member.loadout
|
|
if self.game.settings.restrict_weapons_by_date:
|
|
loadout = loadout.degrade_for_date(
|
|
self.flight.unit_type,
|
|
self.game.date,
|
|
self.flight.squadron.coalition.faction,
|
|
)
|
|
|
|
for pylon_number, weapon in loadout.pylons.items():
|
|
if weapon is None:
|
|
continue
|
|
pylon = Pylon.for_aircraft(self.flight.unit_type, pylon_number)
|
|
pylon.equip(unit, weapon)
|
|
|
|
def setup_fuel(self) -> None:
|
|
fuel = self.flight.state.estimate_fuel()
|
|
if fuel < 0:
|
|
logging.warning(
|
|
f"Flight {self.flight} is estimated to have no fuel at mission start. "
|
|
"This estimate does not account for external fuel tanks. Setting "
|
|
"starting fuel to 100kg."
|
|
)
|
|
fuel = 100
|
|
for unit, pilot in zip(self.group.units, self.flight.roster.iter_pilots()):
|
|
if pilot is not None and pilot.player:
|
|
unit.fuel = fuel
|
|
elif (max_takeoff_fuel := self.flight.max_takeoff_fuel()) is not None:
|
|
unit.fuel = max_takeoff_fuel
|
|
else:
|
|
# pydcs arbitrarily reduces the fuel of in-flight spawns by 10%. We do
|
|
# our own tracking, so undo that.
|
|
# https://github.com/pydcs/dcs/commit/303a81a8e0c778599fe136dd22cb2ae8123639a6
|
|
unit.fuel = self.flight.unit_type.dcs_unit_type.fuel_max
|
|
|
|
def set_callsigns(self) -> None:
|
|
if self.flight.callsign is None:
|
|
return
|
|
for unit_index, unit in enumerate(self.group.units):
|
|
unit_callsign = self.flight.callsign.unit_callsign(unit_index + 1)
|
|
if (
|
|
unit_callsign.name is None
|
|
): # pydcs needs unit.callsign to be set for eastern callsigns
|
|
unit.callsign = str(unit_callsign) # type: ignore
|
|
else: # Use western callsign
|
|
unit.callsign_dict = unit_callsign.pydcs_dict(
|
|
country=self.flight.country
|
|
)
|