mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Merge branch 'develop' into helipads
# Conflicts: # resources/campaigns/golan_heights_lite.miz
This commit is contained in:
commit
4eb78810c6
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -11,10 +11,10 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -13,10 +13,10 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
|
||||
18
changelog.md
18
changelog.md
@ -8,11 +8,19 @@ Saves from 2.5 are not compatible with 3.0.
|
||||
* **[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]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information.
|
||||
* **[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.
|
||||
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
|
||||
* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities.
|
||||
* **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
|
||||
* **[Flight Planner]** Flight plans now include bullseye waypoints.
|
||||
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route.
|
||||
* **[Flight Planner]** Planned airspeed increased to 0.85 mach for supersonic airframes and 85% of max speed for subsonic.
|
||||
* **[Flight Planner]** Taxi time estimation for airfields increased from 5 minutes to 8 minutes.
|
||||
* **[Flight Planner]** Reduce expected error margin for flight plans from 10% to 5%.
|
||||
* **[Flight Planner]** SEAD flights are scheduled one minute ahead of the package's TOT so that they can suppress the site ahead of the strike.
|
||||
* **[Flight Planner]** Automatic ATO generation for the player's coalition can now be disabled in the settings.
|
||||
* **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets.
|
||||
* **[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.
|
||||
@ -24,13 +32,17 @@ Saves from 2.5 are not compatible with 3.0.
|
||||
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
|
||||
* **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default.
|
||||
* **[Configuration]** Liberation preferences (DCS install and save game location) are now saved to `%LOCALAPPDATA%/DCSLiberation` to prevent needing to reconfigure each new install.
|
||||
* **[Skynet]** Updated to 2.1.0.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's).
|
||||
* **[Campaign AI]** Fixed bug causing AI to over-purchase cheap aircraft.
|
||||
* **[Campaign AI]** Auto planner will no longer attempt to plan missions for which the faction has no compatible aircraft.
|
||||
* **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used.
|
||||
* **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives.
|
||||
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups.
|
||||
* **[Flight Planner]** AI BAI/DEAD/SEAD flights now have tasks to attack all groups at the target location, not just the primary group (for multi-group SAM sites).
|
||||
|
||||
# 2.5.1
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import dcs
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||
"fuel",
|
||||
"ammo",
|
||||
"comms",
|
||||
"oil",
|
||||
"ware",
|
||||
|
||||
@ -22,14 +22,46 @@ from dcs.ships import (
|
||||
)
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
UNITS_WITH_RADAR = [
|
||||
# Radars
|
||||
TELARS = {
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
}
|
||||
|
||||
TRACK_RADARS = {
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
AirDefence.SAM_Patriot_STR,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
}
|
||||
|
||||
LAUNCHER_TRACKER_PAIRS = {
|
||||
AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
|
||||
AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46,
|
||||
AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR,
|
||||
}
|
||||
|
||||
UNITS_WITH_RADAR = {
|
||||
# Radars
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_SA_11_Buk_Gadfly_C2,
|
||||
AirDefence.SAM_Patriot_CR__AMG_AN_MRC_137,
|
||||
AirDefence.SAM_Patriot_ECS,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.EWR_1L13,
|
||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||
@ -47,7 +79,11 @@ UNITS_WITH_RADAR = [
|
||||
AirDefence.SAM_Roland_EWR,
|
||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
|
||||
AirDefence.SAM_Rapier_Blindfire_TR,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
AirDefence.EWR_FuMG_401_Freya_LZ,
|
||||
AirDefence.EWR_FuSe_65_Würzburg_Riese,
|
||||
# Ships
|
||||
CVN_70_Carl_Vinson,
|
||||
FFG_Oliver_Hazzard_Perry,
|
||||
@ -69,4 +105,4 @@ UNITS_WITH_RADAR = [
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
Type_052C_Destroyer,
|
||||
]
|
||||
}
|
||||
|
||||
43
game/db.py
43
game/db.py
@ -1459,6 +1459,13 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
return None
|
||||
|
||||
|
||||
def flying_type_from_name(name: str) -> Optional[Type[FlyingType]]:
|
||||
unit_type = plane_map.get(name)
|
||||
if unit_type is not None:
|
||||
return unit_type
|
||||
return helicopter_map.get(name)
|
||||
|
||||
|
||||
def unit_type_of(unit: Unit) -> UnitType:
|
||||
if isinstance(unit, Vehicle):
|
||||
return vehicle_map[unit.type]
|
||||
@ -1603,3 +1610,39 @@ F_16C_50.Liveries = DefaultLiveries
|
||||
P_51D_30_NA.Liveries = DefaultLiveries
|
||||
Ju_88A4.Liveries = DefaultLiveries
|
||||
B_17G.Liveries = DefaultLiveries
|
||||
|
||||
# List of airframes that rely on their gun as a primary weapon. We confiscate bullets
|
||||
# from most AI air-to-ground missions since they aren't smart enough to RTB when they're
|
||||
# out of everything other than bullets (DCS does not have an all-but-gun winchester
|
||||
# option) and we don't want to be attacking fully functional Tors with a Vulcan.
|
||||
#
|
||||
# These airframes are the exceptions. They probably should be using their gun regardless
|
||||
# of the mission type.
|
||||
GUN_RELIANT_AIRFRAMES: List[Type[FlyingType]] = [
|
||||
AH_1W,
|
||||
AH_64A,
|
||||
AH_64D,
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
A_20G,
|
||||
Bf_109K_4,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_86F_Sabre,
|
||||
Ju_88A4,
|
||||
Ka_50,
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
Mi_24V,
|
||||
Mi_28N,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
Su_25,
|
||||
Su_25T,
|
||||
]
|
||||
|
||||
@ -30,6 +30,7 @@ from game.unitmap import (
|
||||
FrontLineUnit,
|
||||
GroundObjectUnit,
|
||||
UnitMap,
|
||||
FlyingUnit,
|
||||
)
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
@ -41,24 +42,24 @@ DEBRIEFING_LOG_EXTENSION = "log"
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirLosses:
|
||||
player: List[Flight]
|
||||
enemy: List[Flight]
|
||||
player: List[FlyingUnit]
|
||||
enemy: List[FlyingUnit]
|
||||
|
||||
@property
|
||||
def losses(self) -> Iterator[Flight]:
|
||||
def losses(self) -> Iterator[FlyingUnit]:
|
||||
return itertools.chain(self.player, self.enemy)
|
||||
|
||||
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
|
||||
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||
losses = self.player if player else self.enemy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
losses_by_type[loss.flight.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def surviving_flight_members(self, flight: Flight) -> int:
|
||||
losses = 0
|
||||
for loss in self.losses:
|
||||
if loss == flight:
|
||||
if loss.flight == flight:
|
||||
losses += 1
|
||||
return flight.count - losses
|
||||
|
||||
@ -239,14 +240,14 @@ class Debriefing:
|
||||
player_losses = []
|
||||
enemy_losses = []
|
||||
for unit_name in self.state_data.killed_aircraft:
|
||||
flight = self.unit_map.flight(unit_name)
|
||||
if flight is None:
|
||||
aircraft = self.unit_map.flight(unit_name)
|
||||
if aircraft is None:
|
||||
logging.error(f"Could not find Flight matching {unit_name}")
|
||||
continue
|
||||
if flight.departure.captured:
|
||||
player_losses.append(flight)
|
||||
if aircraft.flight.departure.captured:
|
||||
player_losses.append(aircraft)
|
||||
else:
|
||||
enemy_losses.append(flight)
|
||||
enemy_losses.append(aircraft)
|
||||
return AirLosses(player_losses, enemy_losses)
|
||||
|
||||
def dead_ground_units(self) -> GroundLosses:
|
||||
|
||||
@ -120,11 +120,15 @@ class Event:
|
||||
self.game.red_ato, debriefing.air_losses, for_player=False
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def commit_air_losses(debriefing: Debriefing) -> None:
|
||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.air_losses.losses:
|
||||
aircraft = loss.unit_type
|
||||
cp = loss.departure
|
||||
if (
|
||||
not loss.pilot.player
|
||||
or not self.game.settings.invulnerable_player_pilots
|
||||
):
|
||||
loss.pilot.kill()
|
||||
aircraft = loss.flight.unit_type
|
||||
cp = loss.flight.departure
|
||||
available = cp.base.total_units_of_type(aircraft)
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
@ -136,6 +140,23 @@ class Event:
|
||||
logging.info(f"{aircraft} destroyed from {cp}")
|
||||
cp.base.aircraft[aircraft] -= 1
|
||||
|
||||
@staticmethod
|
||||
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
for idx, pilot in enumerate(flight.pilots):
|
||||
if pilot is None:
|
||||
logging.error(
|
||||
f"Cannot award experience to pilot #{idx} of {flight} "
|
||||
"because no pilot is assigned"
|
||||
)
|
||||
continue
|
||||
pilot.record.missions_flown += 1
|
||||
|
||||
def commit_pilot_experience(self) -> None:
|
||||
self._commit_pilot_experience(self.game.blue_ato)
|
||||
self._commit_pilot_experience(self.game.red_ato)
|
||||
|
||||
@staticmethod
|
||||
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.front_line_losses:
|
||||
@ -249,6 +270,7 @@ class Event:
|
||||
logging.info("Committing mission results")
|
||||
|
||||
self.commit_air_losses(debriefing)
|
||||
self.commit_pilot_experience()
|
||||
self.commit_front_line_losses(debriefing)
|
||||
self.commit_convoy_losses(debriefing)
|
||||
self.commit_airlift_losses(debriefing)
|
||||
|
||||
@ -27,6 +27,9 @@ from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
|
||||
|
||||
@dataclass
|
||||
class Faction:
|
||||
#: List of locales to use when generating random names. If not set, Faker will
|
||||
#: choose the default locale.
|
||||
locales: Optional[List[str]]
|
||||
|
||||
# Country used by this faction
|
||||
country: str = field(default="")
|
||||
@ -132,8 +135,7 @@ class Faction:
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
|
||||
faction = Faction()
|
||||
faction = Faction(locales=json.get("locales"))
|
||||
|
||||
faction.country = json.get("country", "/")
|
||||
if faction.country not in [c.name for c in country_dict.values()]:
|
||||
|
||||
69
game/game.py
69
game/game.py
@ -4,12 +4,13 @@ import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Iterator
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
from faker import Faker
|
||||
|
||||
from game import db
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
@ -32,7 +33,8 @@ from .infos.information import Information
|
||||
from .navmesh import NavMesh
|
||||
from .procurement import AircraftProcurementRequest, ProcurementAi
|
||||
from .profiling import logged_duration
|
||||
from .settings import Settings
|
||||
from .settings import Settings, AutoAtoBehavior
|
||||
from .squadrons import Pilot, AirWing
|
||||
from .theater import ConflictTheater
|
||||
from .theater.bullseye import Bullseye
|
||||
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||
@ -140,6 +142,12 @@ class Game:
|
||||
|
||||
self.sanitize_sides()
|
||||
|
||||
self.blue_faker = Faker(self.player_faction.locales)
|
||||
self.red_faker = Faker(self.enemy_faction.locales)
|
||||
|
||||
self.blue_air_wing = AirWing(self, player=True)
|
||||
self.red_air_wing = AirWing(self, player=False)
|
||||
|
||||
self.on_load()
|
||||
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
@ -150,6 +158,8 @@ class Game:
|
||||
del state["red_threat_zone"]
|
||||
del state["blue_navmesh"]
|
||||
del state["red_navmesh"]
|
||||
del state["blue_faker"]
|
||||
del state["red_faker"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
@ -205,6 +215,21 @@ class Game:
|
||||
return self.player_faction
|
||||
return self.enemy_faction
|
||||
|
||||
def faker_for(self, player: bool) -> Faker:
|
||||
if player:
|
||||
return self.blue_faker
|
||||
return self.red_faker
|
||||
|
||||
def air_wing_for(self, player: bool) -> AirWing:
|
||||
if player:
|
||||
return self.blue_air_wing
|
||||
return self.red_air_wing
|
||||
|
||||
def country_for(self, player: bool) -> str:
|
||||
if player:
|
||||
return self.player_country
|
||||
return self.enemy_country
|
||||
|
||||
def bullseye_for(self, player: bool) -> Bullseye:
|
||||
if player:
|
||||
return self.blue_bullseye
|
||||
@ -281,6 +306,8 @@ class Game:
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
self.blue_faker = Faker(self.faction_for(player=True).locales)
|
||||
self.red_faker = Faker(self.faction_for(player=False).locales)
|
||||
|
||||
def reset_ato(self) -> None:
|
||||
self.blue_ato.clear()
|
||||
@ -325,8 +352,10 @@ class Game:
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
logging.info("Pass turn")
|
||||
self.finish_turn(no_action)
|
||||
self.initialize_turn()
|
||||
with logged_duration("Turn finalization"):
|
||||
self.finish_turn(no_action)
|
||||
with logged_duration("Turn initialization"):
|
||||
self.initialize_turn()
|
||||
|
||||
# Autosave progress
|
||||
persistency.autosave(self)
|
||||
@ -360,6 +389,8 @@ class Game:
|
||||
# Update statistics
|
||||
self.game_stats.update(self)
|
||||
|
||||
self.blue_air_wing.reset()
|
||||
self.red_air_wing.reset()
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
@ -370,18 +401,28 @@ class Game:
|
||||
return self.process_win_loss(turn_state)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
self.compute_transit_networks()
|
||||
with logged_duration("Computing conflict positions"):
|
||||
self.compute_conflicts_position()
|
||||
with logged_duration("Threat zone computation"):
|
||||
self.compute_threat_zones()
|
||||
with logged_duration("Transit network identification"):
|
||||
self.compute_transit_networks()
|
||||
self.ground_planners = {}
|
||||
|
||||
self.transfers.order_airlift_assets()
|
||||
self.transfers.plan_transports()
|
||||
self.blue_procurement_requests.clear()
|
||||
self.red_procurement_requests.clear()
|
||||
|
||||
with logged_duration("Mission planning"):
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
with logged_duration("Procurement of airlift assets"):
|
||||
self.transfers.order_airlift_assets()
|
||||
with logged_duration("Transport planning"):
|
||||
self.transfers.plan_transports()
|
||||
|
||||
with logged_duration("Blue mission planning"):
|
||||
if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
|
||||
with logged_duration("Red mission planning"):
|
||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
||||
red_planner.plan_missions()
|
||||
|
||||
@ -408,7 +449,7 @@ class Game:
|
||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||
front_line_budget_share=ground_portion,
|
||||
).spend_budget(self.budget, self.blue_procurement_requests)
|
||||
).spend_budget(self.budget)
|
||||
|
||||
self.enemy_budget = ProcurementAi(
|
||||
self,
|
||||
@ -418,7 +459,7 @@ class Game:
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True,
|
||||
front_line_budget_share=ground_portion,
|
||||
).spend_budget(self.enemy_budget, self.red_procurement_requests)
|
||||
).spend_budget(self.enemy_budget)
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
|
||||
@ -103,7 +103,7 @@ class NavMesh:
|
||||
# currently.
|
||||
p = ShapelyPoint(point.x, point.y)
|
||||
for navpoly in self.polys:
|
||||
if navpoly.poly.contains(p):
|
||||
if navpoly.poly.intersects(p):
|
||||
return navpoly
|
||||
return None
|
||||
|
||||
|
||||
@ -50,6 +50,7 @@ class ProcurementAi:
|
||||
|
||||
self.game = game
|
||||
self.is_player = for_player
|
||||
self.air_wing = game.air_wing_for(for_player)
|
||||
self.faction = faction
|
||||
self.manage_runways = manage_runways
|
||||
self.manage_front_line = manage_front_line
|
||||
@ -57,9 +58,7 @@ class ProcurementAi:
|
||||
self.front_line_budget_share = front_line_budget_share
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def spend_budget(
|
||||
self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
|
||||
) -> float:
|
||||
def spend_budget(self, budget: float) -> float:
|
||||
if self.manage_runways:
|
||||
budget = self.repair_runways(budget)
|
||||
if self.manage_front_line:
|
||||
@ -163,23 +162,31 @@ class ProcurementAi:
|
||||
|
||||
return budget
|
||||
|
||||
def _affordable_aircraft_of_types(
|
||||
def _affordable_aircraft_for_task(
|
||||
self,
|
||||
types: List[Type[FlyingType]],
|
||||
task: FlightType,
|
||||
airbase: ControlPoint,
|
||||
number: int,
|
||||
max_price: float,
|
||||
) -> Optional[Type[FlyingType]]:
|
||||
best_choice: Optional[Type[FlyingType]] = None
|
||||
for unit in [u for u in types if u in self.faction.aircrafts]:
|
||||
for unit in aircraft_for_task(task):
|
||||
if unit not in self.faction.aircrafts:
|
||||
continue
|
||||
if db.PRICES[unit] * number > max_price:
|
||||
continue
|
||||
if not airbase.can_operate(unit):
|
||||
continue
|
||||
|
||||
# Affordable and compatible. To keep some variety, skip with a 50/50
|
||||
# chance. Might be a good idea to have the chance to skip based on
|
||||
# the price compared to the rest of the choices.
|
||||
for squadron in self.air_wing.squadrons_for(unit):
|
||||
if task in squadron.mission_types:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
# Affordable, compatible, and we have a squadron capable of the task. To
|
||||
# keep some variety, skip with a 50/50 chance. Might be a good idea to have
|
||||
# the chance to skip based on the price compared to the rest of the choices.
|
||||
best_choice = unit
|
||||
if random.choice([True, False]):
|
||||
break
|
||||
@ -188,8 +195,8 @@ class ProcurementAi:
|
||||
def affordable_aircraft_for(
|
||||
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
|
||||
) -> Optional[Type[FlyingType]]:
|
||||
return self._affordable_aircraft_of_types(
|
||||
aircraft_for_task(request.task_capability), airbase, request.number, budget
|
||||
return self._affordable_aircraft_for_task(
|
||||
request.task_capability, airbase, request.number, budget
|
||||
)
|
||||
|
||||
def fulfill_aircraft_request(
|
||||
@ -255,10 +262,19 @@ class ProcurementAi:
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
|
||||
total_ground_units_allocated_to_this_control_point = (
|
||||
self.total_ground_units_allocated_to(cp)
|
||||
)
|
||||
|
||||
if not cp.has_ground_unit_source(self.game):
|
||||
continue
|
||||
|
||||
if self.total_ground_units_allocated_to(cp) >= 50:
|
||||
if (
|
||||
total_ground_units_allocated_to_this_control_point >= 50
|
||||
or total_ground_units_allocated_to_this_control_point
|
||||
>= cp.frontline_unit_count_limit
|
||||
):
|
||||
# Control point is already sufficiently defended.
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import timeit
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
from typing import Iterator
|
||||
@ -11,3 +14,22 @@ def logged_duration(event: str) -> Iterator[None]:
|
||||
yield
|
||||
end = timeit.default_timer()
|
||||
logging.debug("%s took %s", event, timedelta(seconds=end - start))
|
||||
|
||||
|
||||
class MultiEventTracer:
|
||||
def __init__(self) -> None:
|
||||
self.events: dict[str, timedelta] = defaultdict(timedelta)
|
||||
|
||||
def __enter__(self) -> MultiEventTracer:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
for event, duration in self.events.items():
|
||||
logging.debug("%s took %s", event, duration)
|
||||
|
||||
@contextmanager
|
||||
def trace(self, event: str) -> Iterator[None]:
|
||||
start = timeit.default_timer()
|
||||
yield
|
||||
end = timeit.default_timer()
|
||||
self.events[event] += timedelta(seconds=end - start)
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from enum import Enum, unique
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
|
||||
@unique
|
||||
class AutoAtoBehavior(Enum):
|
||||
Disabled = "Disabled"
|
||||
Never = "Never assign player pilots"
|
||||
Default = "No preference"
|
||||
Prefer = "Prefer player pilots"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
|
||||
@ -27,7 +36,7 @@ class Settings:
|
||||
default_start_type: str = "Cold"
|
||||
|
||||
# Mission specific
|
||||
desired_player_mission_duration: timedelta = timedelta(minutes=90)
|
||||
desired_player_mission_duration: timedelta = timedelta(minutes=60)
|
||||
|
||||
# Campaign management
|
||||
automate_runway_repair: bool = False
|
||||
@ -36,6 +45,9 @@ class Settings:
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = False
|
||||
generate_dark_kneeboard: bool = False
|
||||
invulnerable_player_pilots: bool = True
|
||||
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
|
||||
auto_ato_player_missions_asap: bool = False
|
||||
|
||||
# Performance oriented
|
||||
perf_red_alert_state: bool = True
|
||||
|
||||
354
game/squadrons.py
Normal file
354
game/squadrons.py
Normal file
@ -0,0 +1,354 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import unique, Enum
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
Type,
|
||||
Tuple,
|
||||
List,
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
import yaml
|
||||
from dcs.unittype import FlyingType
|
||||
from faker import Faker
|
||||
|
||||
from game.db import flying_type_from_name
|
||||
from game.settings import AutoAtoBehavior
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PilotRecord:
|
||||
missions_flown: int = field(default=0)
|
||||
|
||||
|
||||
@unique
|
||||
class PilotStatus(Enum):
|
||||
Active = "Active"
|
||||
OnLeave = "On leave"
|
||||
Dead = "Dead"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pilot:
|
||||
name: str
|
||||
player: bool = field(default=False)
|
||||
status: PilotStatus = field(default=PilotStatus.Active)
|
||||
record: PilotRecord = field(default_factory=PilotRecord)
|
||||
|
||||
@property
|
||||
def alive(self) -> bool:
|
||||
return self.status is not PilotStatus.Dead
|
||||
|
||||
@property
|
||||
def on_leave(self) -> bool:
|
||||
return self.status is PilotStatus.OnLeave
|
||||
|
||||
def send_on_leave(self) -> None:
|
||||
if self.status is not PilotStatus.Active:
|
||||
raise RuntimeError("Only active pilots may be sent on leave")
|
||||
self.status = PilotStatus.OnLeave
|
||||
|
||||
def return_from_leave(self) -> None:
|
||||
if self.status is not PilotStatus.OnLeave:
|
||||
raise RuntimeError("Only pilots on leave may be returned from leave")
|
||||
self.status = PilotStatus.Active
|
||||
|
||||
def kill(self) -> None:
|
||||
self.status = PilotStatus.Dead
|
||||
|
||||
@classmethod
|
||||
def random(cls, faker: Faker) -> Pilot:
|
||||
return Pilot(faker.name())
|
||||
|
||||
|
||||
@dataclass
|
||||
class Squadron:
|
||||
name: str
|
||||
nickname: str
|
||||
country: str
|
||||
role: str
|
||||
aircraft: Type[FlyingType]
|
||||
livery: Optional[str]
|
||||
mission_types: Tuple[FlightType, ...]
|
||||
pilots: List[Pilot]
|
||||
available_pilots: List[Pilot] = field(init=False, hash=False, compare=False)
|
||||
|
||||
# We need a reference to the Game so that we can access the Faker without needing to
|
||||
# persist it to the save game, or having to reconstruct it (it's not cheap) each
|
||||
# time we create or load a squadron.
|
||||
game: Game = field(hash=False, compare=False)
|
||||
player: bool
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.available_pilots = list(self.active_pilots)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.name} "{self.nickname}"'
|
||||
|
||||
def claim_available_pilot(self) -> Optional[Pilot]:
|
||||
# No pilots available, so the preference is irrelevant. Create a new pilot and
|
||||
# return it.
|
||||
if not self.available_pilots:
|
||||
self.enlist_new_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
# For opfor, so player/AI option is irrelevant.
|
||||
if not self.player:
|
||||
return self.available_pilots.pop()
|
||||
|
||||
preference = self.game.settings.auto_ato_behavior
|
||||
|
||||
# No preference, so the first pilot is fine.
|
||||
if preference is AutoAtoBehavior.Default:
|
||||
return self.available_pilots.pop()
|
||||
|
||||
prefer_players = preference is AutoAtoBehavior.Prefer
|
||||
for pilot in self.available_pilots:
|
||||
if pilot.player == prefer_players:
|
||||
self.available_pilots.remove(pilot)
|
||||
return pilot
|
||||
|
||||
# No pilot was found that matched the user's preference.
|
||||
#
|
||||
# If they chose to *never* assign players and only players remain in the pool,
|
||||
# we cannot fill the slot with the available pilots. Recruit a new one.
|
||||
#
|
||||
# If they prefer players and we're out of players, just return an AI pilot.
|
||||
if not prefer_players:
|
||||
self.enlist_new_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
def claim_pilot(self, pilot: Pilot) -> None:
|
||||
if pilot not in self.available_pilots:
|
||||
raise ValueError(
|
||||
f"Cannot assign {pilot} to {self} because they are not available"
|
||||
)
|
||||
self.available_pilots.remove(pilot)
|
||||
|
||||
def return_pilot(self, pilot: Pilot) -> None:
|
||||
self.available_pilots.append(pilot)
|
||||
|
||||
def return_pilots(self, pilots: Iterable[Pilot]) -> None:
|
||||
self.available_pilots.extend(pilots)
|
||||
|
||||
def enlist_new_pilots(self, count: int) -> None:
|
||||
new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
|
||||
self.pilots.extend(new_pilots)
|
||||
self.available_pilots.extend(new_pilots)
|
||||
|
||||
def return_all_pilots(self) -> None:
|
||||
self.available_pilots = list(self.active_pilots)
|
||||
|
||||
@property
|
||||
def faker(self) -> Faker:
|
||||
return self.game.faker_for(self.player)
|
||||
|
||||
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
|
||||
return [p for p in self.pilots if p.status == status]
|
||||
|
||||
@property
|
||||
def active_pilots(self) -> list[Pilot]:
|
||||
return self._pilots_with_status(PilotStatus.Active)
|
||||
|
||||
@property
|
||||
def pilots_on_leave(self) -> list[Pilot]:
|
||||
return self._pilots_with_status(PilotStatus.OnLeave)
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return len(self.active_pilots) + len(self.pilots_on_leave)
|
||||
|
||||
def pilot_at_index(self, index: int) -> Pilot:
|
||||
return self.pilots[index]
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
|
||||
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
with path.open() as squadron_file:
|
||||
data = yaml.safe_load(squadron_file)
|
||||
|
||||
unit_type = flying_type_from_name(data["aircraft"])
|
||||
if unit_type is None:
|
||||
raise KeyError(f"Could not find any aircraft with the ID {unit_type}")
|
||||
|
||||
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
|
||||
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
|
||||
|
||||
mission_types = [FlightType.from_name(n) for n in data["mission_types"]]
|
||||
tasks = tasks_for_aircraft(unit_type)
|
||||
for mission_type in list(mission_types):
|
||||
if mission_type not in tasks:
|
||||
logging.error(
|
||||
f"Squadron has mission type {mission_type} but {unit_type} is not "
|
||||
f"capable of that task: {path}"
|
||||
)
|
||||
mission_types.remove(mission_type)
|
||||
|
||||
return Squadron(
|
||||
name=data["name"],
|
||||
nickname=data["nickname"],
|
||||
country=data["country"],
|
||||
role=data["role"],
|
||||
aircraft=unit_type,
|
||||
livery=data.get("livery"),
|
||||
mission_types=tuple(mission_types),
|
||||
pilots=pilots,
|
||||
game=game,
|
||||
player=player,
|
||||
)
|
||||
|
||||
|
||||
class SquadronLoader:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
self.game = game
|
||||
self.player = player
|
||||
|
||||
@staticmethod
|
||||
def squadron_directories() -> Iterator[Path]:
|
||||
from game import persistency
|
||||
|
||||
yield Path(persistency.base_path()) / "Liberation/Squadrons"
|
||||
yield Path("resources/squadrons")
|
||||
|
||||
def load(self) -> dict[Type[FlyingType], list[Squadron]]:
|
||||
squadrons: dict[Type[FlyingType], list[Squadron]] = defaultdict(list)
|
||||
country = self.game.country_for(self.player)
|
||||
faction = self.game.faction_for(self.player)
|
||||
any_country = country.startswith("Combined Joint Task Forces ")
|
||||
for directory in self.squadron_directories():
|
||||
for path, squadron in self.load_squadrons_from(directory):
|
||||
if not any_country and squadron.country != country:
|
||||
logging.debug(
|
||||
"Not using squadron for non-matching country (is "
|
||||
f"{squadron.country}, need {country}: {path}"
|
||||
)
|
||||
continue
|
||||
if squadron.aircraft not in faction.aircrafts:
|
||||
logging.debug(
|
||||
f"Not using squadron because {faction.name} cannot use "
|
||||
f"{squadron.aircraft}: {path}"
|
||||
)
|
||||
continue
|
||||
logging.debug(
|
||||
f"Found {squadron.name} {squadron.aircraft} {squadron.role} "
|
||||
f"compatible with {faction.name}"
|
||||
)
|
||||
squadrons[squadron.aircraft].append(squadron)
|
||||
# Convert away from defaultdict because defaultdict doesn't unpickle so we don't
|
||||
# want it in the save state.
|
||||
return dict(squadrons)
|
||||
|
||||
def load_squadrons_from(self, directory: Path) -> Iterator[Tuple[Path, Squadron]]:
|
||||
logging.debug(f"Looking for factions in {directory}")
|
||||
# First directory level is the aircraft type so that historical squadrons that
|
||||
# have flown multiple airframes can be defined as many times as needed. The main
|
||||
# load() method is responsible for filtering out squadrons that aren't
|
||||
# compatible with the faction.
|
||||
for squadron_path in directory.glob("*/*.yaml"):
|
||||
try:
|
||||
yield squadron_path, Squadron.from_yaml(
|
||||
squadron_path, self.game, self.player
|
||||
)
|
||||
except Exception as ex:
|
||||
raise RuntimeError(
|
||||
f"Failed to load squadron defined by {squadron_path}"
|
||||
) from ex
|
||||
|
||||
|
||||
class AirWing:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
|
||||
|
||||
self.game = game
|
||||
self.player = player
|
||||
self.squadrons = SquadronLoader(game, player).load()
|
||||
|
||||
count = itertools.count(1)
|
||||
for aircraft in game.faction_for(player).aircrafts:
|
||||
if aircraft in self.squadrons:
|
||||
continue
|
||||
self.squadrons[aircraft] = [
|
||||
Squadron(
|
||||
name=f"Squadron {next(count):03}",
|
||||
nickname=self.random_nickname(),
|
||||
country=game.country_for(player),
|
||||
role="Flying Squadron",
|
||||
aircraft=aircraft,
|
||||
livery=None,
|
||||
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
||||
pilots=[],
|
||||
game=game,
|
||||
player=player,
|
||||
)
|
||||
]
|
||||
|
||||
def squadrons_for(self, aircraft: Type[FlyingType]) -> Sequence[Squadron]:
|
||||
return self.squadrons[aircraft]
|
||||
|
||||
def squadrons_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
||||
for squadron in self.iter_squadrons():
|
||||
if task in squadron.mission_types:
|
||||
yield squadron
|
||||
|
||||
def squadron_for(self, aircraft: Type[FlyingType]) -> Squadron:
|
||||
return self.squadrons_for(aircraft)[0]
|
||||
|
||||
def iter_squadrons(self) -> Iterator[Squadron]:
|
||||
return itertools.chain.from_iterable(self.squadrons.values())
|
||||
|
||||
def squadron_at_index(self, index: int) -> Squadron:
|
||||
return list(self.iter_squadrons())[index]
|
||||
|
||||
def reset(self) -> None:
|
||||
for squadron in self.iter_squadrons():
|
||||
squadron.return_all_pilots()
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(len(s) for s in self.squadrons.values())
|
||||
|
||||
@staticmethod
|
||||
def _make_random_nickname() -> str:
|
||||
from gen.naming import ANIMALS
|
||||
|
||||
animal = random.choice(ANIMALS)
|
||||
adjective = random.choice(
|
||||
(
|
||||
None,
|
||||
"Red",
|
||||
"Blue",
|
||||
"Green",
|
||||
"Golden",
|
||||
"Black",
|
||||
"Fighting",
|
||||
"Flying",
|
||||
)
|
||||
)
|
||||
if adjective is None:
|
||||
return animal.title()
|
||||
return f"{adjective} {animal}".title()
|
||||
|
||||
def random_nickname(self) -> str:
|
||||
while True:
|
||||
nickname = self._make_random_nickname()
|
||||
for squadron in self.iter_squadrons():
|
||||
if squadron.nickname == nickname:
|
||||
break
|
||||
else:
|
||||
return nickname
|
||||
@ -22,7 +22,7 @@ from dcs.ships import (
|
||||
DDG_Arleigh_Burke_IIa,
|
||||
LHA_1_Tarawa,
|
||||
)
|
||||
from dcs.statics import Fortification
|
||||
from dcs.statics import Fortification, Warehouse
|
||||
from dcs.terrain import (
|
||||
caucasus,
|
||||
nevada,
|
||||
@ -129,6 +129,8 @@ class MizCampaignLoader:
|
||||
|
||||
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
|
||||
|
||||
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
|
||||
|
||||
REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
||||
|
||||
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
||||
@ -321,6 +323,12 @@ class MizCampaignLoader:
|
||||
if group.units[0].type in self.FACTORY_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ammunition_depots(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.AMMUNITION_DEPOT_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
@ -560,6 +568,12 @@ class MizCampaignLoader:
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ammunition_depots:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ammunition_depots.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_strike_locations.append(
|
||||
|
||||
@ -8,7 +8,20 @@ from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import total_ordering
|
||||
from typing import Any, Dict, Iterator, List, Optional, Set, TYPE_CHECKING, Type, Union
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
TYPE_CHECKING,
|
||||
Type,
|
||||
Union,
|
||||
Sequence,
|
||||
Iterable,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from dcs import helicopters
|
||||
from dcs.mapping import Point
|
||||
@ -48,6 +61,9 @@ if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
|
||||
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
|
||||
|
||||
|
||||
class ControlPointType(Enum):
|
||||
#: An airbase with slots for everything.
|
||||
@ -150,6 +166,9 @@ class PresetLocations:
|
||||
#: Locations of factories for producing ground units. These will always be spawned.
|
||||
factories: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of ammo depots for controlling number of units on the front line at a control point.
|
||||
ammunition_depots: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of stationary armor groups. These will always be spawned.
|
||||
armor_groups: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
@ -308,8 +327,8 @@ class ControlPoint(MissionTarget, ABC):
|
||||
# TODO: Should be Airbase specific.
|
||||
self.has_frontline = has_frontline
|
||||
self.connected_points: List[ControlPoint] = []
|
||||
self.convoy_routes: Dict[ControlPoint, List[Point]] = {}
|
||||
self.shipping_lanes: Dict[ControlPoint, List[Point]] = {}
|
||||
self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {}
|
||||
self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {}
|
||||
self.base: Base = Base()
|
||||
self.cptype = cptype
|
||||
# TODO: Should be Airbase specific.
|
||||
@ -467,24 +486,21 @@ class ControlPoint(MissionTarget, ABC):
|
||||
"""
|
||||
...
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def connect(self, to: ControlPoint) -> None:
|
||||
self.connected_points.append(to)
|
||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||
|
||||
def convoy_origin_for(self, destination: ControlPoint) -> Point:
|
||||
return self.convoy_route_to(destination)[0]
|
||||
|
||||
def convoy_route_to(self, destination: ControlPoint) -> List[Point]:
|
||||
def convoy_route_to(self, destination: ControlPoint) -> Sequence[Point]:
|
||||
return self.convoy_routes[destination]
|
||||
|
||||
def create_convoy_route(self, to: ControlPoint, waypoints: List[Point]) -> None:
|
||||
def create_convoy_route(self, to: ControlPoint, waypoints: Iterable[Point]) -> None:
|
||||
self.connected_points.append(to)
|
||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
||||
self.convoy_routes[to] = waypoints
|
||||
self.convoy_routes[to] = tuple(waypoints)
|
||||
|
||||
def create_shipping_lane(self, to: ControlPoint, waypoints: List[Point]) -> None:
|
||||
self.shipping_lanes[to] = waypoints
|
||||
def create_shipping_lane(
|
||||
self, to: ControlPoint, waypoints: Iterable[Point]
|
||||
) -> None:
|
||||
self.shipping_lanes[to] = tuple(waypoints)
|
||||
|
||||
@abstractmethod
|
||||
def runway_is_operational(self) -> bool:
|
||||
@ -788,15 +804,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
def income_per_turn(self) -> int:
|
||||
return 0
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def has_active_frontline(self) -> bool:
|
||||
return any(not c.is_friendly(self.captured) for c in self.connected_points)
|
||||
@ -807,10 +814,29 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
return self.captured != other.captured
|
||||
|
||||
@property
|
||||
def frontline_unit_count_limit(self) -> int:
|
||||
|
||||
tally_connected_ammo_depots = 0
|
||||
|
||||
for cp_objective in self.connected_objectives:
|
||||
if cp_objective.category == "ammo" and not cp_objective.is_dead:
|
||||
tally_connected_ammo_depots += 1
|
||||
|
||||
return (
|
||||
FREE_FRONTLINE_UNIT_SUPPLY
|
||||
+ tally_connected_ammo_depots * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION
|
||||
)
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
return []
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def category(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
def __init__(
|
||||
@ -840,18 +866,21 @@ class Airfield(ControlPoint):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
else:
|
||||
if not self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.OCA_AIRCRAFT,
|
||||
FlightType.OCA_RUNWAY,
|
||||
]
|
||||
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.AEWC,
|
||||
# TODO: FlightType.INTERCEPTION
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return len(self.airport.parking_slots)
|
||||
@ -888,6 +917,10 @@ class Airfield(ControlPoint):
|
||||
def income_per_turn(self) -> int:
|
||||
return 20
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
return "airfield"
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
@property
|
||||
@ -967,6 +1000,13 @@ class Carrier(NavalControlPoint):
|
||||
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
||||
)
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
yield from super().mission_types(for_player)
|
||||
if self.is_friendly(for_player):
|
||||
yield FlightType.AEWC
|
||||
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
|
||||
@ -981,6 +1021,10 @@ class Carrier(NavalControlPoint):
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 90
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
return "cv"
|
||||
|
||||
|
||||
class Lha(NavalControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
@ -1011,6 +1055,10 @@ class Lha(NavalControlPoint):
|
||||
def total_aircraft_parking(self) -> int:
|
||||
return 20
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
return "lha"
|
||||
|
||||
|
||||
class OffMapSpawn(ControlPoint):
|
||||
def runway_is_operational(self) -> bool:
|
||||
@ -1061,6 +1109,10 @@ class OffMapSpawn(ControlPoint):
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
return "offmap"
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
@ -1094,18 +1146,10 @@ class Fob(ControlPoint):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if self.is_friendly(for_player):
|
||||
yield from [
|
||||
FlightType.BARCAP,
|
||||
# TODO: FlightType.LOGISTICS
|
||||
]
|
||||
else:
|
||||
yield from [
|
||||
FlightType.STRIKE,
|
||||
FlightType.SWEEP,
|
||||
FlightType.ESCORT,
|
||||
FlightType.SEAD,
|
||||
]
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.STRIKE
|
||||
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def total_aircraft_parking(self) -> int:
|
||||
@ -1128,3 +1172,7 @@ class Fob(ControlPoint):
|
||||
@property
|
||||
def income_per_turn(self) -> int:
|
||||
return 10
|
||||
|
||||
@property
|
||||
def category(self) -> str:
|
||||
return "fob"
|
||||
|
||||
@ -52,7 +52,7 @@ class FrontLine(MissionTarget):
|
||||
self.blue_cp = blue_point
|
||||
self.red_cp = red_point
|
||||
try:
|
||||
route = blue_point.convoy_route_to(red_point)
|
||||
route = list(blue_point.convoy_route_to(red_point))
|
||||
except KeyError:
|
||||
# Some campaigns are air only and the mission generator currently relies on
|
||||
# *some* "front line" being drawn between these two. In this case there will
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -9,3 +9,26 @@ class LatLon:
|
||||
|
||||
def as_list(self) -> List[float]:
|
||||
return [self.latitude, self.longitude]
|
||||
|
||||
@staticmethod
|
||||
def _components(dimension: float) -> Tuple[int, int, float]:
|
||||
degrees = int(dimension)
|
||||
minutes = int(dimension * 60 % 60)
|
||||
seconds = dimension * 3600 % 60
|
||||
return degrees, minutes, seconds
|
||||
|
||||
def _format_component(
|
||||
self, dimension: float, hemispheres: Tuple[str, str], seconds_precision: int
|
||||
) -> str:
|
||||
hemisphere = hemispheres[0] if dimension >= 0 else hemispheres[1]
|
||||
degrees, minutes, seconds = self._components(dimension)
|
||||
return f"{degrees}°{minutes:02}'{seconds:02.{seconds_precision}f}\"{hemisphere}"
|
||||
|
||||
def format_dms(self, include_decimal_seconds: bool = False) -> str:
|
||||
precision = 2 if include_decimal_seconds else 0
|
||||
return " ".join(
|
||||
[
|
||||
self._format_component(self.latitude, ("N", "S"), precision),
|
||||
self._format_component(self.longitude, ("E", "W"), precision),
|
||||
]
|
||||
)
|
||||
|
||||
@ -37,7 +37,7 @@ class MissionTarget:
|
||||
yield from [
|
||||
FlightType.ESCORT,
|
||||
FlightType.TARCAP,
|
||||
FlightType.SEAD,
|
||||
FlightType.SEAD_ESCORT,
|
||||
FlightType.SWEEP,
|
||||
# TODO: FlightType.ELINT,
|
||||
# TODO: FlightType.EWAR,
|
||||
|
||||
@ -471,6 +471,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.generate_strike_targets()
|
||||
self.generate_offshore_strike_targets()
|
||||
self.generate_factories()
|
||||
self.generate_ammunition_depots()
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
@ -629,6 +630,10 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_ammunition_depots(self) -> None:
|
||||
for position in self.control_point.preset_locations.ammunition_depots:
|
||||
self.generate_strike_target_at(category="ammo", position=position)
|
||||
|
||||
def generate_factories(self) -> None:
|
||||
"""Generates the factories that are required by the campaign."""
|
||||
for position in self.control_point.preset_locations.factories:
|
||||
@ -828,6 +833,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
FobDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_armor_groups()
|
||||
self.generate_factories()
|
||||
self.generate_ammunition_depots()
|
||||
self.generate_required_aa()
|
||||
self.generate_required_ewr()
|
||||
self.generate_scenery_sites()
|
||||
|
||||
@ -8,9 +8,15 @@ from dcs.mapping import Point
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from .. import db
|
||||
from ..data.radar_db import UNITS_WITH_RADAR
|
||||
from ..data.radar_db import (
|
||||
UNITS_WITH_RADAR,
|
||||
TRACK_RADARS,
|
||||
TELARS,
|
||||
LAUNCHER_TRACKER_PAIRS,
|
||||
)
|
||||
from ..utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -137,12 +143,11 @@ class TheaterGroundObject(MissionTarget):
|
||||
return False
|
||||
|
||||
@property
|
||||
def has_radar(self) -> bool:
|
||||
"""Returns True if the ground object contains a unit with radar."""
|
||||
def has_live_radar_sam(self) -> bool:
|
||||
"""Returns True if the ground object contains a unit with working radar SAM."""
|
||||
for group in self.groups:
|
||||
for unit in group.units:
|
||||
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
|
||||
return True
|
||||
if self.threat_range(group, radar_only=True):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
|
||||
@ -163,17 +168,16 @@ class TheaterGroundObject(MissionTarget):
|
||||
max_range = max(max_range, meters(unit_range))
|
||||
return max_range
|
||||
|
||||
def max_detection_range(self) -> Distance:
|
||||
return max(self.detection_range(g) for g in self.groups)
|
||||
|
||||
def detection_range(self, group: Group) -> Distance:
|
||||
return self._max_range_of_type(group, "detection_range")
|
||||
|
||||
def threat_range(self, group: Group) -> Distance:
|
||||
if not self.detection_range(group):
|
||||
# For simple SAMs like shilkas, the unit has both a threat and
|
||||
# detection range. For complex sites like SA-2s, the launcher has a
|
||||
# threat range and the search/track radars have detection ranges. If
|
||||
# the site has no detection range it has no radars and can't fire,
|
||||
# so it's not actually a threat even if it still has launchers.
|
||||
return meters(0)
|
||||
def max_threat_range(self) -> Distance:
|
||||
return max(self.threat_range(g) for g in self.groups)
|
||||
|
||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
||||
return self._max_range_of_type(group, "threat_range")
|
||||
|
||||
@property
|
||||
@ -452,12 +456,45 @@ class SamGroundObject(BaseDefenseGroundObject):
|
||||
|
||||
if not self.is_friendly(for_player):
|
||||
yield FlightType.DEAD
|
||||
yield FlightType.SEAD
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
||||
max_non_radar = meters(0)
|
||||
live_trs = set()
|
||||
max_telar_range = meters(0)
|
||||
launchers = set()
|
||||
for unit in group.units:
|
||||
unit_type = db.unit_type_from_name(unit.type)
|
||||
if unit_type is None or not issubclass(unit_type, VehicleType):
|
||||
continue
|
||||
if unit_type in TRACK_RADARS:
|
||||
live_trs.add(unit_type)
|
||||
elif unit_type in TELARS:
|
||||
max_telar_range = max(
|
||||
max_telar_range, meters(getattr(unit_type, "threat_range", 0))
|
||||
)
|
||||
elif unit_type in LAUNCHER_TRACKER_PAIRS:
|
||||
launchers.add(unit_type)
|
||||
else:
|
||||
max_non_radar = max(
|
||||
max_non_radar, meters(getattr(unit_type, "threat_range", 0))
|
||||
)
|
||||
max_tel_range = meters(0)
|
||||
for launcher in launchers:
|
||||
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
|
||||
max_tel_range = max(
|
||||
max_tel_range, meters(getattr(launcher, "threat_range"))
|
||||
)
|
||||
if radar_only:
|
||||
return max(max_tel_range, max_telar_range)
|
||||
else:
|
||||
return max(max_tel_range, max_telar_range, max_non_radar)
|
||||
|
||||
|
||||
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||
def __init__(
|
||||
|
||||
@ -88,9 +88,27 @@ class TransitNetwork:
|
||||
TransitConnection.Airlift: a.position.distance_to_point(b.position),
|
||||
}[self.link_type(a, b)]
|
||||
|
||||
def has_path_between(
|
||||
self,
|
||||
origin: ControlPoint,
|
||||
destination: ControlPoint,
|
||||
seen: Optional[set[ControlPoint]] = None,
|
||||
) -> bool:
|
||||
if seen is None:
|
||||
seen = set()
|
||||
seen.add(origin)
|
||||
for connection in self.connections_from(origin):
|
||||
if connection in seen:
|
||||
continue
|
||||
if connection == destination:
|
||||
return True
|
||||
if self.has_path_between(connection, destination, seen):
|
||||
return True
|
||||
return False
|
||||
|
||||
def shortest_path_between(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> List[ControlPoint]:
|
||||
) -> list[ControlPoint]:
|
||||
return self.shortest_path_with_cost(origin, destination)[0]
|
||||
|
||||
def shortest_path_with_cost(
|
||||
@ -127,7 +145,7 @@ class TransitNetwork:
|
||||
path: List[ControlPoint] = []
|
||||
while current != origin:
|
||||
path.append(current)
|
||||
previous = came_from[current]
|
||||
previous = came_from.get(current)
|
||||
if previous is None:
|
||||
raise NoPathError(origin, destination)
|
||||
current = previous
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import singledispatchmethod
|
||||
from typing import Optional, TYPE_CHECKING, Union
|
||||
from typing import Optional, TYPE_CHECKING, Union, Iterable
|
||||
|
||||
from dcs.mapping import Point as DcsPoint
|
||||
from shapely.geometry import (
|
||||
@ -13,11 +13,10 @@ from shapely.geometry import (
|
||||
from shapely.geometry.base import BaseGeometry
|
||||
from shapely.ops import nearest_points, unary_union
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from gen import Conflict
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flight import Flight, FlightWaypoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@ -27,9 +26,12 @@ ThreatPoly = Union[MultiPolygon, Polygon]
|
||||
|
||||
|
||||
class ThreatZones:
|
||||
def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None:
|
||||
def __init__(
|
||||
self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
|
||||
) -> None:
|
||||
self.airbases = airbases
|
||||
self.air_defenses = air_defenses
|
||||
self.radar_sam_threats = radar_sam_threats
|
||||
self.all = unary_union([airbases, air_defenses])
|
||||
|
||||
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
|
||||
@ -69,6 +71,13 @@ class ThreatZones:
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||
)
|
||||
|
||||
def waypoints_threatened_by_aircraft(
|
||||
self, waypoints: Iterable[FlightWaypoint]
|
||||
) -> bool:
|
||||
return self.threatened_by_aircraft(
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
|
||||
)
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_air_defense(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
@ -83,6 +92,33 @@ class ThreatZones:
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||
)
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
def _threatened_by_air_defense_mission_target(self, target: MissionTarget) -> bool:
|
||||
return self.threatened_by_air_defense(
|
||||
self.dcs_to_shapely_point(target.position)
|
||||
)
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened_by_radar_sam(self, target) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_radar_sam.register
|
||||
def _threatened_by_radar_sam_geom(self, position: BaseGeometry) -> bool:
|
||||
return self.radar_sam_threats.intersects(position)
|
||||
|
||||
@threatened_by_radar_sam.register
|
||||
def _threatened_by_radar_sam_flight(self, flight: Flight) -> bool:
|
||||
return self.threatened_by_radar_sam(
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
||||
)
|
||||
|
||||
def waypoints_threatened_by_radar_sam(
|
||||
self, waypoints: Iterable[FlightWaypoint]
|
||||
) -> bool:
|
||||
return self.threatened_by_radar_sam(
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def closest_enemy_airbase(
|
||||
cls, location: ControlPoint, max_distance: Distance
|
||||
@ -134,6 +170,7 @@ class ThreatZones:
|
||||
"""
|
||||
air_threats = []
|
||||
air_defenses = []
|
||||
radar_sam_threats = []
|
||||
for control_point in game.theater.controlpoints:
|
||||
if control_point.captured != player:
|
||||
continue
|
||||
@ -151,9 +188,16 @@ class ThreatZones:
|
||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||
threat_zone = point.buffer(threat_range.meters)
|
||||
air_defenses.append(threat_zone)
|
||||
radar_threat_range = tgo.threat_range(group, radar_only=True)
|
||||
if radar_threat_range > nautical_miles(3):
|
||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||
threat_zone = point.buffer(threat_range.meters)
|
||||
radar_sam_threats.append(threat_zone)
|
||||
|
||||
return cls(
|
||||
airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses)
|
||||
airbases=unary_union(air_threats),
|
||||
air_defenses=unary_union(air_defenses),
|
||||
radar_sam_threats=unary_union(radar_sam_threats),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@ -4,12 +4,23 @@ import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from functools import singledispatchmethod
|
||||
from typing import Dict, Generic, Iterator, List, Optional, TYPE_CHECKING, Type, TypeVar
|
||||
from typing import (
|
||||
Dict,
|
||||
Generic,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Type,
|
||||
TypeVar,
|
||||
Sequence,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.theater.transitnetwork import (
|
||||
TransitConnection,
|
||||
@ -222,17 +233,27 @@ class AirliftPlanner:
|
||||
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
for unit_type, available in inventory.all_aircraft:
|
||||
squadrons = [
|
||||
s
|
||||
for s in self.game.air_wing_for(self.for_player).squadrons_for(
|
||||
unit_type
|
||||
)
|
||||
if FlightType.TRANSPORT in s.mission_types
|
||||
]
|
||||
if not squadrons:
|
||||
continue
|
||||
squadron = squadrons[0]
|
||||
if self.compatible_with_mission(unit_type, cp):
|
||||
while available and self.transfer.transport is None:
|
||||
flight_size = self.create_airlift_flight(unit_type, inventory)
|
||||
flight_size = self.create_airlift_flight(squadron, inventory)
|
||||
available -= flight_size
|
||||
if self.package.flights:
|
||||
self.game.ato_for(self.for_player).add_package(self.package)
|
||||
|
||||
def create_airlift_flight(
|
||||
self, unit_type: Type[FlyingType], inventory: ControlPointAircraftInventory
|
||||
self, squadron: Squadron, inventory: ControlPointAircraftInventory
|
||||
) -> int:
|
||||
available = inventory.available(unit_type)
|
||||
available = inventory.available(squadron.aircraft)
|
||||
# 4 is the max flight size in DCS.
|
||||
flight_size = min(self.transfer.size, available, 4)
|
||||
|
||||
@ -241,10 +262,11 @@ class AirliftPlanner:
|
||||
else:
|
||||
transfer = self.transfer
|
||||
|
||||
player = inventory.control_point.captured
|
||||
flight = Flight(
|
||||
self.package,
|
||||
self.game.player_country,
|
||||
unit_type,
|
||||
self.game.country_for(player),
|
||||
squadron,
|
||||
flight_size,
|
||||
FlightType.TRANSPORT,
|
||||
self.game.settings.default_start_type,
|
||||
@ -363,7 +385,7 @@ class CargoShip(MultiGroupTransport):
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def route(self) -> List[Point]:
|
||||
def route(self) -> Sequence[Point]:
|
||||
return self.origin.shipping_lanes[self.destination]
|
||||
|
||||
def description(self) -> str:
|
||||
@ -518,6 +540,7 @@ class PendingTransfers:
|
||||
flight = transport.flight
|
||||
flight.package.remove_flight(flight)
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
flight.clear_roster()
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_convoy(
|
||||
@ -562,10 +585,14 @@ class PendingTransfers:
|
||||
|
||||
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||
inventory = self.game.aircraft_inventory.for_control_point(control_point)
|
||||
squadrons = self.game.air_wing_for(control_point.captured).squadrons_for_task(
|
||||
FlightType.TRANSPORT
|
||||
)
|
||||
unit_types = {s.aircraft for s in squadrons}.intersection(TRANSPORT_CAPABLE)
|
||||
return sum(
|
||||
count
|
||||
for unit_type, count in inventory.all_aircraft
|
||||
if unit_type in TRANSPORT_CAPABLE
|
||||
if unit_type in unit_types
|
||||
)
|
||||
|
||||
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
||||
|
||||
@ -139,7 +139,9 @@ class PendingUnitDeliveries:
|
||||
) -> Optional[ControlPoint]:
|
||||
sources = []
|
||||
for control_point in game.theater.control_points_for(self.destination.captured):
|
||||
if control_point.can_recruit_ground_units(game):
|
||||
if control_point.can_recruit_ground_units(
|
||||
game
|
||||
) and network.has_path_between(self.destination, control_point):
|
||||
sources.append(control_point)
|
||||
|
||||
if not sources:
|
||||
|
||||
@ -7,12 +7,19 @@ from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game import db
|
||||
from game.squadrons import Pilot
|
||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
|
||||
from game.transfers import CargoShip, Convoy, TransferOrder
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FlyingUnit:
|
||||
flight: Flight
|
||||
pilot: Pilot
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrontLineUnit:
|
||||
unit_type: Type[VehicleType]
|
||||
@ -45,7 +52,7 @@ class Building:
|
||||
|
||||
class UnitMap:
|
||||
def __init__(self) -> None:
|
||||
self.aircraft: Dict[str, Flight] = {}
|
||||
self.aircraft: Dict[str, FlyingUnit] = {}
|
||||
self.airfields: Dict[str, Airfield] = {}
|
||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
||||
@ -55,17 +62,19 @@ class UnitMap:
|
||||
self.airlifts: Dict[str, AirliftUnit] = {}
|
||||
|
||||
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
for unit in group.units:
|
||||
for pilot, unit in zip(flight.pilots, group.units):
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
if name in self.aircraft:
|
||||
raise RuntimeError(f"Duplicate unit name: {name}")
|
||||
self.aircraft[name] = flight
|
||||
if pilot is None:
|
||||
raise ValueError(f"{name} has no pilot assigned")
|
||||
self.aircraft[name] = FlyingUnit(flight, pilot)
|
||||
if flight.cargo is not None:
|
||||
self.add_airlift_units(group, flight.cargo)
|
||||
|
||||
def flight(self, unit_name: str) -> Optional[Flight]:
|
||||
def flight(self, unit_name: str) -> Optional[FlyingUnit]:
|
||||
return self.aircraft.get(unit_name, None)
|
||||
|
||||
def add_airfield(self, airfield: Airfield) -> None:
|
||||
|
||||
@ -73,4 +73,12 @@ VERSION = _build_version_string()
|
||||
#: * AAA_8_8cm_Flak_18,
|
||||
#: * SPAAA_Vulcan_M163,
|
||||
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
CAMPAIGN_FORMAT_VERSION = (4, 2)
|
||||
#:
|
||||
#: Version 5.0
|
||||
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition Depot"
|
||||
#: Warehouse object, and through trigger zone based scenery objects.
|
||||
#: * The number of alive Ammunition Depot objective buildings connected to a control point
|
||||
#: directly influences how many ground units can be supported on the front line.
|
||||
#: * The number of supported ground units at any control point is artificially capped at 50,
|
||||
#: even if the number of alive Ammunition Depot objectives can support more.
|
||||
CAMPAIGN_FORMAT_VERSION = (5, 0)
|
||||
|
||||
321
gen/aircraft.py
321
gen/aircraft.py
@ -62,23 +62,25 @@ from dcs.task import (
|
||||
OptRestrictJettison,
|
||||
OrbitAction,
|
||||
RunwayAttack,
|
||||
SEAD,
|
||||
StartCommand,
|
||||
Targets,
|
||||
Transport,
|
||||
WeaponType,
|
||||
TargetType,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
||||
from dcs.unit import Unit
|
||||
from dcs.unit import Unit, Skill
|
||||
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
from dcs.unittype import FlyingType, UnitType
|
||||
|
||||
from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.data.weapons import Pylon
|
||||
from game.db import GUN_RELIANT_AIRFRAMES
|
||||
from game.factions.faction import Faction
|
||||
from game.settings import Settings
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@ -725,6 +727,70 @@ class AircraftConflictGenerator:
|
||||
return StartType.Cold
|
||||
return StartType.Warm
|
||||
|
||||
def skill_level_for(
|
||||
self, unit: FlyingUnit, pilot: Optional[Pilot], blue: bool
|
||||
) -> Skill:
|
||||
if blue:
|
||||
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
|
||||
new_level = min(current_level + increase, len(levels) - 1)
|
||||
return levels[new_level]
|
||||
|
||||
def set_skill(self, unit: FlyingUnit, pilot: Optional[Pilot], blue: bool) -> None:
|
||||
if pilot is None or not pilot.player:
|
||||
unit.skill = self.skill_level_for(unit, pilot, blue)
|
||||
return
|
||||
|
||||
if self.use_client:
|
||||
unit.set_client()
|
||||
else:
|
||||
unit.set_player()
|
||||
|
||||
@staticmethod
|
||||
def livery_from_db(flight: Flight) -> Optional[str]:
|
||||
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type)
|
||||
|
||||
def livery_from_faction(self, flight: Flight) -> Optional[str]:
|
||||
faction = self.game.faction_for(player=flight.departure.captured)
|
||||
if (choices := faction.liveries_overrides.get(flight.unit_type)) is not None:
|
||||
return random.choice(choices)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def livery_from_squadron(flight: Flight) -> Optional[str]:
|
||||
return flight.squadron.livery
|
||||
|
||||
def livery_for(self, flight: Flight) -> Optional[str]:
|
||||
if (livery := self.livery_from_squadron(flight)) is not None:
|
||||
return livery
|
||||
if (livery := self.livery_from_faction(flight)) is not None:
|
||||
return livery
|
||||
if (livery := self.livery_from_db(flight)) is not None:
|
||||
return livery
|
||||
return None
|
||||
|
||||
def _setup_livery(self, flight: Flight, group: FlyingGroup) -> None:
|
||||
livery = self.livery_for(flight)
|
||||
if livery is None:
|
||||
return
|
||||
for unit in group.units:
|
||||
unit.livery_id = livery
|
||||
|
||||
def _setup_group(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
@ -735,34 +801,16 @@ class AircraftConflictGenerator:
|
||||
unit_type = group.units[0].unit_type
|
||||
|
||||
self._setup_payload(flight, group)
|
||||
self._setup_livery(flight, group)
|
||||
|
||||
if unit_type in db.PLANE_LIVERY_OVERRIDES:
|
||||
for unit_instance in group.units:
|
||||
unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]
|
||||
|
||||
# Override livery by faction file data
|
||||
if flight.from_cp.captured:
|
||||
faction = self.game.player_faction
|
||||
else:
|
||||
faction = self.game.enemy_faction
|
||||
|
||||
if unit_type in faction.liveries_overrides:
|
||||
livery = random.choice(faction.liveries_overrides[unit_type])
|
||||
for unit_instance in group.units:
|
||||
unit_instance.livery_id = livery
|
||||
|
||||
for idx in range(0, min(len(group.units), flight.client_count)):
|
||||
unit = group.units[idx]
|
||||
if self.use_client:
|
||||
unit.set_client()
|
||||
else:
|
||||
unit.set_player()
|
||||
|
||||
for unit, pilot in zip(group.units, flight.pilots):
|
||||
player = pilot is not None and pilot.player
|
||||
self.set_skill(unit, pilot, blue=flight.departure.captured)
|
||||
# Do not generate player group with late activation.
|
||||
if group.late_activation:
|
||||
if player and group.late_activation:
|
||||
group.late_activation = False
|
||||
|
||||
# Set up F-14 Client to have pre-stored alignement
|
||||
# Set up F-14 Client to have pre-stored alignment
|
||||
if unit_type is F_14B:
|
||||
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
|
||||
|
||||
@ -783,7 +831,7 @@ class AircraftConflictGenerator:
|
||||
self.flights.append(
|
||||
FlightData(
|
||||
package=package,
|
||||
country=faction.country,
|
||||
country=self.game.faction_for(player=flight.departure.captured).country,
|
||||
flight_type=flight.flight_type,
|
||||
units=group.units,
|
||||
size=len(group.units),
|
||||
@ -1019,7 +1067,7 @@ class AircraftConflictGenerator:
|
||||
flight = Flight(
|
||||
Package(control_point),
|
||||
faction.country,
|
||||
aircraft,
|
||||
self.game.air_wing_for(control_point.captured).squadron_for(aircraft),
|
||||
1,
|
||||
FlightType.BARCAP,
|
||||
"Cold",
|
||||
@ -1179,12 +1227,23 @@ class AircraftConflictGenerator:
|
||||
raise RuntimeError(f"No reduced fuel case for type {unit_type}")
|
||||
|
||||
@staticmethod
|
||||
def flight_always_keeps_gun(flight: Flight) -> bool:
|
||||
# Never take bullets from players. They're smart enough to know when to use it
|
||||
# and when to RTB.
|
||||
if flight.client_count > 0:
|
||||
return True
|
||||
|
||||
return flight.unit_type in GUN_RELIANT_AIRFRAMES
|
||||
|
||||
def configure_behavior(
|
||||
self,
|
||||
flight: Flight,
|
||||
group: FlyingGroup,
|
||||
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
||||
roe: Optional[OptROE.Values] = None,
|
||||
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
||||
restrict_jettison: Optional[bool] = None,
|
||||
mission_uses_gun: bool = True,
|
||||
) -> None:
|
||||
group.points[0].tasks.clear()
|
||||
if react_on_threat is not None:
|
||||
@ -1196,6 +1255,17 @@ class AircraftConflictGenerator:
|
||||
if rtb_winchester is not None:
|
||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
|
||||
|
||||
# Confiscate the bullets of AI missions that do not rely on the gun. There is no
|
||||
# "all but gun" RTB winchester option, so air to ground missions with mixed
|
||||
# weapon types will insist on using all of their bullets after running out of
|
||||
# missiles and bombs. Take away their bullets so they don't strafe a Tor.
|
||||
#
|
||||
# Exceptions are made for player flights and for airframes where the gun is
|
||||
# essential like the A-10 or warbirds.
|
||||
if not mission_uses_gun and not self.flight_always_keeps_gun(flight):
|
||||
for unit in group.units:
|
||||
unit.gun = 0
|
||||
|
||||
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
||||
# Do not restrict afterburner.
|
||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
|
||||
@ -1221,7 +1291,7 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||
|
||||
self.configure_behavior(group, rtb_winchester=ammo_type)
|
||||
self.configure_behavior(flight, group, rtb_winchester=ammo_type)
|
||||
|
||||
def configure_sweep(
|
||||
self,
|
||||
@ -1238,7 +1308,7 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||
|
||||
self.configure_behavior(group, rtb_winchester=ammo_type)
|
||||
self.configure_behavior(flight, group, rtb_winchester=ammo_type)
|
||||
|
||||
def configure_cas(
|
||||
self,
|
||||
@ -1250,6 +1320,7 @@ class AircraftConflictGenerator:
|
||||
group.task = CAS.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
@ -1273,11 +1344,13 @@ class AircraftConflictGenerator:
|
||||
group.task = CAS.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.All,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
|
||||
def configure_sead(
|
||||
@ -1287,14 +1360,21 @@ class AircraftConflictGenerator:
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> None:
|
||||
group.task = SEAD.name
|
||||
# CAS is able to perform all the same tasks as SEAD using a superset of the
|
||||
# available aircraft, and F-14s are not able to be SEAD despite having TALDs.
|
||||
# https://forums.eagle.ru/topic/272112-cannot-assign-f-14-to-sead/
|
||||
group.task = CAS.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
# ASM includes ARMs and TALDs (among other things, but those are the useful
|
||||
# weapons for SEAD).
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
|
||||
def configure_strike(
|
||||
@ -1307,10 +1387,12 @@ class AircraftConflictGenerator:
|
||||
group.task = GroundAttack.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
|
||||
def configure_anti_ship(
|
||||
@ -1323,10 +1405,12 @@ class AircraftConflictGenerator:
|
||||
group.task = AntishipStrike.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
|
||||
def configure_runway_attack(
|
||||
@ -1339,10 +1423,12 @@ class AircraftConflictGenerator:
|
||||
group.task = RunwayAttack.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
|
||||
def configure_oca_strike(
|
||||
@ -1355,6 +1441,7 @@ class AircraftConflictGenerator:
|
||||
group.task = CAS.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
@ -1380,6 +1467,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
# Awacs task action
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
@ -1401,7 +1489,30 @@ class AircraftConflictGenerator:
|
||||
group.task = CAP.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
group, roe=OptROE.Values.OpenFire, restrict_jettison=True
|
||||
flight, group, roe=OptROE.Values.OpenFire, restrict_jettison=True
|
||||
)
|
||||
|
||||
def configure_sead_escort(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> None:
|
||||
# CAS is able to perform all the same tasks as SEAD using a superset of the
|
||||
# available aircraft, and F-14s are not able to be SEAD despite having TALDs.
|
||||
# https://forums.eagle.ru/topic/272112-cannot-assign-f-14-to-sead/
|
||||
group.task = CAS.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
roe=OptROE.Values.OpenFire,
|
||||
# ASM includes ARMs and TALDs (among other things, but those are the useful
|
||||
# weapons for SEAD).
|
||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||
restrict_jettison=True,
|
||||
mission_uses_gun=False,
|
||||
)
|
||||
|
||||
def configure_transport(
|
||||
@ -1414,6 +1525,7 @@ class AircraftConflictGenerator:
|
||||
group.task = Transport.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
self.configure_behavior(
|
||||
flight,
|
||||
group,
|
||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||
roe=OptROE.Values.WeaponHold,
|
||||
@ -1422,7 +1534,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||
self.configure_behavior(group)
|
||||
self.configure_behavior(flight, group)
|
||||
|
||||
def setup_flight_group(
|
||||
self,
|
||||
@ -1448,6 +1560,8 @@ class AircraftConflictGenerator:
|
||||
self.configure_dead(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.SEAD:
|
||||
self.configure_sead(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.SEAD_ESCORT:
|
||||
self.configure_sead_escort(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.STRIKE:
|
||||
self.configure_strike(group, package, flight, dynamic_runways)
|
||||
elif flight_type == FlightType.ANTISHIP:
|
||||
@ -1695,29 +1809,32 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
||||
waypoint = super().build()
|
||||
|
||||
# TODO: Add common "UnitGroupTarget" base type.
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
group_name = target_group.group_name
|
||||
elif isinstance(target_group, MultiGroupTransport):
|
||||
group_name = target_group.name
|
||||
group_names = []
|
||||
target = self.package.target
|
||||
if isinstance(target, TheaterGroundObject):
|
||||
for group in target.groups:
|
||||
group_names.append(group.name)
|
||||
elif isinstance(target, MultiGroupTransport):
|
||||
group_names.append(target.name)
|
||||
else:
|
||||
logging.error(
|
||||
"Unexpected target type for BAI mission: %s",
|
||||
target_group.__class__.__name__,
|
||||
target.__class__.__name__,
|
||||
)
|
||||
return waypoint
|
||||
|
||||
group = self.mission.find_group(group_name)
|
||||
if group is None:
|
||||
logging.error("Could not find group for BAI mission %s", group_name)
|
||||
return waypoint
|
||||
for group_name in group_names:
|
||||
group = self.mission.find_group(group_name)
|
||||
if group is None:
|
||||
logging.error("Could not find group for BAI mission %s", group_name)
|
||||
continue
|
||||
|
||||
task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
return waypoint
|
||||
|
||||
|
||||
@ -1754,23 +1871,29 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
target_group = self.package.target
|
||||
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.Auto)
|
||||
task.params["expend"] = "All"
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
else:
|
||||
logging.error(
|
||||
f"Could not find group for DEAD mission {target_group.group_name}"
|
||||
)
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
|
||||
target = self.package.target
|
||||
if not isinstance(target, TheaterGroundObject):
|
||||
logging.error(
|
||||
"Unexpected target type for DEAD mission: %s",
|
||||
target.__class__.__name__,
|
||||
)
|
||||
return waypoint
|
||||
|
||||
for group in target.groups:
|
||||
miz_group = self.mission.find_group(group.name)
|
||||
if miz_group is None:
|
||||
logging.error(f"Could not find group for DEAD mission {group.name}")
|
||||
continue
|
||||
|
||||
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)
|
||||
task.params["expend"] = "All"
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
return waypoint
|
||||
|
||||
|
||||
@ -1822,25 +1945,29 @@ class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
|
||||
class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
target_group = self.package.target
|
||||
if isinstance(target_group, TheaterGroundObject):
|
||||
tgroup = self.mission.find_group(target_group.group_name)
|
||||
if tgroup is not None:
|
||||
waypoint.add_task(
|
||||
EngageTargetsInZone(
|
||||
position=tgroup.position,
|
||||
radius=int(nautical_miles(30).meters),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.AirDefence,
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
f"Could not find group for DEAD mission {target_group.group_name}"
|
||||
)
|
||||
self.register_special_waypoints(self.waypoint.targets)
|
||||
|
||||
target = self.package.target
|
||||
if not isinstance(target, TheaterGroundObject):
|
||||
logging.error(
|
||||
"Unexpected target type for SEAD mission: %s",
|
||||
target.__class__.__name__,
|
||||
)
|
||||
return waypoint
|
||||
|
||||
for group in target.groups:
|
||||
miz_group = self.mission.find_group(group.name)
|
||||
if miz_group is None:
|
||||
logging.error(f"Could not find group for SEAD mission {group.name}")
|
||||
continue
|
||||
|
||||
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Guided)
|
||||
task.params["expend"] = "All"
|
||||
task.params["attackQtyLimit"] = False
|
||||
task.params["directionEnabled"] = False
|
||||
task.params["altitudeEnabled"] = False
|
||||
task.params["groupAttack"] = True
|
||||
waypoint.tasks.append(task)
|
||||
return waypoint
|
||||
|
||||
|
||||
@ -1903,7 +2030,10 @@ class SweepIngressBuilder(PydcsWaypointBuilder):
|
||||
waypoint.tasks.append(
|
||||
EngageTargets(
|
||||
max_distance=int(nautical_miles(50).meters),
|
||||
targets=[Targets.All.Air.Planes.Fighters],
|
||||
targets=[
|
||||
Targets.All.Air.Planes.Fighters,
|
||||
Targets.All.Air.Planes.MultiroleFighters,
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
@ -1914,11 +2044,23 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
if self.flight.flight_type == FlightType.ESCORT:
|
||||
self.configure_escort_tasks(waypoint)
|
||||
self.configure_escort_tasks(
|
||||
waypoint,
|
||||
[
|
||||
Targets.All.Air.Planes.Fighters,
|
||||
Targets.All.Air.Planes.MultiroleFighters,
|
||||
],
|
||||
)
|
||||
elif self.flight.flight_type == FlightType.SEAD_ESCORT:
|
||||
self.configure_escort_tasks(
|
||||
waypoint, [Targets.All.GroundUnits.AirDefence.AAA.SAMRelated]
|
||||
)
|
||||
return waypoint
|
||||
|
||||
@staticmethod
|
||||
def configure_escort_tasks(waypoint: MovingPoint) -> None:
|
||||
def configure_escort_tasks(
|
||||
waypoint: MovingPoint, target_types: List[Type[TargetType]]
|
||||
) -> None:
|
||||
# Ideally we would use the escort mission type and escort task to have
|
||||
# the AI automatically but the AI only escorts AI flights while they are
|
||||
# traveling between waypoints. When an AI flight performs an attack
|
||||
@ -1944,16 +2086,13 @@ class JoinPointBuilder(PydcsWaypointBuilder):
|
||||
# for the target area that is set to end on a flag flip that occurs when
|
||||
# the strike aircraft finish their attack task.
|
||||
#
|
||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted
|
||||
# https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior
|
||||
waypoint.add_task(
|
||||
ControlledTask(
|
||||
EngageTargets(
|
||||
# TODO: From doctrine.
|
||||
max_distance=int(nautical_miles(30).meters),
|
||||
targets=[
|
||||
Targets.All.Air.Planes.Fighters,
|
||||
Targets.All.Air.Planes.MultiroleFighters,
|
||||
],
|
||||
targets=target_types,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@ -620,9 +620,9 @@ AIRFIELD_DATA = {
|
||||
tacan=TacanChannel(78, TacanBand.X),
|
||||
tacan_callsign="BND",
|
||||
vor=("BND", MHz(117, 200)),
|
||||
atc=AtcData(MHz(4, 250), MHz(39, 401), MHz(118, 100), MHz(251, 0)),
|
||||
atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(118, 100), MHz(251, 0)),
|
||||
ils={
|
||||
"21": ("IBND", MHz(333, 800)),
|
||||
"21": ("IBND", MHz(109, 900)),
|
||||
},
|
||||
),
|
||||
"Jiroft": AirfieldData(
|
||||
|
||||
@ -67,6 +67,10 @@ class Package:
|
||||
|
||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||
|
||||
@property
|
||||
def has_players(self) -> bool:
|
||||
return any(flight.client_count for flight in self.flights)
|
||||
|
||||
@property
|
||||
def formation_speed(self) -> Optional[Speed]:
|
||||
"""The speed of the package when in formation.
|
||||
|
||||
@ -17,12 +17,17 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.infos.information import Information
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.profiling import logged_duration, MultiEventTracer
|
||||
from game.squadrons import AirWing, Squadron
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@ -40,8 +45,7 @@ from game.theater.theatergroundobject import (
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from game.transfers import CargoShip, Convoy
|
||||
from game.utils import Distance, nautical_miles
|
||||
from gen import Conflict
|
||||
from game.utils import Distance, nautical_miles, meters
|
||||
from gen.ato import Package
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import (
|
||||
@ -109,6 +113,8 @@ class ProposedMission:
|
||||
#: The proposed flights that are required for the mission.
|
||||
flights: List[ProposedFlight]
|
||||
|
||||
asap: bool = field(default=False)
|
||||
|
||||
def __str__(self) -> str:
|
||||
flights = ", ".join([str(f) for f in self.flights])
|
||||
return f"{self.location.name}: {flights}"
|
||||
@ -119,17 +125,19 @@ class AircraftAllocator:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
air_wing: AirWing,
|
||||
closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
is_player: bool,
|
||||
) -> None:
|
||||
self.air_wing = air_wing
|
||||
self.closest_airfields = closest_airfields
|
||||
self.global_inventory = global_inventory
|
||||
self.is_player = is_player
|
||||
|
||||
def find_aircraft_for_flight(
|
||||
def find_squadron_for_flight(
|
||||
self, flight: ProposedFlight
|
||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||
"""Finds aircraft suitable for the given mission.
|
||||
|
||||
Searches for aircraft capable of performing the given mission within the
|
||||
@ -148,16 +156,18 @@ class AircraftAllocator:
|
||||
on subsequent calls. If the found aircraft are not used, the caller is
|
||||
responsible for returning them to the inventory.
|
||||
"""
|
||||
return self.find_aircraft_of_type(flight, aircraft_for_task(flight.task))
|
||||
return self.find_aircraft_for_task(flight, flight.task)
|
||||
|
||||
def find_aircraft_of_type(
|
||||
self,
|
||||
flight: ProposedFlight,
|
||||
types: List[Type[FlyingType]],
|
||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
||||
def find_aircraft_for_task(
|
||||
self, flight: ProposedFlight, task: FlightType
|
||||
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||
types = aircraft_for_task(task)
|
||||
airfields_in_range = self.closest_airfields.airfields_within(
|
||||
flight.max_distance
|
||||
)
|
||||
|
||||
# Prefer using squadrons with pilots first
|
||||
best_understaffed: Optional[Tuple[ControlPoint, Squadron]] = None
|
||||
for airfield in airfields_in_range:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
continue
|
||||
@ -165,11 +175,28 @@ class AircraftAllocator:
|
||||
for aircraft in types:
|
||||
if not airfield.can_operate(aircraft):
|
||||
continue
|
||||
if inventory.available(aircraft) >= flight.num_aircraft:
|
||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||
return airfield, aircraft
|
||||
if inventory.available(aircraft) < flight.num_aircraft:
|
||||
continue
|
||||
# Valid location with enough aircraft available. Find a squadron to fit
|
||||
# the role.
|
||||
for squadron in self.air_wing.squadrons_for(aircraft):
|
||||
if task not in squadron.mission_types:
|
||||
continue
|
||||
if len(squadron.available_pilots) >= flight.num_aircraft:
|
||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||
return airfield, squadron
|
||||
|
||||
return None
|
||||
# A compatible squadron that doesn't have enough pilots. Remember it
|
||||
# as a fallback in case we find no better choices.
|
||||
if best_understaffed is None:
|
||||
best_understaffed = airfield, squadron
|
||||
|
||||
if best_understaffed is not None:
|
||||
airfield, squadron = best_understaffed
|
||||
self.global_inventory.for_control_point(airfield).remove_aircraft(
|
||||
squadron.aircraft, flight.num_aircraft
|
||||
)
|
||||
return best_understaffed
|
||||
|
||||
|
||||
class PackageBuilder:
|
||||
@ -180,16 +207,18 @@ class PackageBuilder:
|
||||
location: MissionTarget,
|
||||
closest_airfields: ClosestAirfields,
|
||||
global_inventory: GlobalAircraftInventory,
|
||||
air_wing: AirWing,
|
||||
is_player: bool,
|
||||
package_country: str,
|
||||
start_type: str,
|
||||
asap: bool,
|
||||
) -> None:
|
||||
self.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
self.package_country = package_country
|
||||
self.package = Package(location)
|
||||
self.package = Package(location, auto_asap=asap)
|
||||
self.allocator = AircraftAllocator(
|
||||
closest_airfields, global_inventory, is_player
|
||||
air_wing, closest_airfields, global_inventory, is_player
|
||||
)
|
||||
self.global_inventory = global_inventory
|
||||
self.start_type = start_type
|
||||
@ -202,10 +231,10 @@ class PackageBuilder:
|
||||
caller should return any previously planned flights to the inventory
|
||||
using release_planned_aircraft.
|
||||
"""
|
||||
assignment = self.allocator.find_aircraft_for_flight(plan)
|
||||
assignment = self.allocator.find_squadron_for_flight(plan)
|
||||
if assignment is None:
|
||||
return False
|
||||
airfield, aircraft = assignment
|
||||
airfield, squadron = assignment
|
||||
if isinstance(airfield, OffMapSpawn):
|
||||
start_type = "In Flight"
|
||||
else:
|
||||
@ -214,13 +243,13 @@ class PackageBuilder:
|
||||
flight = Flight(
|
||||
self.package,
|
||||
self.package_country,
|
||||
aircraft,
|
||||
squadron,
|
||||
plan.num_aircraft,
|
||||
plan.task,
|
||||
start_type,
|
||||
departure=airfield,
|
||||
arrival=airfield,
|
||||
divert=self.find_divert_field(aircraft, airfield),
|
||||
divert=self.find_divert_field(squadron.aircraft, airfield),
|
||||
)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
@ -250,9 +279,13 @@ class PackageBuilder:
|
||||
flights = list(self.package.flights)
|
||||
for flight in flights:
|
||||
self.global_inventory.return_from_flight(flight)
|
||||
flight.clear_roster()
|
||||
self.package.remove_flight(flight)
|
||||
|
||||
|
||||
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
||||
|
||||
|
||||
class ObjectiveFinder:
|
||||
"""Identifies potential objectives for the mission planner."""
|
||||
|
||||
@ -264,41 +297,53 @@ class ObjectiveFinder:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
|
||||
def enemy_sams(self) -> Iterator[TheaterGroundObject]:
|
||||
def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject, Distance]]:
|
||||
"""Iterates over all enemy SAM sites."""
|
||||
# Control points might have the same ground object several times, for
|
||||
# some reason.
|
||||
found_targets: Set[str] = set()
|
||||
doctrine = self.game.faction_for(self.is_player).doctrine
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
for cp in self.enemy_control_points():
|
||||
for ground_object in cp.ground_objects:
|
||||
is_ewr = isinstance(ground_object, EwrGroundObject)
|
||||
is_sam = isinstance(ground_object, SamGroundObject)
|
||||
if not is_ewr and not is_sam:
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
if ground_object.name in found_targets:
|
||||
if isinstance(ground_object, EwrGroundObject):
|
||||
if threat_zones.threatened_by_air_defense(ground_object):
|
||||
# This is a very weak heuristic for determining whether the EWR
|
||||
# is close enough to be worth targeting before a SAM that is
|
||||
# covering it. Ingress distance corresponds to the beginning of
|
||||
# the attack range and is sufficient for most standoff weapons,
|
||||
# so treating the ingress distance as the threat distance sorts
|
||||
# these EWRs such that they will be attacked before SAMs that do
|
||||
# not threaten the ingress point, but after those that do.
|
||||
target_range = doctrine.ingress_egress_distance
|
||||
else:
|
||||
# But if the EWR isn't covered then we should only be worrying
|
||||
# about its detection range.
|
||||
target_range = ground_object.max_detection_range()
|
||||
elif isinstance(ground_object, SamGroundObject):
|
||||
target_range = ground_object.max_threat_range()
|
||||
else:
|
||||
continue
|
||||
|
||||
if not ground_object.has_radar:
|
||||
continue
|
||||
yield ground_object, target_range
|
||||
|
||||
# TODO: Yield in order of most threatening.
|
||||
# Need to sort in order of how close their defensive range comes
|
||||
# to friendly assets. To do that we need to add effective range
|
||||
# information to the database.
|
||||
yield ground_object
|
||||
found_targets.add(ground_object.name)
|
||||
|
||||
def threatening_sams(self) -> Iterator[MissionTarget]:
|
||||
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject]:
|
||||
"""Iterates over enemy SAMs in threat range of friendly control points.
|
||||
|
||||
SAM sites are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
return self._targets_by_range(self.enemy_sams())
|
||||
|
||||
target_ranges: list[tuple[TheaterGroundObject, Distance]] = []
|
||||
for target, threat_range in self.enemy_air_defenses():
|
||||
ranges: list[Distance] = []
|
||||
for cp in self.friendly_control_points():
|
||||
ranges.append(meters(target.distance_to(cp)) - threat_range)
|
||||
target_ranges.append((target, min(ranges)))
|
||||
|
||||
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
|
||||
for target, _range in target_ranges:
|
||||
yield target
|
||||
|
||||
def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]:
|
||||
"""Iterates over all enemy vehicle groups."""
|
||||
@ -340,9 +385,9 @@ class ObjectiveFinder:
|
||||
return self._targets_by_range(self.enemy_ships())
|
||||
|
||||
def _targets_by_range(
|
||||
self, targets: Iterable[MissionTarget]
|
||||
) -> Iterator[MissionTarget]:
|
||||
target_ranges: List[Tuple[MissionTarget, int]] = []
|
||||
self, targets: Iterable[MissionTargetType]
|
||||
) -> Iterator[MissionTargetType]:
|
||||
target_ranges: List[Tuple[MissionTargetType, int]] = []
|
||||
for target in targets:
|
||||
ranges: List[int] = []
|
||||
for cp in self.friendly_control_points():
|
||||
@ -551,15 +596,20 @@ class CoalitionMissionPlanner:
|
||||
self.procurement_requests = self.game.procurement_requests_for(self.is_player)
|
||||
self.faction = self.game.faction_for(self.is_player)
|
||||
|
||||
def faction_can_plan(self, mission_type: FlightType) -> bool:
|
||||
"""Returns True if it is possible for the faction to plan this mission type.
|
||||
def air_wing_can_plan(self, mission_type: FlightType) -> bool:
|
||||
"""Returns True if it is possible for the air wing to plan this mission type.
|
||||
|
||||
Not all mission types can be fulfilled by all factions. Many factions do not
|
||||
have AEW&C aircraft, so they will never be able to plan those missions.
|
||||
Not all mission types can be fulfilled by all air wings. Many factions do not
|
||||
have AEW&C aircraft, so they will never be able to plan those missions. It's
|
||||
also possible for the player to exclude mission types from their squadron
|
||||
designs.
|
||||
"""
|
||||
all_compatible = aircraft_for_task(mission_type)
|
||||
for aircraft in self.faction.aircrafts:
|
||||
if aircraft in all_compatible:
|
||||
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
|
||||
if (
|
||||
squadron.aircraft in all_compatible
|
||||
and mission_type in squadron.mission_types
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -578,7 +628,10 @@ class CoalitionMissionPlanner:
|
||||
cp = self.objective_finder.farthest_friendly_control_point()
|
||||
if cp is not None:
|
||||
yield ProposedMission(
|
||||
cp, [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)]
|
||||
cp,
|
||||
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
|
||||
# Supports all the early CAP flights, so should be in the air ASAP.
|
||||
asap=True,
|
||||
)
|
||||
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
@ -630,17 +683,36 @@ class CoalitionMissionPlanner:
|
||||
# or objects, plan DEAD.
|
||||
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
||||
# friendly CPs, front, lines, or objects, plan DEAD.
|
||||
for sam in self.objective_finder.threatening_sams():
|
||||
yield ProposedMission(
|
||||
sam,
|
||||
[
|
||||
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
|
||||
# TODO: Max escort range.
|
||||
for sam in self.objective_finder.threatening_air_defenses():
|
||||
flights = [ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE)]
|
||||
|
||||
# Only include SEAD against SAMs that still have emitters. No need to
|
||||
# suppress an EWR, and SEAD isn't useful against a SAM that no longer has a
|
||||
# working track radar.
|
||||
#
|
||||
# For SAMs without track radars and EWRs, we still want a SEAD escort if
|
||||
# needed.
|
||||
#
|
||||
# Note that there is a quirk here: we should potentially be included a SEAD
|
||||
# escort *and* SEAD when the target is a radar SAM but the flight path is
|
||||
# also threatened by SAMs. We don't want to include a SEAD escort if the
|
||||
# package is *only* threatened by the target though. Could be improved, but
|
||||
# needs a decent refactor to the escort planning to do so.
|
||||
if sam.has_live_radar_sam:
|
||||
flights.append(ProposedFlight(FlightType.SEAD, 2, self.MAX_SEAD_RANGE))
|
||||
else:
|
||||
flights.append(
|
||||
ProposedFlight(
|
||||
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
|
||||
),
|
||||
],
|
||||
FlightType.SEAD_ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.Sead
|
||||
)
|
||||
)
|
||||
# TODO: Max escort range.
|
||||
flights.append(
|
||||
ProposedFlight(
|
||||
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
|
||||
)
|
||||
)
|
||||
yield ProposedMission(sam, flights)
|
||||
|
||||
# These will only rarely get planned. When a convoy is travelling multiple legs,
|
||||
# they're targetable after the first leg. The reason for this is that
|
||||
@ -665,7 +737,7 @@ class CoalitionMissionPlanner:
|
||||
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
|
||||
),
|
||||
ProposedFlight(
|
||||
FlightType.SEAD, 2, self.MAX_BAI_RANGE, EscortType.Sead
|
||||
FlightType.SEAD_ESCORT, 2, self.MAX_BAI_RANGE, EscortType.Sead
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -680,7 +752,7 @@ class CoalitionMissionPlanner:
|
||||
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
|
||||
),
|
||||
ProposedFlight(
|
||||
FlightType.SEAD, 2, self.MAX_BAI_RANGE, EscortType.Sead
|
||||
FlightType.SEAD_ESCORT, 2, self.MAX_BAI_RANGE, EscortType.Sead
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -710,7 +782,7 @@ class CoalitionMissionPlanner:
|
||||
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
|
||||
),
|
||||
ProposedFlight(
|
||||
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
|
||||
FlightType.SEAD_ESCORT, 2, self.MAX_OCA_RANGE, EscortType.Sead
|
||||
),
|
||||
],
|
||||
)
|
||||
@ -732,7 +804,7 @@ class CoalitionMissionPlanner:
|
||||
FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
|
||||
),
|
||||
ProposedFlight(
|
||||
FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
|
||||
FlightType.SEAD_ESCORT, 2, self.MAX_OCA_RANGE, EscortType.Sead
|
||||
),
|
||||
]
|
||||
)
|
||||
@ -749,20 +821,29 @@ class CoalitionMissionPlanner:
|
||||
FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
|
||||
),
|
||||
ProposedFlight(
|
||||
FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, EscortType.Sead
|
||||
FlightType.SEAD_ESCORT,
|
||||
2,
|
||||
self.MAX_STRIKE_RANGE,
|
||||
EscortType.Sead,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
"""Identifies and plans mission for the turn."""
|
||||
for proposed_mission in self.propose_missions():
|
||||
self.plan_mission(proposed_mission)
|
||||
player = "Blue" if self.is_player else "Red"
|
||||
with logged_duration(f"{player} mission identification and fulfillment"):
|
||||
with MultiEventTracer() as tracer:
|
||||
for proposed_mission in self.propose_missions():
|
||||
self.plan_mission(proposed_mission, tracer)
|
||||
|
||||
for critical_mission in self.critical_missions():
|
||||
self.plan_mission(critical_mission, reserves=True)
|
||||
with logged_duration(f"{player} reserve mission planning"):
|
||||
with MultiEventTracer() as tracer:
|
||||
for critical_mission in self.critical_missions():
|
||||
self.plan_mission(critical_mission, tracer, reserves=True)
|
||||
|
||||
self.stagger_missions()
|
||||
with logged_duration(f"{player} mission scheduling"):
|
||||
self.stagger_missions()
|
||||
|
||||
for cp in self.objective_finder.friendly_control_points():
|
||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||
@ -817,27 +898,29 @@ class CoalitionMissionPlanner:
|
||||
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
||||
threats = defaultdict(bool)
|
||||
for flight in builder.package.flights:
|
||||
if self.threat_zones.threatened_by_aircraft(flight):
|
||||
if self.threat_zones.waypoints_threatened_by_aircraft(
|
||||
flight.flight_plan.escorted_waypoints()
|
||||
):
|
||||
threats[EscortType.AirToAir] = True
|
||||
if self.threat_zones.threatened_by_air_defense(flight):
|
||||
if self.threat_zones.waypoints_threatened_by_radar_sam(
|
||||
list(flight.flight_plan.escorted_waypoints())
|
||||
):
|
||||
threats[EscortType.Sead] = True
|
||||
return threats
|
||||
|
||||
def plan_mission(self, mission: ProposedMission, reserves: bool = False) -> None:
|
||||
def plan_mission(
|
||||
self, mission: ProposedMission, tracer: MultiEventTracer, reserves: bool = False
|
||||
) -> None:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
|
||||
if self.is_player:
|
||||
package_country = self.game.player_country
|
||||
else:
|
||||
package_country = self.game.enemy_country
|
||||
|
||||
builder = PackageBuilder(
|
||||
mission.location,
|
||||
self.objective_finder.closest_airfields_to(mission.location),
|
||||
self.game.aircraft_inventory,
|
||||
self.game.air_wing_for(self.is_player),
|
||||
self.is_player,
|
||||
package_country,
|
||||
self.game.country_for(self.is_player),
|
||||
self.game.settings.default_start_type,
|
||||
mission.asap,
|
||||
)
|
||||
|
||||
# Attempt to plan all the main elements of the mission first. Escorts
|
||||
@ -846,17 +929,20 @@ class CoalitionMissionPlanner:
|
||||
missing_types: Set[FlightType] = set()
|
||||
escorts = []
|
||||
for proposed_flight in mission.flights:
|
||||
if not self.faction_can_plan(proposed_flight.task):
|
||||
# This faction can never plan this mission type because they do not have
|
||||
# compatible aircraft. Skip fulfillment so that we don't place the
|
||||
# purchase request.
|
||||
if not self.air_wing_can_plan(proposed_flight.task):
|
||||
# This air wing can never plan this mission type because they do not
|
||||
# have compatible aircraft or squadrons. Skip fulfillment so that we
|
||||
# don't place the purchase request.
|
||||
continue
|
||||
if proposed_flight.escort_type is not None:
|
||||
# Escorts are planned after the primary elements of the package.
|
||||
# If the package does not need escorts they may be pruned.
|
||||
escorts.append(proposed_flight)
|
||||
continue
|
||||
self.plan_flight(mission, proposed_flight, builder, missing_types, reserves)
|
||||
with tracer.trace("Flight planning"):
|
||||
self.plan_flight(
|
||||
mission, proposed_flight, builder, missing_types, reserves
|
||||
)
|
||||
|
||||
if missing_types:
|
||||
self.scrub_mission_missing_aircraft(
|
||||
@ -880,7 +966,8 @@ class CoalitionMissionPlanner:
|
||||
self.game, builder.package, self.is_player
|
||||
)
|
||||
for flight in builder.package.flights:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
with tracer.trace("Flight plan population"):
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
|
||||
needed_escorts = self.check_needed_escorts(builder)
|
||||
for escort in escorts:
|
||||
@ -888,7 +975,8 @@ class CoalitionMissionPlanner:
|
||||
# impossible.
|
||||
assert escort.escort_type is not None
|
||||
if needed_escorts[escort.escort_type]:
|
||||
self.plan_flight(mission, escort, builder, missing_types, reserves)
|
||||
with tracer.trace("Flight planning"):
|
||||
self.plan_flight(mission, escort, builder, missing_types, reserves)
|
||||
|
||||
# Check again for unavailable aircraft. If the escort was required and
|
||||
# none were found, scrub the mission.
|
||||
@ -908,7 +996,13 @@ class CoalitionMissionPlanner:
|
||||
# Add flight plans for escorts.
|
||||
for flight in package.flights:
|
||||
if not flight.flight_plan.waypoints:
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
with tracer.trace("Flight plan population"):
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
|
||||
if package.has_players and self.game.settings.auto_ato_player_missions_asap:
|
||||
package.auto_asap = True
|
||||
package.set_tot_asap()
|
||||
|
||||
self.ato.add_package(package)
|
||||
|
||||
def stagger_missions(self) -> None:
|
||||
@ -956,6 +1050,8 @@ class CoalitionMissionPlanner:
|
||||
logging.error(f"Could not determine mission end time for {package}")
|
||||
continue
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
elif package.auto_asap:
|
||||
package.set_tot_asap()
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
|
||||
@ -230,7 +230,7 @@ CAS_CAPABLE = [
|
||||
]
|
||||
|
||||
|
||||
# Aircraft used for SEAD tasks. Must be capable of the SEAD DCS task.
|
||||
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
|
||||
SEAD_CAPABLE = [
|
||||
JF_17,
|
||||
F_16C_50,
|
||||
@ -240,6 +240,8 @@ SEAD_CAPABLE = [
|
||||
Su_25TM,
|
||||
F_4E,
|
||||
A_4E_C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
AV8BNA,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
@ -394,7 +396,7 @@ AEWC_CAPABLE = [
|
||||
|
||||
|
||||
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
elif task == FlightType.ANTISHIP:
|
||||
@ -405,6 +407,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
return CAS_CAPABLE
|
||||
elif task == FlightType.SEAD:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.SEAD_ESCORT:
|
||||
return SEAD_CAPABLE
|
||||
elif task == FlightType.DEAD:
|
||||
return DEAD_CAPABLE
|
||||
elif task == FlightType.OCA_AIRCRAFT:
|
||||
@ -422,3 +426,11 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
||||
else:
|
||||
logging.error(f"Unplannable flight type: {task}")
|
||||
return []
|
||||
|
||||
|
||||
def tasks_for_aircraft(aircraft: Type[FlyingType]) -> list[FlightType]:
|
||||
tasks = []
|
||||
for task in FlightType:
|
||||
if aircraft in aircraft_for_task(task):
|
||||
tasks.append(task)
|
||||
return tasks
|
||||
|
||||
@ -10,6 +10,7 @@ from dcs.unit import Unit
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
from game.utils import Distance, meters
|
||||
from gen.flights.loadouts import Loadout
|
||||
@ -28,6 +29,27 @@ class FlightType(Enum):
|
||||
These values are persisted to the save game as well since they are a part of
|
||||
each flight and thus a part of the ATO, so changing these values will break
|
||||
save compat.
|
||||
|
||||
When adding new mission types to this list, you will also need to update:
|
||||
|
||||
* flightplan.py: Add waypoint population in generate_flight_plan. Add a new flight
|
||||
plan type if necessary, though most are a subclass of StrikeFlightPlan.
|
||||
* aircraft.py: Add a configuration method and call it in setup_flight_group. This is
|
||||
responsible for configuring waypoint 0 actions like setting ROE, threat reaction,
|
||||
and mission abort parameters (winchester, bingo, etc).
|
||||
* Implementations of MissionTarget.mission_types: A mission type can only be planned
|
||||
against compatible targets. The mission_types method of each target class defines
|
||||
which missions may target it.
|
||||
* ai_flight_planner_db.py: Add the new mission type to aircraft_for_task that
|
||||
returns the list of compatible aircraft in order of preference.
|
||||
|
||||
You may also need to update:
|
||||
|
||||
* flight.py: Add a new waypoint type if necessary. Most mission types will need
|
||||
these, as aircraft.py uses the ingress point type to specialize AI tasks, and non-
|
||||
strike-like missions will need more specialized control.
|
||||
* ai_flight_planner.py: Use the new mission type in propose_missions so the AI will
|
||||
plan the new mission type.
|
||||
"""
|
||||
|
||||
TARCAP = "TARCAP"
|
||||
@ -45,12 +67,34 @@ class FlightType(Enum):
|
||||
OCA_AIRCRAFT = "OCA/Aircraft"
|
||||
AEWC = "AEW&C"
|
||||
TRANSPORT = "Transport"
|
||||
SEAD_ESCORT = "SEAD Escort"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def from_name(cls, name: str) -> FlightType:
|
||||
for entry in cls:
|
||||
if name == entry.value:
|
||||
return entry
|
||||
raise KeyError(f"No FlightType with name {name}")
|
||||
|
||||
|
||||
class FlightWaypointType(Enum):
|
||||
"""Enumeration of waypoint types.
|
||||
|
||||
The value of the enum has no meaning but should remain stable to prevent breaking
|
||||
save game compatibility.
|
||||
|
||||
When adding a new waypoint type, you will also need to update:
|
||||
|
||||
* waypointbuilder.py: Add a builder to simplify construction of the new waypoint
|
||||
type unless the new waypoint type will be a parameter to an existing builder
|
||||
method (such as how escort ingress waypoints work).
|
||||
* aircraft.py: Associate AI actions with the new waypoint type by subclassing
|
||||
PydcsWaypointBuilder and using it in PydcsWaypointBuilder.for_waypoint.
|
||||
"""
|
||||
|
||||
TAKEOFF = 0 # Take off point
|
||||
ASCEND_POINT = 1 # Ascension point after take off
|
||||
PATROL = 2 # Patrol point
|
||||
@ -65,7 +109,7 @@ class FlightWaypointType(Enum):
|
||||
LANDING_POINT = 11 # Should land there
|
||||
TARGET_POINT = 12 # A target building or static object, position
|
||||
TARGET_GROUP_LOC = 13 # A target group approximate location
|
||||
TARGET_SHIP = 14 # A target ship known location
|
||||
TARGET_SHIP = 14 # Unused.
|
||||
CUSTOM = 15 # User waypoint (no specific behaviour)
|
||||
JOIN = 16
|
||||
SPLIT = 17
|
||||
@ -163,7 +207,7 @@ class Flight:
|
||||
self,
|
||||
package: Package,
|
||||
country: str,
|
||||
unit_type: Type[FlyingType],
|
||||
squadron: Squadron,
|
||||
count: int,
|
||||
flight_type: FlightType,
|
||||
start_type: str,
|
||||
@ -175,8 +219,8 @@ class Flight:
|
||||
) -> None:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.unit_type = unit_type
|
||||
self.count = count
|
||||
self.squadron = squadron
|
||||
self.pilots = [squadron.claim_available_pilot() for _ in range(count)]
|
||||
self.departure = departure
|
||||
self.arrival = arrival
|
||||
self.divert = divert
|
||||
@ -186,7 +230,6 @@ class Flight:
|
||||
self.loadout = Loadout.default_for(self)
|
||||
self.start_type = start_type
|
||||
self.use_custom_loadout = False
|
||||
self.client_count = 0
|
||||
self.custom_name = custom_name
|
||||
|
||||
# Only used by transport missions.
|
||||
@ -201,6 +244,18 @@ class Flight:
|
||||
package=package, flight=self, custom_waypoints=[]
|
||||
)
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
return len(self.pilots)
|
||||
|
||||
@property
|
||||
def client_count(self) -> int:
|
||||
return len([p for p in self.pilots if p is not None and p.player])
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Type[FlyingType]:
|
||||
return self.squadron.aircraft
|
||||
|
||||
@property
|
||||
def from_cp(self) -> ControlPoint:
|
||||
return self.departure
|
||||
@ -209,6 +264,34 @@ class Flight:
|
||||
def points(self) -> List[FlightWaypoint]:
|
||||
return self.flight_plan.waypoints[1:]
|
||||
|
||||
def resize(self, new_size: int) -> None:
|
||||
if self.count > new_size:
|
||||
self.squadron.return_pilots(
|
||||
p for p in self.pilots[new_size:] if p is not None
|
||||
)
|
||||
self.pilots = self.pilots[:new_size]
|
||||
return
|
||||
self.pilots.extend(
|
||||
[
|
||||
self.squadron.claim_available_pilot()
|
||||
for _ in range(new_size - self.count)
|
||||
]
|
||||
)
|
||||
|
||||
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
|
||||
if pilot is not None:
|
||||
self.squadron.claim_pilot(pilot)
|
||||
if (current_pilot := self.pilots[index]) is not None:
|
||||
self.squadron.return_pilot(current_pilot)
|
||||
self.pilots[index] = pilot
|
||||
|
||||
@property
|
||||
def missing_pilots(self) -> int:
|
||||
return len([p for p in self.pilots if p is None])
|
||||
|
||||
def clear_roster(self) -> None:
|
||||
self.squadron.return_pilots([p for p in self.pilots if p is not None])
|
||||
|
||||
def __repr__(self):
|
||||
name = db.unit_type_name(self.unit_type)
|
||||
if self.custom_name:
|
||||
|
||||
@ -10,18 +10,18 @@ from __future__ import annotations
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
|
||||
from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.squadrons import Pilot
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@ -125,6 +125,10 @@ class FlightPlan:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def tot(self) -> timedelta:
|
||||
return self.package.time_over_target + self.tot_offset
|
||||
|
||||
@cached_property
|
||||
def bingo_fuel(self) -> int:
|
||||
"""Bingo fuel value for the FlightPlan"""
|
||||
@ -198,15 +202,28 @@ class FlightPlan:
|
||||
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
|
||||
return None
|
||||
|
||||
def escorted_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
begin = self.request_escort_at()
|
||||
end = self.dismiss_escort_at()
|
||||
if begin is None or end is None:
|
||||
return
|
||||
escorting = False
|
||||
for waypoint in self.waypoints:
|
||||
if waypoint == begin:
|
||||
escorting = True
|
||||
if escorting:
|
||||
yield waypoint
|
||||
if waypoint == end:
|
||||
return
|
||||
|
||||
def takeoff_time(self) -> Optional[timedelta]:
|
||||
tot_waypoint = self.tot_waypoint
|
||||
if tot_waypoint is None:
|
||||
return None
|
||||
|
||||
time = self.tot_for_waypoint(tot_waypoint)
|
||||
time = self.tot
|
||||
if time is None:
|
||||
return None
|
||||
time += self.tot_offset
|
||||
return time - self._travel_time_to_waypoint(tot_waypoint)
|
||||
|
||||
def startup_time(self) -> Optional[timedelta]:
|
||||
@ -243,7 +260,7 @@ class FlightPlan:
|
||||
if self.flight.from_cp.is_fleet:
|
||||
return timedelta(minutes=2)
|
||||
else:
|
||||
return timedelta(minutes=5)
|
||||
return timedelta(minutes=8)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
@ -506,7 +523,7 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
||||
start = self.package.escort_start_time
|
||||
if start is not None:
|
||||
return start + self.tot_offset
|
||||
return super().patrol_start_time + self.tot_offset
|
||||
return self.tot
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> timedelta:
|
||||
@ -530,6 +547,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
land: FlightWaypoint
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
lead_time: timedelta = field(default_factory=timedelta)
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
@ -568,6 +586,13 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.targets[0]
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
try:
|
||||
return -self.lead_time
|
||||
except AttributeError:
|
||||
return timedelta()
|
||||
|
||||
@property
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
@ -600,10 +625,6 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
)
|
||||
return total
|
||||
|
||||
@property
|
||||
def mission_speed(self) -> Speed:
|
||||
return GroundSpeed.for_flight(self.flight, self.ingress.alt)
|
||||
|
||||
@property
|
||||
def join_time(self) -> timedelta:
|
||||
travel_time = self.travel_time_between_waypoints(self.join, self.ingress)
|
||||
@ -616,7 +637,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> timedelta:
|
||||
tot = self.package.time_over_target
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.ingress, self.target_area_waypoint
|
||||
)
|
||||
@ -624,7 +645,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
|
||||
@property
|
||||
def egress_time(self) -> timedelta:
|
||||
tot = self.package.time_over_target
|
||||
tot = self.tot
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.target_area_waypoint, self.egress
|
||||
)
|
||||
@ -636,7 +657,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
||||
elif waypoint == self.egress:
|
||||
return self.egress_time
|
||||
elif waypoint in self.targets:
|
||||
return self.package.time_over_target
|
||||
return self.tot
|
||||
return super().tot_for_waypoint(waypoint)
|
||||
|
||||
|
||||
@ -681,7 +702,7 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
|
||||
@property
|
||||
def sweep_end_time(self) -> timedelta:
|
||||
return self.package.time_over_target + self.tot_offset
|
||||
return self.tot
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||
if waypoint == self.sweep_start:
|
||||
@ -837,11 +858,7 @@ class FlightPlanBuilder:
|
||||
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.doctrine
|
||||
self.doctrine: Doctrine = self.game.faction_for(self.is_player).doctrine
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def populate_flight_plan(
|
||||
@ -853,12 +870,12 @@ class FlightPlanBuilder:
|
||||
"""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")
|
||||
if self.package.waypoints is None:
|
||||
self.regenerate_package_waypoints()
|
||||
|
||||
from game.navmesh import NavMeshError
|
||||
|
||||
try:
|
||||
if self.package.waypoints is None:
|
||||
self.regenerate_package_waypoints()
|
||||
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
|
||||
except NavMeshError as ex:
|
||||
color = "blue" if self.is_player else "red"
|
||||
@ -890,6 +907,8 @@ class FlightPlanBuilder:
|
||||
return self.generate_runway_attack(flight)
|
||||
elif task == FlightType.SEAD:
|
||||
return self.generate_sead(flight, custom_targets)
|
||||
elif task == FlightType.SEAD_ESCORT:
|
||||
return self.generate_escort(flight)
|
||||
elif task == FlightType.STRIKE:
|
||||
return self.generate_strike(flight)
|
||||
elif task == FlightType.SWEEP:
|
||||
@ -1501,7 +1520,11 @@ class FlightPlanBuilder:
|
||||
targets.append(StrikeTarget(location.name, target))
|
||||
|
||||
return self.strike_flightplan(
|
||||
flight, location, FlightWaypointType.INGRESS_SEAD, targets
|
||||
flight,
|
||||
location,
|
||||
FlightWaypointType.INGRESS_SEAD,
|
||||
targets,
|
||||
lead_time=timedelta(minutes=1),
|
||||
)
|
||||
|
||||
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
|
||||
@ -1679,6 +1702,7 @@ class FlightPlanBuilder:
|
||||
location: MissionTarget,
|
||||
ingress_type: FlightWaypointType,
|
||||
targets: Optional[List[StrikeTarget]] = None,
|
||||
lead_time: timedelta = timedelta(),
|
||||
) -> StrikeFlightPlan:
|
||||
assert self.package.waypoints is not None
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
|
||||
@ -1718,6 +1742,7 @@ class FlightPlanBuilder:
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
lead_time=lead_time,
|
||||
)
|
||||
|
||||
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
|
||||
|
||||
@ -62,7 +62,7 @@ class Loadout:
|
||||
# "tasks": List (as a dict) of task IDs the payload is used by.
|
||||
# }
|
||||
payloads = flight.unit_type.load_payloads()
|
||||
for payload in payloads["payloads"].values():
|
||||
for payload in payloads.values():
|
||||
name = payload["name"]
|
||||
pylons = payload["pylons"]
|
||||
yield Loadout(
|
||||
@ -90,22 +90,33 @@ class Loadout:
|
||||
# etc.
|
||||
loadout_names = {t: [f"Liberation {t.value}"] for t in FlightType}
|
||||
legacy_names = {
|
||||
FlightType.TARCAP: ("CAP HEAVY", "CAP"),
|
||||
FlightType.BARCAP: ("CAP HEAVY", "CAP"),
|
||||
FlightType.TARCAP: ("CAP HEAVY", "CAP", "Liberation BARCAP"),
|
||||
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Liberation TARCAP"),
|
||||
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
||||
FlightType.INTERCEPTION: ("CAP HEAVY", "CAP"),
|
||||
FlightType.STRIKE: ("STRIKE",),
|
||||
FlightType.ANTISHIP: ("ANTISHIP",),
|
||||
FlightType.SEAD: ("SEAD",),
|
||||
FlightType.DEAD: ("SEAD",),
|
||||
FlightType.ESCORT: ("CAP HEAVY", "CAP"),
|
||||
FlightType.BAI: ("BAI", "CAS MAVERICK F", "CAS"),
|
||||
FlightType.SWEEP: ("CAP HEAVY", "CAP"),
|
||||
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE", "STRIKE"),
|
||||
FlightType.OCA_AIRCRAFT: ("OCA", "CAS MAVERICK F", "CAS"),
|
||||
FlightType.BAI: ("BAI",),
|
||||
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
|
||||
FlightType.OCA_AIRCRAFT: ("OCA",),
|
||||
}
|
||||
for flight_type, names in legacy_names.items():
|
||||
loadout_names[flight_type].extend(names)
|
||||
# A SEAD escort typically does not need a different loadout than a regular
|
||||
# SEAD flight, so fall back to SEAD if needed.
|
||||
loadout_names[FlightType.SEAD_ESCORT].extend(loadout_names[FlightType.SEAD])
|
||||
# Sweep and escort can fall back to TARCAP.
|
||||
loadout_names[FlightType.ESCORT].extend(loadout_names[FlightType.TARCAP])
|
||||
loadout_names[FlightType.SWEEP].extend(loadout_names[FlightType.TARCAP])
|
||||
# Intercept can fall back to BARCAP.
|
||||
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
|
||||
# OCA/Aircraft falls back to BAI, which falls back to CAS.
|
||||
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
|
||||
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
|
||||
# DEAD also falls back to BAI.
|
||||
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])
|
||||
# OCA/Runway falls back to Strike
|
||||
loadout_names[FlightType.OCA_RUNWAY].extend(loadout_names[FlightType.STRIKE])
|
||||
yield from loadout_names[flight.flight_type]
|
||||
|
||||
@classmethod
|
||||
|
||||
@ -36,23 +36,23 @@ class GroundSpeed:
|
||||
# DCS's max speed is in kph at 0 MSL.
|
||||
max_speed = kph(flight.unit_type.max_speed)
|
||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
||||
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
|
||||
# Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and
|
||||
# account for heavily loaded jets.
|
||||
return mach(0.8, altitude)
|
||||
return mach(0.85, altitude)
|
||||
|
||||
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
||||
# as it can at sea level. This probably isn't great assumption, but
|
||||
# might. be sufficient given the wiggle room. We can come up with
|
||||
# another heuristic if needed.
|
||||
cruise_mach = max_speed.mach() * 0.8
|
||||
cruise_mach = max_speed.mach() * 0.85
|
||||
return mach(cruise_mach, altitude)
|
||||
|
||||
|
||||
class TravelTime:
|
||||
@staticmethod
|
||||
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
||||
error_factor = 1.1
|
||||
error_factor = 1.05
|
||||
distance = meters(a.distance_to_point(b))
|
||||
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
||||
|
||||
|
||||
@ -423,7 +423,10 @@ class WaypointBuilder:
|
||||
return self.sweep_start(start, altitude), self.sweep_end(end, altitude)
|
||||
|
||||
def escort(
|
||||
self, ingress: Point, target: MissionTarget, egress: Point
|
||||
self,
|
||||
ingress: Point,
|
||||
target: MissionTarget,
|
||||
egress: Point,
|
||||
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||
"""Creates the waypoints needed to escort the package.
|
||||
|
||||
|
||||
@ -80,6 +80,10 @@ class GroundPlanner:
|
||||
|
||||
def plan_groundwar(self):
|
||||
|
||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||
|
||||
remaining_available_frontline_units = ground_unit_limit
|
||||
|
||||
if hasattr(self.cp, "stance"):
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
||||
else:
|
||||
@ -118,6 +122,12 @@ class GroundPlanner:
|
||||
continue
|
||||
|
||||
available = self.cp.base.armor[key]
|
||||
|
||||
if available > remaining_available_frontline_units:
|
||||
available = remaining_available_frontline_units
|
||||
|
||||
remaining_available_frontline_units -= available
|
||||
|
||||
while available > 0:
|
||||
|
||||
if role == CombatGroupRole.SHORAD:
|
||||
@ -144,6 +154,9 @@ class GroundPlanner:
|
||||
group.units.append(key)
|
||||
collection.append(group)
|
||||
|
||||
if remaining_available_frontline_units == 0:
|
||||
break
|
||||
|
||||
print("------------------")
|
||||
print("Ground Planner : ")
|
||||
print(self.cp.name)
|
||||
|
||||
@ -36,7 +36,7 @@ from dcs.unittype import FlyingType
|
||||
from tabulate import tabulate
|
||||
|
||||
from game.data.alic import AlicCodes
|
||||
from game.db import find_unittype, unit_type_from_name
|
||||
from game.db import unit_type_from_name
|
||||
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import meters
|
||||
@ -298,9 +298,7 @@ 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.text(f"Bullseye: {self.bullseye.to_lat_lon(self.theater).format_dms()}")
|
||||
|
||||
writer.table(
|
||||
[
|
||||
@ -507,7 +505,7 @@ class SeadTaskPage(KneeboardPage):
|
||||
ll = self.theater.point_to_ll(unit.position)
|
||||
unit_type = unit_type_from_name(unit.type)
|
||||
name = unit.name if unit_type is None else unit_type.name
|
||||
return [name, self.alic_for(unit), self.format_ll(ll)]
|
||||
return [name, self.alic_for(unit), ll.format_dms(include_decimal_seconds=True)]
|
||||
|
||||
|
||||
class StrikeTaskPage(KneeboardPage):
|
||||
@ -546,7 +544,11 @@ class StrikeTaskPage(KneeboardPage):
|
||||
|
||||
def target_info_row(self, target: NumberedWaypoint) -> List[str]:
|
||||
ll = self.theater.point_to_ll(target.waypoint.position)
|
||||
return [str(target.number), target.waypoint.pretty_name, self.format_ll(ll)]
|
||||
return [
|
||||
str(target.number),
|
||||
target.waypoint.pretty_name,
|
||||
ll.format_dms(include_decimal_seconds=True),
|
||||
]
|
||||
|
||||
|
||||
class KneeboardGenerator(MissionInfoGenerator):
|
||||
|
||||
@ -39,7 +39,7 @@ ALPHA_MILITARY = [
|
||||
"Zero",
|
||||
]
|
||||
|
||||
ANIMALS = [
|
||||
ANIMALS: tuple[str, ...] = (
|
||||
"SHARK",
|
||||
"TORTOISE",
|
||||
"BAT",
|
||||
@ -243,7 +243,7 @@ ANIMALS = [
|
||||
"CANARY",
|
||||
"WOODCHUCK",
|
||||
"ANACONDA",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class NameGenerator:
|
||||
@ -253,7 +253,7 @@ class NameGenerator:
|
||||
convoy_number = 0
|
||||
cargo_ship_number = 0
|
||||
|
||||
ANIMALS = ANIMALS
|
||||
animals: list[str] = list(ANIMALS)
|
||||
existing_alphas: List[str] = []
|
||||
|
||||
@classmethod
|
||||
@ -262,7 +262,7 @@ class NameGenerator:
|
||||
cls.infantry_number = 0
|
||||
cls.convoy_number = 0
|
||||
cls.cargo_ship_number = 0
|
||||
cls.ANIMALS = ANIMALS
|
||||
cls.animals = list(ANIMALS)
|
||||
cls.existing_alphas = []
|
||||
|
||||
@classmethod
|
||||
@ -345,30 +345,25 @@ class NameGenerator:
|
||||
|
||||
@classmethod
|
||||
def random_objective_name(cls):
|
||||
if len(cls.ANIMALS) == 0:
|
||||
for i in range(10):
|
||||
new_name_generated = True
|
||||
alpha_mil_name = (
|
||||
random.choice(ALPHA_MILITARY).upper()
|
||||
+ "#"
|
||||
+ str(random.randint(0, 100))
|
||||
)
|
||||
for existing_name in cls.existing_alphas:
|
||||
if existing_name == alpha_mil_name:
|
||||
new_name_generated = False
|
||||
if new_name_generated:
|
||||
cls.existing_alphas.append(alpha_mil_name)
|
||||
return alpha_mil_name
|
||||
|
||||
# At this point, give up trying - something has gone wrong and we haven't been able to make a new name in 10 tries.
|
||||
# We'll just make a longer name using the current unix epoch in nanoseconds. That should be unique... right?
|
||||
last_chance_name = alpha_mil_name + str(time.time_ns())
|
||||
cls.existing_alphas.append(last_chance_name)
|
||||
return last_chance_name
|
||||
else:
|
||||
animal = random.choice(cls.ANIMALS)
|
||||
cls.ANIMALS.remove(animal)
|
||||
if cls.animals:
|
||||
animal = random.choice(cls.animals)
|
||||
cls.animals.remove(animal)
|
||||
return animal
|
||||
|
||||
for _ in range(10):
|
||||
alpha = random.choice(ALPHA_MILITARY).upper()
|
||||
number = str(random.randint(0, 100))
|
||||
alpha_mil_name = f"{alpha} #{number:02}"
|
||||
if alpha_mil_name not in cls.existing_alphas:
|
||||
cls.existing_alphas.append(alpha_mil_name)
|
||||
return alpha_mil_name
|
||||
|
||||
# At this point, give up trying - something has gone wrong and we haven't been
|
||||
# able to make a new name in 10 tries. We'll just make a longer name using the
|
||||
# current unix epoch in nanoseconds. That should be unique... right?
|
||||
last_chance_name = alpha_mil_name + str(time.time_ns())
|
||||
cls.existing_alphas.append(last_chance_name)
|
||||
return last_chance_name
|
||||
|
||||
|
||||
namegen = NameGenerator
|
||||
|
||||
@ -96,32 +96,15 @@ class TriggersGenerator:
|
||||
"""
|
||||
for coalition_name, coalition in self.mission.coalition.items():
|
||||
if coalition_name == player_coalition:
|
||||
skill_level = (
|
||||
self.game.settings.player_skill,
|
||||
self.game.settings.player_skill,
|
||||
)
|
||||
skill_level = Skill(self.game.settings.player_skill)
|
||||
elif coalition_name == enemy_coalition:
|
||||
skill_level = (
|
||||
self.game.settings.enemy_skill,
|
||||
self.game.settings.enemy_vehicle_skill,
|
||||
)
|
||||
skill_level = Skill(self.game.settings.enemy_vehicle_skill)
|
||||
else:
|
||||
continue
|
||||
|
||||
for country in coalition.countries.values():
|
||||
flying_groups = (
|
||||
country.plane_group + country.helicopter_group
|
||||
) # type: FlyingGroup
|
||||
for flying_group in flying_groups:
|
||||
for plane_unit in flying_group.units:
|
||||
if (
|
||||
plane_unit.skill != Skill.Client
|
||||
and plane_unit.skill != Skill.Player
|
||||
):
|
||||
plane_unit.skill = Skill(skill_level[0])
|
||||
|
||||
for vehicle_group in country.vehicle_group:
|
||||
vehicle_group.set_skill(Skill(skill_level[1]))
|
||||
vehicle_group.set_skill(skill_level)
|
||||
|
||||
def _gen_markers(self):
|
||||
"""
|
||||
|
||||
3
mypy.ini
3
mypy.ini
@ -5,6 +5,9 @@ namespace_packages = True
|
||||
follow_imports=silent
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-faker.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-PIL.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
||||
2
pydcs
2
pydcs
@ -1 +1 @@
|
||||
Subproject commit 4972988c978f2057e7aa06919c4de71ee9a06ea5
|
||||
Subproject commit 53632aa7a8749c67eba371aaea95bfef73f43cdc
|
||||
@ -4,6 +4,8 @@ from dcs import task
|
||||
from dcs.planes import PlaneType
|
||||
from dcs.weapons_data import Weapons
|
||||
|
||||
from pydcs_extensions.weapon_injector import inject_weapons
|
||||
|
||||
|
||||
class WeaponsA4EC:
|
||||
AN_M57__2__TER_ = {
|
||||
@ -432,6 +434,9 @@ class WeaponsA4EC:
|
||||
}
|
||||
|
||||
|
||||
inject_weapons(WeaponsA4EC)
|
||||
|
||||
|
||||
class A_4E_C(PlaneType):
|
||||
id = "A-4E-C"
|
||||
flyable = True
|
||||
|
||||
@ -4,12 +4,17 @@ from dcs import task
|
||||
from dcs.planes import PlaneType
|
||||
from dcs.weapons_data import Weapons
|
||||
|
||||
from pydcs_extensions.weapon_injector import inject_weapons
|
||||
|
||||
|
||||
class F22AWeapons:
|
||||
AIM_9XX = {"clsid": "{AIM-9XX}", "name": "AIM-9XX", "weight": 85}
|
||||
AIM_120D = {"clsid": "{AIM-120D}", "name": "AIM-120D", "weight": 152}
|
||||
|
||||
|
||||
inject_weapons(F22AWeapons)
|
||||
|
||||
|
||||
class F_22A(PlaneType):
|
||||
id = "F-22A"
|
||||
flyable = True
|
||||
|
||||
@ -4,6 +4,8 @@ from dcs import task
|
||||
from dcs.planes import PlaneType
|
||||
from dcs.weapons_data import Weapons
|
||||
|
||||
from pydcs_extensions.weapon_injector import inject_weapons
|
||||
|
||||
|
||||
class HerculesWeapons:
|
||||
GAU_23A_Chain_Gun__30mm_ = {
|
||||
@ -679,6 +681,9 @@ class HerculesWeapons:
|
||||
}
|
||||
|
||||
|
||||
inject_weapons(HerculesWeapons)
|
||||
|
||||
|
||||
class Hercules(PlaneType):
|
||||
id = "Hercules"
|
||||
flyable = True
|
||||
|
||||
@ -4,6 +4,8 @@ from dcs import task
|
||||
from dcs.planes import PlaneType
|
||||
from dcs.weapons_data import Weapons
|
||||
|
||||
from pydcs_extensions.weapon_injector import inject_weapons
|
||||
|
||||
|
||||
class MB_339PAN_Weapons:
|
||||
ARF8M3_TP = {"clsid": "{ARF8M3_TP}", "name": "ARF8M3 TP", "weight": None}
|
||||
@ -107,6 +109,9 @@ class MB_339PAN_Weapons:
|
||||
}
|
||||
|
||||
|
||||
inject_weapons(MB_339PAN_Weapons)
|
||||
|
||||
|
||||
class MB_339PAN(PlaneType):
|
||||
id = "MB-339PAN"
|
||||
flyable = True
|
||||
|
||||
@ -4,6 +4,8 @@ from dcs import task
|
||||
from dcs.planes import PlaneType
|
||||
from dcs.weapons_data import Weapons
|
||||
|
||||
from pydcs_extensions.weapon_injector import inject_weapons
|
||||
|
||||
|
||||
class Su57Weapons:
|
||||
Kh_59MK2 = {"clsid": "{KH_59MK2}", "name": "Kh-59MK2", "weight": None}
|
||||
@ -18,6 +20,9 @@ class Su57Weapons:
|
||||
}
|
||||
|
||||
|
||||
inject_weapons(Su57Weapons)
|
||||
|
||||
|
||||
class Su_57(PlaneType):
|
||||
id = "Su-57"
|
||||
flyable = True
|
||||
|
||||
17
pydcs_extensions/weapon_injector.py
Normal file
17
pydcs_extensions/weapon_injector.py
Normal file
@ -0,0 +1,17 @@
|
||||
from typing import List, Any
|
||||
|
||||
from dcs.weapons_data import Weapons, weapon_ids
|
||||
|
||||
|
||||
def inject_weapons(weapon_class: Any) -> None:
|
||||
"""
|
||||
Inject custom weapons from mods into pydcs weapons databases via introspection
|
||||
:param weapon_class: The custom weapons class containing dictionaries with weapon info
|
||||
:return: None
|
||||
"""
|
||||
for key, value in weapon_class.__dict__.items():
|
||||
if key.startswith("__"):
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("clsid"):
|
||||
setattr(Weapons, key, value)
|
||||
weapon_ids[value["clsid"]] = value
|
||||
@ -1,13 +0,0 @@
|
||||
from contextlib import contextmanager
|
||||
from typing import ContextManager
|
||||
|
||||
from PySide2.QtGui import QPainter
|
||||
|
||||
|
||||
@contextmanager
|
||||
def painter_context(painter: QPainter) -> ContextManager[None]:
|
||||
try:
|
||||
painter.save()
|
||||
yield
|
||||
finally:
|
||||
painter.restore()
|
||||
122
qt_ui/delegates.py
Normal file
122
qt_ui/delegates.py
Normal file
@ -0,0 +1,122 @@
|
||||
from contextlib import contextmanager
|
||||
from typing import ContextManager, Optional
|
||||
|
||||
from PySide2.QtCore import QModelIndex, Qt, QSize
|
||||
from PySide2.QtGui import QPainter, QFont, QFontMetrics, QIcon
|
||||
from PySide2.QtWidgets import QStyledItemDelegate, QStyleOptionViewItem, QStyle
|
||||
|
||||
|
||||
@contextmanager
|
||||
def painter_context(painter: QPainter) -> ContextManager[None]:
|
||||
try:
|
||||
painter.save()
|
||||
yield
|
||||
finally:
|
||||
painter.restore()
|
||||
|
||||
|
||||
class TwoColumnRowDelegate(QStyledItemDelegate):
|
||||
HMARGIN = 4
|
||||
VMARGIN = 4
|
||||
|
||||
def __init__(self, rows: int, columns: int, font_size: int = 12) -> None:
|
||||
if columns not in (1, 2):
|
||||
raise ValueError(f"Only one or two columns may be used, not {columns}")
|
||||
super().__init__()
|
||||
self.font_size = font_size
|
||||
self.rows = rows
|
||||
self.columns = columns
|
||||
|
||||
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
||||
font = QFont(option.font)
|
||||
font.setPointSize(self.font_size)
|
||||
return font
|
||||
|
||||
def text_for(self, index: QModelIndex, row: int, column: int) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def paint(
|
||||
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
|
||||
) -> None:
|
||||
# Draw the list item with all the default selection styling, but with an
|
||||
# invalid index so text formatting is left to us.
|
||||
super().paint(painter, option, QModelIndex())
|
||||
|
||||
rect = option.rect.adjusted(
|
||||
self.HMARGIN, self.VMARGIN, -self.HMARGIN, -self.VMARGIN
|
||||
)
|
||||
|
||||
with painter_context(painter):
|
||||
painter.setFont(self.get_font(option))
|
||||
|
||||
icon: Optional[QIcon] = index.data(Qt.DecorationRole)
|
||||
|
||||
if icon is not None:
|
||||
icon.paint(
|
||||
painter,
|
||||
rect,
|
||||
Qt.AlignLeft | Qt.AlignVCenter,
|
||||
self.icon_mode(option),
|
||||
self.icon_state(option),
|
||||
)
|
||||
rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN, 0, 0, 0)
|
||||
|
||||
row_height = rect.height() / self.rows
|
||||
for row in range(self.rows):
|
||||
y = row_height * row
|
||||
location = rect.adjusted(0, y, 0, y)
|
||||
painter.drawText(location, Qt.AlignLeft, self.text_for(index, row, 0))
|
||||
if self.columns == 2:
|
||||
painter.drawText(
|
||||
location, Qt.AlignRight, self.text_for(index, row, 1)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode:
|
||||
if not (option.state & QStyle.State_Enabled):
|
||||
return QIcon.Disabled
|
||||
elif option.state & QStyle.State_Selected:
|
||||
return QIcon.Selected
|
||||
elif option.state & QStyle.State_Active:
|
||||
return QIcon.Active
|
||||
return QIcon.Normal
|
||||
|
||||
@staticmethod
|
||||
def icon_state(option: QStyleOptionViewItem) -> QIcon.State:
|
||||
return QIcon.On if option.state & QStyle.State_Open else QIcon.Off
|
||||
|
||||
@staticmethod
|
||||
def icon_size(option: QStyleOptionViewItem) -> QSize:
|
||||
icon_size: Optional[QSize] = option.decorationSize
|
||||
if icon_size is None:
|
||||
return QSize(0, 0)
|
||||
else:
|
||||
return icon_size
|
||||
|
||||
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
||||
metrics = QFontMetrics(self.get_font(option))
|
||||
widths = []
|
||||
heights = []
|
||||
|
||||
icon_size = self.icon_size(option)
|
||||
icon_width = 0
|
||||
icon_height = 0
|
||||
if icon_size.width():
|
||||
icon_width = icon_size.width() + self.HMARGIN
|
||||
if icon_size.height():
|
||||
icon_height = icon_size.height() + self.VMARGIN
|
||||
|
||||
for row in range(self.rows):
|
||||
width = 0
|
||||
height = 0
|
||||
for column in range(self.columns):
|
||||
size = metrics.size(0, self.text_for(index, row, column))
|
||||
width += size.width()
|
||||
height = max(height, size.height())
|
||||
widths.append(width)
|
||||
heights.append(height)
|
||||
|
||||
return QSize(
|
||||
icon_width + max(widths) + 2 * self.HMARGIN,
|
||||
max(icon_height, sum(heights)) + 2 * self.VMARGIN,
|
||||
)
|
||||
@ -6,10 +6,10 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import dcs
|
||||
from PySide2 import QtWidgets
|
||||
from PySide2.QtGui import QPixmap
|
||||
from PySide2.QtWidgets import QApplication, QSplashScreen
|
||||
from dcs.payloads import PayloadDirectories
|
||||
from dcs.weapons_data import weapon_ids
|
||||
|
||||
from game import Game, VERSION, persistency
|
||||
@ -35,6 +35,27 @@ from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
||||
QLiberationFirstStartWindow,
|
||||
)
|
||||
|
||||
THIS_DIR = Path(__file__).parent
|
||||
|
||||
|
||||
def inject_custom_payloads(user_path: Path) -> None:
|
||||
dev_payloads = THIS_DIR.parent / "resources/customized_payloads"
|
||||
# The packaged release rearranges the file locations, so the release has the
|
||||
# customized payloads in a different location.
|
||||
release_payloads = THIS_DIR / "resources/customized_payloads"
|
||||
if dev_payloads.exists():
|
||||
payloads = dev_payloads
|
||||
elif release_payloads.exists():
|
||||
payloads = release_payloads
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Could not find customized payloads at {release_payloads} or "
|
||||
f"{dev_payloads}. Aircraft will have no payloads."
|
||||
)
|
||||
# We configure these as fallbacks so that the user's payloads override ours.
|
||||
PayloadDirectories.set_fallback(payloads)
|
||||
PayloadDirectories.set_preferred(user_path / "MissionEditor" / "UnitPayloads")
|
||||
|
||||
|
||||
def run_ui(game: Optional[Game], new_map: bool) -> None:
|
||||
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
|
||||
@ -48,22 +69,6 @@ def run_ui(game: Optional[Game], new_map: bool) -> None:
|
||||
logging.info("Loading stylesheet: %s", liberation_theme.get_theme_css_file())
|
||||
app.setStyleSheet(stylesheet.read())
|
||||
|
||||
# Inject custom payload in pydcs framework
|
||||
custom_payloads = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"..\\resources\\customized_payloads",
|
||||
)
|
||||
if os.path.exists(custom_payloads):
|
||||
dcs.unittype.FlyingType.payload_dirs.append(custom_payloads)
|
||||
else:
|
||||
# For release version the path is different.
|
||||
custom_payloads = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
"resources\\customized_payloads",
|
||||
)
|
||||
if os.path.exists(custom_payloads):
|
||||
dcs.unittype.FlyingType.payload_dirs.append(custom_payloads)
|
||||
|
||||
first_start = liberation_install.init()
|
||||
if first_start:
|
||||
window = QLiberationFirstStartWindow()
|
||||
@ -76,6 +81,8 @@ def run_ui(game: Optional[Game], new_map: bool) -> None:
|
||||
)
|
||||
)
|
||||
|
||||
inject_custom_payloads(Path(persistency.base_path()))
|
||||
|
||||
# Splash screen setup
|
||||
pixmap = QPixmap("./resources/ui/splash_screen.png")
|
||||
splash = QSplashScreen(pixmap)
|
||||
@ -189,6 +196,15 @@ def create_game(
|
||||
"Cannot generate campaign without configuring DCS Liberation. Start the UI "
|
||||
"for the first run configuration."
|
||||
)
|
||||
|
||||
# This needs to run before the pydcs payload cache is created, which happens
|
||||
# extremely early. It's not a problem that we inject these paths twice because we'll
|
||||
# get the same answers each time.
|
||||
#
|
||||
# Without this, it is not possible to use next turn (or anything that needs to check
|
||||
# for loadouts) without saving the generated campaign and reloading it the normal
|
||||
# way.
|
||||
inject_custom_payloads(Path(persistency.base_path()))
|
||||
campaign = Campaign.from_json(campaign_path)
|
||||
generator = GameGenerator(
|
||||
blue,
|
||||
|
||||
103
qt_ui/models.py
103
qt_ui/models.py
@ -14,6 +14,7 @@ from PySide2.QtGui import QIcon
|
||||
|
||||
from game import db
|
||||
from game.game import Game
|
||||
from game.squadrons import Squadron, Pilot
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.transfers import TransferOrder
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
@ -166,6 +167,7 @@ class PackageModel(QAbstractListModel):
|
||||
if flight.cargo is not None:
|
||||
flight.cargo.transport = None
|
||||
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
||||
flight.clear_roster()
|
||||
self.package.remove_flight(flight)
|
||||
self.endRemoveRows()
|
||||
self.update_tot()
|
||||
@ -258,6 +260,7 @@ class AtoModel(QAbstractListModel):
|
||||
self.ato.remove_package(package)
|
||||
for flight in package.flights:
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
flight.clear_roster()
|
||||
if flight.cargo is not None:
|
||||
flight.cargo.transport = None
|
||||
self.endRemoveRows()
|
||||
@ -366,6 +369,105 @@ class TransferModel(QAbstractListModel):
|
||||
return self.game_model.game.transfers.transfer_at_index(index.row())
|
||||
|
||||
|
||||
class AirWingModel(QAbstractListModel):
|
||||
"""The model for an air wing."""
|
||||
|
||||
SquadronRole = Qt.UserRole
|
||||
|
||||
def __init__(self, game_model: GameModel, player: bool) -> None:
|
||||
super().__init__()
|
||||
self.game_model = game_model
|
||||
self.player = player
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return self.game_model.game.air_wing_for(self.player).size
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid():
|
||||
return None
|
||||
squadron = self.squadron_at_index(index)
|
||||
if role == Qt.DisplayRole:
|
||||
return self.text_for_squadron(squadron)
|
||||
if role == Qt.DecorationRole:
|
||||
return self.icon_for_squadron(squadron)
|
||||
elif role == AirWingModel.SquadronRole:
|
||||
return squadron
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def text_for_squadron(squadron: Squadron) -> str:
|
||||
"""Returns the text that should be displayed for the squadron."""
|
||||
return str(squadron)
|
||||
|
||||
@staticmethod
|
||||
def icon_for_squadron(squadron: Squadron) -> Optional[QIcon]:
|
||||
"""Returns the icon that should be displayed for the squadron."""
|
||||
name = db.unit_type_name(squadron.aircraft)
|
||||
if name in AIRCRAFT_ICONS:
|
||||
return QIcon(AIRCRAFT_ICONS[name])
|
||||
return None
|
||||
|
||||
def squadron_at_index(self, index: QModelIndex) -> Squadron:
|
||||
"""Returns the squadron located at the given index."""
|
||||
return self.game_model.game.air_wing_for(self.player).squadron_at_index(
|
||||
index.row()
|
||||
)
|
||||
|
||||
|
||||
class SquadronModel(QAbstractListModel):
|
||||
"""The model for a squadron."""
|
||||
|
||||
PilotRole = Qt.UserRole
|
||||
|
||||
def __init__(self, squadron: Squadron) -> None:
|
||||
super().__init__()
|
||||
self.squadron = squadron
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return self.squadron.size
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid():
|
||||
return None
|
||||
pilot = self.pilot_at_index(index)
|
||||
if role == Qt.DisplayRole:
|
||||
return self.text_for_pilot(pilot)
|
||||
if role == Qt.DecorationRole:
|
||||
return self.icon_for_pilot(pilot)
|
||||
elif role == SquadronModel.PilotRole:
|
||||
return pilot
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def text_for_pilot(pilot: Pilot) -> str:
|
||||
"""Returns the text that should be displayed for the pilot."""
|
||||
return pilot.name
|
||||
|
||||
@staticmethod
|
||||
def icon_for_pilot(_pilot: Pilot) -> Optional[QIcon]:
|
||||
"""Returns the icon that should be displayed for the pilot."""
|
||||
return None
|
||||
|
||||
def pilot_at_index(self, index: QModelIndex) -> Pilot:
|
||||
"""Returns the pilot located at the given index."""
|
||||
return self.squadron.pilot_at_index(index.row())
|
||||
|
||||
def toggle_ai_state(self, index: QModelIndex) -> None:
|
||||
pilot = self.pilot_at_index(index)
|
||||
self.beginResetModel()
|
||||
pilot.player = not pilot.player
|
||||
self.endResetModel()
|
||||
|
||||
def toggle_leave_state(self, index: QModelIndex) -> None:
|
||||
pilot = self.pilot_at_index(index)
|
||||
self.beginResetModel()
|
||||
if pilot.on_leave:
|
||||
pilot.return_from_leave()
|
||||
else:
|
||||
pilot.send_on_leave()
|
||||
self.endResetModel()
|
||||
|
||||
|
||||
class GameModel:
|
||||
"""A model for the Game object.
|
||||
|
||||
@ -376,6 +478,7 @@ class GameModel:
|
||||
def __init__(self, game: Optional[Game]) -> None:
|
||||
self.game: Optional[Game] = game
|
||||
self.transfer_model = TransferModel(self)
|
||||
self.blue_air_wing_model = AirWingModel(self, player=True)
|
||||
if self.game is None:
|
||||
self.ato_model = AtoModel(self, AirTaskingOrder())
|
||||
self.red_ato_model = AtoModel(self, AirTaskingOrder())
|
||||
|
||||
@ -21,6 +21,7 @@ from qt_ui.widgets.QConditionsWidget import QConditionsWidget
|
||||
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
|
||||
from qt_ui.widgets.QIntelBox import QIntelBox
|
||||
from qt_ui.widgets.clientslots import MaxPlayerCount
|
||||
from qt_ui.windows.AirWingDialog import AirWingDialog
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
|
||||
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
|
||||
@ -63,6 +64,11 @@ class QTopPanel(QFrame):
|
||||
|
||||
self.factionsInfos = QFactionsInfos(self.game)
|
||||
|
||||
self.air_wing = QPushButton("Air Wing")
|
||||
self.air_wing.setDisabled(True)
|
||||
self.air_wing.setProperty("style", "btn-primary")
|
||||
self.air_wing.clicked.connect(self.open_air_wing)
|
||||
|
||||
self.transfers = QPushButton("Transfers")
|
||||
self.transfers.setDisabled(True)
|
||||
self.transfers.setProperty("style", "btn-primary")
|
||||
@ -84,6 +90,7 @@ class QTopPanel(QFrame):
|
||||
|
||||
self.buttonBox = QGroupBox("Misc")
|
||||
self.buttonBoxLayout = QHBoxLayout()
|
||||
self.buttonBoxLayout.addWidget(self.air_wing)
|
||||
self.buttonBoxLayout.addWidget(self.transfers)
|
||||
self.buttonBoxLayout.addWidget(self.settings)
|
||||
self.buttonBoxLayout.addWidget(self.statistics)
|
||||
@ -114,6 +121,7 @@ class QTopPanel(QFrame):
|
||||
if game is None:
|
||||
return
|
||||
|
||||
self.air_wing.setEnabled(True)
|
||||
self.transfers.setEnabled(True)
|
||||
self.settings.setEnabled(True)
|
||||
self.statistics.setEnabled(True)
|
||||
@ -130,6 +138,10 @@ class QTopPanel(QFrame):
|
||||
else:
|
||||
self.proceedButton.setEnabled(True)
|
||||
|
||||
def open_air_wing(self):
|
||||
self.dialog = AirWingDialog(self.game_model, self.window())
|
||||
self.dialog.show()
|
||||
|
||||
def open_transfers(self):
|
||||
self.dialog = PendingTransfersDialog(self.game_model)
|
||||
self.dialog.show()
|
||||
@ -176,17 +188,18 @@ class QTopPanel(QFrame):
|
||||
def confirm_no_client_launch(self) -> bool:
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"Continue without client slots?",
|
||||
"Continue without player pilots?",
|
||||
(
|
||||
"No client slots have been created for players. Continuing will "
|
||||
"allow the AI to perform the mission, but players will be unable "
|
||||
"to participate.<br />"
|
||||
"No player pilots have been assigned to flights. Continuing will allow "
|
||||
"the AI to perform the mission, but players will be unable to "
|
||||
"participate.<br />"
|
||||
"<br />"
|
||||
"To add client slots for players, select a package from the "
|
||||
"Packages panel on the left of the main window, and then a flight "
|
||||
"from the Flights panel below the Packages panel. The edit button "
|
||||
"below the Flights panel will allow you to edit the number of "
|
||||
"client slots in the flight. Each client slot allows one player.<br />"
|
||||
"To assign player pilots to a flight, select a package from the "
|
||||
"Packages panel on the left of the main window, and then a flight from "
|
||||
"the Flights panel below the Packages panel. The edit button below the "
|
||||
"Flights panel will allow you to assign specific pilots to the flight. "
|
||||
"If you have no player pilots available, the checkbox next to the "
|
||||
"name will convert them to a player.<br />"
|
||||
"<br />Click 'Yes' to continue with an AI only mission"
|
||||
"<br />Click 'No' if you'd like to make more changes."
|
||||
),
|
||||
@ -232,11 +245,44 @@ class QTopPanel(QFrame):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check_no_missing_pilots(self) -> bool:
|
||||
missing_pilots = []
|
||||
for package in self.game.blue_ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.missing_pilots > 0:
|
||||
missing_pilots.append((package, flight))
|
||||
|
||||
if not missing_pilots:
|
||||
return False
|
||||
|
||||
formatted = "<br />".join(
|
||||
[f"{p.primary_task} {p.target}: {f}" for p, f in missing_pilots]
|
||||
)
|
||||
mbox = QMessageBox(
|
||||
QMessageBox.Critical,
|
||||
"Flights are missing pilots",
|
||||
(
|
||||
"The following flights are missing one or more pilots:<br />"
|
||||
"<br />"
|
||||
f"{formatted}<br />"
|
||||
"<br />"
|
||||
"You must either assign pilots to those flights or cancel those "
|
||||
"missions."
|
||||
),
|
||||
parent=self,
|
||||
)
|
||||
mbox.setEscapeButton(mbox.addButton(QMessageBox.Close))
|
||||
mbox.exec_()
|
||||
return True
|
||||
|
||||
def launch_mission(self):
|
||||
"""Finishes planning and waits for mission completion."""
|
||||
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
||||
return
|
||||
|
||||
if self.check_no_missing_pilots():
|
||||
return
|
||||
|
||||
negative_starts = self.negative_start_packages()
|
||||
if negative_starts:
|
||||
if not self.confirm_negative_start_time(negative_starts):
|
||||
|
||||
@ -10,10 +10,6 @@ from PySide2.QtCore import (
|
||||
)
|
||||
from PySide2.QtGui import (
|
||||
QContextMenuEvent,
|
||||
QFont,
|
||||
QFontMetrics,
|
||||
QIcon,
|
||||
QPainter,
|
||||
)
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
@ -25,9 +21,6 @@ from PySide2.QtWidgets import (
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QStyle,
|
||||
QStyleOptionViewItem,
|
||||
QStyledItemDelegate,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
@ -35,111 +28,42 @@ from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from ..delegate_helpers import painter_context
|
||||
from ..delegates import TwoColumnRowDelegate
|
||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||
|
||||
|
||||
class FlightDelegate(QStyledItemDelegate):
|
||||
FONT_SIZE = 10
|
||||
HMARGIN = 4
|
||||
VMARGIN = 4
|
||||
|
||||
class FlightDelegate(TwoColumnRowDelegate):
|
||||
def __init__(self, package: Package) -> None:
|
||||
super().__init__()
|
||||
super().__init__(rows=2, columns=2, font_size=10)
|
||||
self.package = package
|
||||
|
||||
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
||||
font = QFont(option.font)
|
||||
font.setPointSize(self.FONT_SIZE)
|
||||
return font
|
||||
|
||||
@staticmethod
|
||||
def flight(index: QModelIndex) -> Flight:
|
||||
return index.data(PackageModel.FlightRole)
|
||||
|
||||
def first_row_text(self, index: QModelIndex) -> str:
|
||||
def text_for(self, index: QModelIndex, row: int, column: int) -> str:
|
||||
flight = self.flight(index)
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = estimator.mission_start_time(flight)
|
||||
return f"{flight} in {delay}"
|
||||
|
||||
def second_row_text(self, index: QModelIndex) -> str:
|
||||
flight = self.flight(index)
|
||||
origin = flight.from_cp.name
|
||||
if flight.arrival != flight.departure:
|
||||
return f"From {origin} to {flight.arrival.name}"
|
||||
return f"From {origin}"
|
||||
|
||||
def paint(
|
||||
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
|
||||
) -> None:
|
||||
# Draw the list item with all the default selection styling, but with an
|
||||
# invalid index so text formatting is left to us.
|
||||
super().paint(painter, option, QModelIndex())
|
||||
|
||||
rect = option.rect.adjusted(
|
||||
self.HMARGIN, self.VMARGIN, -self.HMARGIN, -self.VMARGIN
|
||||
)
|
||||
|
||||
with painter_context(painter):
|
||||
painter.setFont(self.get_font(option))
|
||||
|
||||
icon: Optional[QIcon] = index.data(Qt.DecorationRole)
|
||||
if icon is not None:
|
||||
icon.paint(
|
||||
painter,
|
||||
rect,
|
||||
Qt.AlignLeft | Qt.AlignVCenter,
|
||||
self.icon_mode(option),
|
||||
self.icon_state(option),
|
||||
)
|
||||
|
||||
rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN, 0, 0, 0)
|
||||
painter.drawText(rect, Qt.AlignLeft, self.first_row_text(index))
|
||||
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
|
||||
painter.drawText(line2, Qt.AlignLeft, self.second_row_text(index))
|
||||
|
||||
if (row, column) == (0, 0):
|
||||
estimator = TotEstimator(self.package)
|
||||
delay = estimator.mission_start_time(flight)
|
||||
return f"{flight} in {delay}"
|
||||
elif (row, column) == (0, 1):
|
||||
clients = self.num_clients(index)
|
||||
if clients:
|
||||
painter.drawText(rect, Qt.AlignRight, f"Player Slots: {clients}")
|
||||
return f"Player Slots: {clients}" if clients else ""
|
||||
elif (row, column) == (1, 0):
|
||||
origin = flight.from_cp.name
|
||||
if flight.arrival != flight.departure:
|
||||
return f"From {origin} to {flight.arrival.name}"
|
||||
return f"From {origin}"
|
||||
elif (row, column) == (1, 1):
|
||||
missing_pilots = flight.missing_pilots
|
||||
return f"Missing pilots: {flight.missing_pilots}" if missing_pilots else ""
|
||||
return ""
|
||||
|
||||
def num_clients(self, index: QModelIndex) -> int:
|
||||
flight = self.flight(index)
|
||||
return flight.client_count
|
||||
|
||||
@staticmethod
|
||||
def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode:
|
||||
if not (option.state & QStyle.State_Enabled):
|
||||
return QIcon.Disabled
|
||||
elif option.state & QStyle.State_Selected:
|
||||
return QIcon.Selected
|
||||
elif option.state & QStyle.State_Active:
|
||||
return QIcon.Active
|
||||
return QIcon.Normal
|
||||
|
||||
@staticmethod
|
||||
def icon_state(option: QStyleOptionViewItem) -> QIcon.State:
|
||||
return QIcon.On if option.state & QStyle.State_Open else QIcon.Off
|
||||
|
||||
@staticmethod
|
||||
def icon_size(option: QStyleOptionViewItem) -> QSize:
|
||||
icon_size: Optional[QSize] = option.decorationSize
|
||||
if icon_size is None:
|
||||
return QSize(0, 0)
|
||||
else:
|
||||
return icon_size
|
||||
|
||||
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
||||
left = self.icon_size(option).width() + self.HMARGIN
|
||||
metrics = QFontMetrics(self.get_font(option))
|
||||
first = metrics.size(0, self.first_row_text(index))
|
||||
second = metrics.size(0, self.second_row_text(index))
|
||||
text_width = max(first.width(), second.width())
|
||||
return QSize(
|
||||
left + text_width + 2 * self.HMARGIN,
|
||||
first.height() + second.height() + 2 * self.VMARGIN,
|
||||
)
|
||||
|
||||
|
||||
class QFlightList(QListView):
|
||||
"""List view for displaying the flights of a package."""
|
||||
@ -310,62 +234,35 @@ class QFlightPanel(QGroupBox):
|
||||
self.flight_list.delete_flight(index)
|
||||
|
||||
|
||||
class PackageDelegate(QStyledItemDelegate):
|
||||
FONT_SIZE = 12
|
||||
HMARGIN = 4
|
||||
VMARGIN = 4
|
||||
|
||||
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
||||
font = QFont(option.font)
|
||||
font.setPointSize(self.FONT_SIZE)
|
||||
return font
|
||||
class PackageDelegate(TwoColumnRowDelegate):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(rows=2, columns=2)
|
||||
|
||||
@staticmethod
|
||||
def package(index: QModelIndex) -> Package:
|
||||
return index.data(AtoModel.PackageRole)
|
||||
|
||||
def left_text(self, index: QModelIndex) -> str:
|
||||
def text_for(self, index: QModelIndex, row: int, column: int) -> str:
|
||||
package = self.package(index)
|
||||
return f"{package.package_description} {package.target.name}"
|
||||
|
||||
def right_text(self, index: QModelIndex) -> str:
|
||||
package = self.package(index)
|
||||
return f"TOT T+{package.time_over_target}"
|
||||
|
||||
def paint(
|
||||
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
|
||||
) -> None:
|
||||
# Draw the list item with all the default selection styling, but with an
|
||||
# invalid index so text formatting is left to us.
|
||||
super().paint(painter, option, QModelIndex())
|
||||
|
||||
rect = option.rect.adjusted(
|
||||
self.HMARGIN, self.VMARGIN, -self.HMARGIN, -self.VMARGIN
|
||||
)
|
||||
|
||||
with painter_context(painter):
|
||||
painter.setFont(self.get_font(option))
|
||||
|
||||
painter.drawText(rect, Qt.AlignLeft, self.left_text(index))
|
||||
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
|
||||
painter.drawText(line2, Qt.AlignLeft, self.right_text(index))
|
||||
|
||||
if (row, column) == (0, 0):
|
||||
return f"{package.package_description} {package.target.name}"
|
||||
elif (row, column) == (0, 1):
|
||||
clients = self.num_clients(index)
|
||||
if clients:
|
||||
painter.drawText(rect, Qt.AlignRight, f"Player Slots: {clients}")
|
||||
return f"Player Slots: {clients}" if clients else ""
|
||||
elif (row, column) == (1, 0):
|
||||
return f"TOT T+{package.time_over_target}"
|
||||
elif (row, column) == (1, 1):
|
||||
unassigned_pilots = self.missing_pilots(index)
|
||||
return f"Missing pilots: {unassigned_pilots}" if unassigned_pilots else ""
|
||||
return ""
|
||||
|
||||
def num_clients(self, index: QModelIndex) -> int:
|
||||
package = self.package(index)
|
||||
return sum(f.client_count for f in package.flights)
|
||||
|
||||
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
||||
metrics = QFontMetrics(self.get_font(option))
|
||||
left = metrics.size(0, self.left_text(index))
|
||||
right = metrics.size(0, self.right_text(index))
|
||||
return QSize(
|
||||
max(left.width(), right.width()) + 2 * self.HMARGIN,
|
||||
left.height() + right.height() + 2 * self.VMARGIN,
|
||||
)
|
||||
def missing_pilots(self, index: QModelIndex) -> int:
|
||||
package = self.package(index)
|
||||
return sum(f.missing_pilots for f in package.flights)
|
||||
|
||||
|
||||
class QPackageList(QListView):
|
||||
@ -376,7 +273,7 @@ class QPackageList(QListView):
|
||||
self.ato_model = model
|
||||
self.setModel(model)
|
||||
self.setItemDelegate(PackageDelegate())
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setIconSize(QSize(0, 0))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
self.model().rowsInserted.connect(self.on_new_packages)
|
||||
self.doubleClicked.connect(self.on_double_click)
|
||||
|
||||
@ -2,15 +2,12 @@
|
||||
from typing import Iterable, Type
|
||||
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
import gen.flights.ai_flight_planner_db
|
||||
|
||||
from game import Game, db
|
||||
|
||||
|
||||
class QAircraftTypeSelector(QComboBox):
|
||||
"""Combo box for selecting among the given aircraft types."""
|
||||
@ -19,77 +16,24 @@ class QAircraftTypeSelector(QComboBox):
|
||||
self,
|
||||
aircraft_types: Iterable[Type[FlyingType]],
|
||||
country: str,
|
||||
mission_type: str,
|
||||
mission_type: FlightType,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.model().sort(0)
|
||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||
self.country = country
|
||||
self.updateItems(mission_type, aircraft_types)
|
||||
self.update_items(mission_type, aircraft_types)
|
||||
|
||||
def updateItems(self, mission_type: str, aircraft_types):
|
||||
def update_items(self, mission_type: FlightType, aircraft_types):
|
||||
current_aircraft = self.currentData()
|
||||
self.clear()
|
||||
for aircraft in aircraft_types:
|
||||
if mission_type in [
|
||||
FlightType.BARCAP,
|
||||
FlightType.ESCORT,
|
||||
FlightType.INTERCEPTION,
|
||||
FlightType.SWEEP,
|
||||
FlightType.TARCAP,
|
||||
]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.CAP_CAPABLE:
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
elif mission_type in [
|
||||
FlightType.CAS,
|
||||
FlightType.BAI,
|
||||
FlightType.OCA_AIRCRAFT,
|
||||
]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.CAS_CAPABLE:
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
elif mission_type in [FlightType.SEAD]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.SEAD_CAPABLE:
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
elif mission_type in [FlightType.DEAD]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.DEAD_CAPABLE:
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
elif mission_type in [FlightType.STRIKE]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.STRIKE_CAPABLE:
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
elif mission_type in [FlightType.ANTISHIP]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.ANTISHIP_CAPABLE:
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
elif mission_type in [FlightType.OCA_RUNWAY]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.RUNWAY_ATTACK_CAPABLE:
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
elif mission_type in [FlightType.AEWC]:
|
||||
if aircraft in gen.flights.ai_flight_planner_db.AEWC_CAPABLE:
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
if aircraft in aircraft_for_task(mission_type):
|
||||
self.addItem(
|
||||
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||
userData=aircraft,
|
||||
)
|
||||
current_aircraft_index = self.findData(current_aircraft)
|
||||
if current_aircraft_index != -1:
|
||||
self.setCurrentIndex(current_aircraft_index)
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
|
||||
from game import Game
|
||||
from game.data.radar_db import UNITS_WITH_RADAR
|
||||
from gen import db
|
||||
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
|
||||
|
||||
|
||||
class SEADTargetInfo:
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
self.location = None
|
||||
self.radars = []
|
||||
self.threat_range = 0
|
||||
self.detection_range = 0
|
||||
|
||||
|
||||
class QSEADTargetSelectionComboBox(QFilteredComboBox):
|
||||
def __init__(self, game: Game, parent=None):
|
||||
super(QSEADTargetSelectionComboBox, self).__init__(parent)
|
||||
self.game = game
|
||||
self.find_possible_sead_targets()
|
||||
|
||||
def get_selected_target(self) -> SEADTargetInfo:
|
||||
n = self.currentText()
|
||||
for target in self.targets:
|
||||
if target.name == n:
|
||||
return target
|
||||
|
||||
def find_possible_sead_targets(self):
|
||||
|
||||
self.targets = []
|
||||
i = 0
|
||||
model = QStandardItemModel()
|
||||
|
||||
def add_model_item(i, model, target):
|
||||
item = QStandardItem(target.name)
|
||||
model.setItem(i, 0, item)
|
||||
self.targets.append(target)
|
||||
return i + 1
|
||||
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured:
|
||||
continue
|
||||
for g in cp.ground_objects:
|
||||
|
||||
radars = []
|
||||
detection_range = 0
|
||||
threat_range = 0
|
||||
if g.dcs_identifier == "AA":
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
utype = db.unit_type_from_name(u.type)
|
||||
|
||||
if utype in UNITS_WITH_RADAR:
|
||||
if (
|
||||
hasattr(utype, "detection_range")
|
||||
and utype.detection_range > 1000
|
||||
):
|
||||
if utype.detection_range > detection_range:
|
||||
detection_range = utype.detection_range
|
||||
radars.append(u)
|
||||
|
||||
if hasattr(utype, "threat_range"):
|
||||
if utype.threat_range > threat_range:
|
||||
threat_range = utype.threat_range
|
||||
if len(radars) > 0:
|
||||
tgt_info = SEADTargetInfo()
|
||||
tgt_info.name = (
|
||||
g.obj_name
|
||||
+ " ["
|
||||
+ ",".join(
|
||||
[db.unit_type_from_name(u.type).id for u in radars]
|
||||
)
|
||||
+ " ]"
|
||||
)
|
||||
if len(tgt_info.name) > 25:
|
||||
tgt_info.name = (
|
||||
g.obj_name + " [" + str(len(radars)) + " units]"
|
||||
)
|
||||
tgt_info.radars = radars
|
||||
tgt_info.location = g
|
||||
tgt_info.threat_range = threat_range
|
||||
tgt_info.detection_range = detection_range
|
||||
i = add_model_item(i, model, tgt_info)
|
||||
|
||||
self.setModel(model)
|
||||
@ -40,7 +40,6 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
from dcs import Point
|
||||
from dcs.mapping import point_from_heading
|
||||
from dcs.planes import F_16C_50
|
||||
from dcs.unitgroup import Group
|
||||
from shapely.geometry import (
|
||||
LineString,
|
||||
@ -167,11 +166,6 @@ class LeafletMap(QWebEngineView, LiberationMap):
|
||||
)
|
||||
self.setPage(self.page)
|
||||
|
||||
self.loadFinished.connect(self.load_finished)
|
||||
|
||||
def load_finished(self) -> None:
|
||||
self.page.runJavaScript(Path("resources/ui/map/map.js").read_text())
|
||||
|
||||
def set_game(self, game: Optional[Game]) -> None:
|
||||
if game is None:
|
||||
self.map_model.clear()
|
||||
@ -567,10 +561,17 @@ class QLiberationMap(QGraphicsView, LiberationMap):
|
||||
origin = self.game.theater.enemy_points()[0]
|
||||
|
||||
package = Package(target)
|
||||
for squadron_list in self.game.air_wing_for(player=True).squadrons.values():
|
||||
squadron = squadron_list[0]
|
||||
break
|
||||
else:
|
||||
logging.error("Player has no squadrons?")
|
||||
return
|
||||
|
||||
flight = Flight(
|
||||
package,
|
||||
self.game.player_country if player else self.game.enemy_country,
|
||||
F_16C_50,
|
||||
self.game.country_for(player),
|
||||
squadron,
|
||||
2,
|
||||
task,
|
||||
start_type="Warm",
|
||||
|
||||
@ -2,16 +2,17 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import List, Optional, Tuple
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
from dcs import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.vehicles import vehicle_map
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint, Polygon
|
||||
from shapely.geometry import LineString, Point as ShapelyPoint, Polygon, MultiPolygon
|
||||
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.navmesh import NavMesh
|
||||
from game.profiling import logged_duration
|
||||
from game.theater import (
|
||||
ConflictTheater,
|
||||
@ -20,6 +21,7 @@ from game.theater import (
|
||||
FrontLine,
|
||||
LatLon,
|
||||
)
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import MultiGroupTransport, TransportMap
|
||||
from game.utils import meters, nautical_miles
|
||||
from gen.ato import AirTaskingOrder
|
||||
@ -31,7 +33,8 @@ from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
|
||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||
|
||||
LeafletLatLon = List[float]
|
||||
LeafletLatLon = list[float]
|
||||
LeafletPoly = list[LeafletLatLon]
|
||||
|
||||
# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL**
|
||||
#
|
||||
@ -51,9 +54,9 @@ LeafletLatLon = List[float]
|
||||
|
||||
def shapely_poly_to_leaflet_points(
|
||||
poly: Polygon, theater: ConflictTheater
|
||||
) -> Optional[List[LeafletLatLon]]:
|
||||
) -> LeafletPoly:
|
||||
if poly.is_empty:
|
||||
return None
|
||||
return []
|
||||
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in poly.exterior.coords]
|
||||
|
||||
|
||||
@ -63,6 +66,7 @@ class ControlPointJs(QObject):
|
||||
positionChanged = Signal()
|
||||
mobileChanged = Signal()
|
||||
destinationChanged = Signal(list)
|
||||
categoryChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -84,6 +88,10 @@ class ControlPointJs(QObject):
|
||||
def blue(self) -> bool:
|
||||
return self.control_point.captured
|
||||
|
||||
@Property(str, notify=categoryChanged)
|
||||
def category(self) -> str:
|
||||
return self.control_point.category
|
||||
|
||||
@Property(list, notify=positionChanged)
|
||||
def position(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.control_point.position)
|
||||
@ -373,7 +381,9 @@ class WaypointJs(QObject):
|
||||
altitudeReferenceChanged = Signal()
|
||||
nameChanged = Signal()
|
||||
timingChanged = Signal()
|
||||
isTargetPointChanged = Signal()
|
||||
isTakeoffChanged = Signal()
|
||||
isLandingChanged = Signal()
|
||||
isDivertChanged = Signal()
|
||||
isBullseyeChanged = Signal()
|
||||
|
||||
@ -432,10 +442,18 @@ class WaypointJs(QObject):
|
||||
return ""
|
||||
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"
|
||||
|
||||
@Property(bool, notify=isTargetPointChanged)
|
||||
def isTargetPoint(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.TARGET_POINT
|
||||
|
||||
@Property(bool, notify=isTakeoffChanged)
|
||||
def isTakeoff(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.TAKEOFF
|
||||
|
||||
@Property(bool, notify=isLandingChanged)
|
||||
def isLanding(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.LANDING_POINT
|
||||
|
||||
@Property(bool, notify=isDivertChanged)
|
||||
def isDivert(self) -> bool:
|
||||
return self.waypoint.waypoint_type is FlightWaypointType.DIVERT
|
||||
@ -512,7 +530,7 @@ class FlightJs(QObject):
|
||||
return self._selected
|
||||
|
||||
@Property(list, notify=commitBoundaryChanged)
|
||||
def commitBoundary(self) -> Optional[List[LeafletLatLon]]:
|
||||
def commitBoundary(self) -> LeafletPoly:
|
||||
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||
return []
|
||||
start = self.flight.flight_plan.patrol_start
|
||||
@ -528,6 +546,118 @@ class FlightJs(QObject):
|
||||
return shapely_poly_to_leaflet_points(bubble, self.theater)
|
||||
|
||||
|
||||
class ThreatZonesJs(QObject):
|
||||
fullChanged = Signal()
|
||||
aircraftChanged = Signal()
|
||||
airDefensesChanged = Signal()
|
||||
radarSamsChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
full: list[LeafletPoly],
|
||||
aircraft: list[LeafletPoly],
|
||||
air_defenses: list[LeafletPoly],
|
||||
radar_sams: list[LeafletPoly],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._full = full
|
||||
self._aircraft = aircraft
|
||||
self._air_defenses = air_defenses
|
||||
self._radar_sams = radar_sams
|
||||
|
||||
@Property(list, notify=fullChanged)
|
||||
def full(self) -> list[LeafletPoly]:
|
||||
return self._full
|
||||
|
||||
@Property(list, notify=aircraftChanged)
|
||||
def aircraft(self) -> list[LeafletPoly]:
|
||||
return self._aircraft
|
||||
|
||||
@Property(list, notify=airDefensesChanged)
|
||||
def airDefenses(self) -> list[LeafletPoly]:
|
||||
return self._air_defenses
|
||||
|
||||
@Property(list, notify=radarSamsChanged)
|
||||
def radarSams(self) -> list[LeafletPoly]:
|
||||
return self._radar_sams
|
||||
|
||||
@staticmethod
|
||||
def polys_to_leaflet(
|
||||
poly: Union[Polygon, MultiPolygon], theater: ConflictTheater
|
||||
) -> list[LeafletPoly]:
|
||||
if isinstance(poly, MultiPolygon):
|
||||
polys = poly.geoms
|
||||
else:
|
||||
polys = [poly]
|
||||
return [shapely_poly_to_leaflet_points(poly, theater) for poly in polys]
|
||||
|
||||
@classmethod
|
||||
def from_zones(cls, zones: ThreatZones, theater: ConflictTheater) -> ThreatZonesJs:
|
||||
return ThreatZonesJs(
|
||||
cls.polys_to_leaflet(zones.all, theater),
|
||||
cls.polys_to_leaflet(zones.airbases, theater),
|
||||
cls.polys_to_leaflet(zones.air_defenses, theater),
|
||||
cls.polys_to_leaflet(zones.radar_sam_threats, theater),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> ThreatZonesJs:
|
||||
return ThreatZonesJs([], [], [], [])
|
||||
|
||||
|
||||
class ThreatZoneContainerJs(QObject):
|
||||
blueChanged = Signal()
|
||||
redChanged = Signal()
|
||||
|
||||
def __init__(self, blue: ThreatZonesJs, red: ThreatZonesJs) -> None:
|
||||
super().__init__()
|
||||
self._blue = blue
|
||||
self._red = red
|
||||
|
||||
@Property(ThreatZonesJs, notify=blueChanged)
|
||||
def blue(self) -> ThreatZonesJs:
|
||||
return self._blue
|
||||
|
||||
@Property(ThreatZonesJs, notify=redChanged)
|
||||
def red(self) -> ThreatZonesJs:
|
||||
return self._red
|
||||
|
||||
|
||||
class NavMeshJs(QObject):
|
||||
blueChanged = Signal()
|
||||
redChanged = Signal()
|
||||
|
||||
def __init__(self, blue: list[LeafletPoly], red: list[LeafletPoly]) -> None:
|
||||
super().__init__()
|
||||
self._blue = blue
|
||||
self._red = red
|
||||
# TODO: Boundary markers.
|
||||
# TODO: Numbering.
|
||||
# TODO: Localization debugging.
|
||||
|
||||
@Property(list, notify=blueChanged)
|
||||
def blue(self) -> list[LeafletPoly]:
|
||||
return self._blue
|
||||
|
||||
@Property(list, notify=redChanged)
|
||||
def red(self) -> list[LeafletPoly]:
|
||||
return self._red
|
||||
|
||||
@staticmethod
|
||||
def to_polys(navmesh: NavMesh, theater: ConflictTheater) -> list[LeafletPoly]:
|
||||
polys = []
|
||||
for poly in navmesh.polys:
|
||||
polys.append(shapely_poly_to_leaflet_points(poly.poly, theater))
|
||||
return polys
|
||||
|
||||
@classmethod
|
||||
def from_game(cls, game: Game) -> NavMeshJs:
|
||||
return NavMeshJs(
|
||||
cls.to_polys(game.blue_navmesh, game.theater),
|
||||
cls.to_polys(game.red_navmesh, game.theater),
|
||||
)
|
||||
|
||||
|
||||
class MapModel(QObject):
|
||||
cleared = Signal()
|
||||
|
||||
@ -537,6 +667,8 @@ class MapModel(QObject):
|
||||
supplyRoutesChanged = Signal()
|
||||
flightsChanged = Signal()
|
||||
frontLinesChanged = Signal()
|
||||
threatZonesChanged = Signal()
|
||||
navmeshesChanged = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
@ -547,6 +679,10 @@ class MapModel(QObject):
|
||||
self._supply_routes = []
|
||||
self._flights = []
|
||||
self._front_lines = []
|
||||
self._threat_zones = ThreatZoneContainerJs(
|
||||
ThreatZonesJs.empty(), ThreatZonesJs.empty()
|
||||
)
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
||||
@ -564,6 +700,10 @@ class MapModel(QObject):
|
||||
self._ground_objects = []
|
||||
self._flights = []
|
||||
self._front_lines = []
|
||||
self._threat_zones = ThreatZoneContainerJs(
|
||||
ThreatZonesJs.empty(), ThreatZonesJs.empty()
|
||||
)
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self.cleared.emit()
|
||||
|
||||
def set_package_selection(self, index: int) -> None:
|
||||
@ -607,6 +747,8 @@ class MapModel(QObject):
|
||||
self.reset_routes()
|
||||
self.reset_atos()
|
||||
self.reset_front_lines()
|
||||
self.reset_threat_zones()
|
||||
self.reset_navmeshes()
|
||||
|
||||
def on_game_load(self, game: Optional[Game]) -> None:
|
||||
if game is not None:
|
||||
@ -730,6 +872,29 @@ class MapModel(QObject):
|
||||
def frontLines(self) -> List[FrontLineJs]:
|
||||
return self._front_lines
|
||||
|
||||
def reset_threat_zones(self) -> None:
|
||||
self._threat_zones = ThreatZoneContainerJs(
|
||||
ThreatZonesJs.from_zones(
|
||||
self.game.threat_zone_for(player=True), self.game.theater
|
||||
),
|
||||
ThreatZonesJs.from_zones(
|
||||
self.game.threat_zone_for(player=False), self.game.theater
|
||||
),
|
||||
)
|
||||
self.threatZonesChanged.emit()
|
||||
|
||||
@Property(ThreatZoneContainerJs, notify=threatZonesChanged)
|
||||
def threatZones(self) -> ThreatZoneContainerJs:
|
||||
return self._threat_zones
|
||||
|
||||
def reset_navmeshes(self) -> None:
|
||||
self._navmeshes = NavMeshJs.from_game(self.game)
|
||||
self.navmeshesChanged.emit()
|
||||
|
||||
@Property(NavMeshJs, notify=navmeshesChanged)
|
||||
def navmeshes(self) -> NavMeshJs:
|
||||
return self._navmeshes
|
||||
|
||||
@property
|
||||
def game(self) -> Game:
|
||||
if self.game_model.game is None:
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
from PySide2.QtGui import QStandardItemModel, QStandardItem
|
||||
from PySide2.QtWidgets import QGroupBox, QVBoxLayout, QListView, QAbstractItemView
|
||||
|
||||
from qt_ui.widgets.combos.QSEADTargetSelectionComboBox import SEADTargetInfo
|
||||
|
||||
|
||||
class QSeadTargetInfoView(QGroupBox):
|
||||
"""
|
||||
UI Component to display info about a sead target
|
||||
"""
|
||||
|
||||
def __init__(self, sead_target_infos: SEADTargetInfo):
|
||||
if sead_target_infos is None:
|
||||
sead_target_infos = SEADTargetInfo()
|
||||
super(QSeadTargetInfoView, self).__init__("Target : " + sead_target_infos.name)
|
||||
self.sead_target_infos = sead_target_infos
|
||||
self.radar_list = QListView()
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(self.radar_list)
|
||||
self.setLayout(layout)
|
||||
|
||||
def setTarget(self, target: SEADTargetInfo):
|
||||
self.setTitle(target.name)
|
||||
self.sead_target_infos = target
|
||||
radar_list_model = QStandardItemModel()
|
||||
self.radar_list.setSelectionMode(QAbstractItemView.NoSelection)
|
||||
for r in self.sead_target_infos.radars:
|
||||
radar_list_model.appendRow(QStandardItem(r.type))
|
||||
self.radar_list.setModel(radar_list_model)
|
||||
92
qt_ui/windows/AirWingDialog.py
Normal file
92
qt_ui/windows/AirWingDialog.py
Normal file
@ -0,0 +1,92 @@
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QItemSelectionModel,
|
||||
QModelIndex,
|
||||
Qt,
|
||||
QSize,
|
||||
)
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QDialog,
|
||||
QListView,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from game import db
|
||||
from game.squadrons import Squadron
|
||||
from qt_ui.delegates import TwoColumnRowDelegate
|
||||
from qt_ui.models import GameModel, AirWingModel, SquadronModel
|
||||
from qt_ui.windows.SquadronDialog import SquadronDialog
|
||||
|
||||
|
||||
class SquadronDelegate(TwoColumnRowDelegate):
|
||||
def __init__(self, air_wing_model: AirWingModel) -> None:
|
||||
super().__init__(rows=2, columns=2, font_size=12)
|
||||
self.air_wing_model = air_wing_model
|
||||
|
||||
@staticmethod
|
||||
def squadron(index: QModelIndex) -> Squadron:
|
||||
return index.data(AirWingModel.SquadronRole)
|
||||
|
||||
def text_for(self, index: QModelIndex, row: int, column: int) -> str:
|
||||
if (row, column) == (0, 0):
|
||||
return self.air_wing_model.data(index, Qt.DisplayRole)
|
||||
elif (row, column) == (0, 1):
|
||||
squadron = self.air_wing_model.data(index, AirWingModel.SquadronRole)
|
||||
return db.unit_get_expanded_info(
|
||||
squadron.country, squadron.aircraft, "name"
|
||||
)
|
||||
elif (row, column) == (1, 0):
|
||||
return self.squadron(index).nickname
|
||||
elif (row, column) == (1, 1):
|
||||
squadron = self.squadron(index)
|
||||
active = len(squadron.active_pilots)
|
||||
available = len(squadron.available_pilots)
|
||||
return f"{squadron.size} pilots, {active} active, {available} unassigned"
|
||||
return ""
|
||||
|
||||
|
||||
class SquadronList(QListView):
|
||||
"""List view for displaying the air wing's squadrons."""
|
||||
|
||||
def __init__(self, air_wing_model: AirWingModel) -> None:
|
||||
super().__init__()
|
||||
self.air_wing_model = air_wing_model
|
||||
self.dialog: Optional[SquadronDialog] = None
|
||||
|
||||
self.setIconSize(QSize(91, 24))
|
||||
self.setItemDelegate(SquadronDelegate(self.air_wing_model))
|
||||
self.setModel(self.air_wing_model)
|
||||
self.selectionModel().setCurrentIndex(
|
||||
self.air_wing_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
|
||||
)
|
||||
|
||||
# self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
self.doubleClicked.connect(self.on_double_click)
|
||||
|
||||
def on_double_click(self, index: QModelIndex) -> None:
|
||||
if not index.isValid():
|
||||
return
|
||||
self.dialog = SquadronDialog(
|
||||
SquadronModel(self.air_wing_model.squadron_at_index(index)), self
|
||||
)
|
||||
self.dialog.show()
|
||||
|
||||
|
||||
class AirWingDialog(QDialog):
|
||||
"""Dialog window showing the player's air wing."""
|
||||
|
||||
def __init__(self, game_model: GameModel, parent) -> None:
|
||||
super().__init__(parent)
|
||||
self.air_wing_model = game_model.blue_air_wing_model
|
||||
|
||||
self.setMinimumSize(1000, 440)
|
||||
self.setWindowTitle(f"Air Wing")
|
||||
# TODO: self.setWindowIcon()
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(SquadronList(self.air_wing_model))
|
||||
@ -1,13 +1,10 @@
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QItemSelection,
|
||||
QItemSelectionModel,
|
||||
QModelIndex,
|
||||
QSize,
|
||||
Qt,
|
||||
)
|
||||
from PySide2.QtGui import QContextMenuEvent, QFont, QFontMetrics, QIcon, QPainter
|
||||
from PySide2.QtGui import QContextMenuEvent
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QAction,
|
||||
@ -16,102 +13,29 @@ from PySide2.QtWidgets import (
|
||||
QListView,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QStyle,
|
||||
QStyleOptionViewItem,
|
||||
QStyledItemDelegate,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from game.transfers import TransferOrder
|
||||
from qt_ui.delegate_helpers import painter_context
|
||||
from qt_ui.delegates import TwoColumnRowDelegate
|
||||
from qt_ui.models import GameModel, TransferModel
|
||||
|
||||
|
||||
class TransferDelegate(QStyledItemDelegate):
|
||||
FONT_SIZE = 10
|
||||
HMARGIN = 4
|
||||
VMARGIN = 4
|
||||
|
||||
class TransferDelegate(TwoColumnRowDelegate):
|
||||
def __init__(self, transfer_model: TransferModel) -> None:
|
||||
super().__init__()
|
||||
super().__init__(rows=2, columns=1, font_size=12)
|
||||
self.transfer_model = transfer_model
|
||||
|
||||
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
||||
font = QFont(option.font)
|
||||
font.setPointSize(self.FONT_SIZE)
|
||||
return font
|
||||
|
||||
@staticmethod
|
||||
def transfer(index: QModelIndex) -> TransferOrder:
|
||||
return index.data(TransferModel.TransferRole)
|
||||
|
||||
def first_row_text(self, index: QModelIndex) -> str:
|
||||
return self.transfer_model.data(index, Qt.DisplayRole)
|
||||
|
||||
def second_row_text(self, index: QModelIndex) -> str:
|
||||
return self.transfer(index).description
|
||||
|
||||
def paint(
|
||||
self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
|
||||
) -> None:
|
||||
# Draw the list item with all the default selection styling, but with an
|
||||
# invalid index so text formatting is left to us.
|
||||
super().paint(painter, option, QModelIndex())
|
||||
|
||||
rect = option.rect.adjusted(
|
||||
self.HMARGIN, self.VMARGIN, -self.HMARGIN, -self.VMARGIN
|
||||
)
|
||||
|
||||
with painter_context(painter):
|
||||
painter.setFont(self.get_font(option))
|
||||
|
||||
icon: Optional[QIcon] = index.data(Qt.DecorationRole)
|
||||
if icon is not None:
|
||||
icon.paint(
|
||||
painter,
|
||||
rect,
|
||||
Qt.AlignLeft | Qt.AlignVCenter,
|
||||
self.icon_mode(option),
|
||||
self.icon_state(option),
|
||||
)
|
||||
|
||||
rect = rect.adjusted(self.icon_size(option).width() + self.HMARGIN, 0, 0, 0)
|
||||
painter.drawText(rect, Qt.AlignLeft, self.first_row_text(index))
|
||||
line2 = rect.adjusted(0, rect.height() / 2, 0, rect.height() / 2)
|
||||
painter.drawText(line2, Qt.AlignLeft, self.second_row_text(index))
|
||||
|
||||
@staticmethod
|
||||
def icon_mode(option: QStyleOptionViewItem) -> QIcon.Mode:
|
||||
if not (option.state & QStyle.State_Enabled):
|
||||
return QIcon.Disabled
|
||||
elif option.state & QStyle.State_Selected:
|
||||
return QIcon.Selected
|
||||
elif option.state & QStyle.State_Active:
|
||||
return QIcon.Active
|
||||
return QIcon.Normal
|
||||
|
||||
@staticmethod
|
||||
def icon_state(option: QStyleOptionViewItem) -> QIcon.State:
|
||||
return QIcon.On if option.state & QStyle.State_Open else QIcon.Off
|
||||
|
||||
@staticmethod
|
||||
def icon_size(option: QStyleOptionViewItem) -> QSize:
|
||||
icon_size: Optional[QSize] = option.decorationSize
|
||||
if icon_size is None:
|
||||
return QSize(0, 0)
|
||||
else:
|
||||
return icon_size
|
||||
|
||||
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
||||
left = self.icon_size(option).width() + self.HMARGIN
|
||||
metrics = QFontMetrics(self.get_font(option))
|
||||
first = metrics.size(0, self.first_row_text(index))
|
||||
second = metrics.size(0, self.second_row_text(index))
|
||||
text_width = max(first.width(), second.width())
|
||||
return QSize(
|
||||
left + text_width + 2 * self.HMARGIN,
|
||||
first.height() + second.height() + 2 * self.VMARGIN,
|
||||
)
|
||||
def text_for(self, index: QModelIndex, row: int, column: int) -> str:
|
||||
if row == 0:
|
||||
return self.transfer_model.data(index, Qt.DisplayRole)
|
||||
elif row == 1:
|
||||
return self.transfer(index).description
|
||||
return ""
|
||||
|
||||
|
||||
class PendingTransfersList(QListView):
|
||||
|
||||
155
qt_ui/windows/SquadronDialog.py
Normal file
155
qt_ui/windows/SquadronDialog.py
Normal file
@ -0,0 +1,155 @@
|
||||
import logging
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QItemSelectionModel,
|
||||
QModelIndex,
|
||||
Qt,
|
||||
QItemSelection,
|
||||
)
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QDialog,
|
||||
QListView,
|
||||
QVBoxLayout,
|
||||
QPushButton,
|
||||
QHBoxLayout,
|
||||
)
|
||||
|
||||
from game.squadrons import Pilot
|
||||
from qt_ui.delegates import TwoColumnRowDelegate
|
||||
from qt_ui.models import SquadronModel
|
||||
|
||||
|
||||
class PilotDelegate(TwoColumnRowDelegate):
|
||||
def __init__(self, squadron_model: SquadronModel) -> None:
|
||||
super().__init__(rows=2, columns=2, font_size=12)
|
||||
self.squadron_model = squadron_model
|
||||
|
||||
@staticmethod
|
||||
def pilot(index: QModelIndex) -> Pilot:
|
||||
return index.data(SquadronModel.PilotRole)
|
||||
|
||||
def text_for(self, index: QModelIndex, row: int, column: int) -> str:
|
||||
pilot = self.pilot(index)
|
||||
if (row, column) == (0, 0):
|
||||
return self.squadron_model.data(index, Qt.DisplayRole)
|
||||
elif (row, column) == (0, 1):
|
||||
flown = pilot.record.missions_flown
|
||||
missions = "missions" if flown != 1 else "mission"
|
||||
return f"{flown} {missions} flown"
|
||||
elif (row, column) == (1, 0):
|
||||
return "Player" if pilot.player else "AI"
|
||||
elif (row, column) == (1, 1):
|
||||
return pilot.status.value
|
||||
return ""
|
||||
|
||||
|
||||
class PilotList(QListView):
|
||||
"""List view for displaying a squadron's pilots."""
|
||||
|
||||
def __init__(self, squadron_model: SquadronModel) -> None:
|
||||
super().__init__()
|
||||
self.squadron_model = squadron_model
|
||||
|
||||
self.setItemDelegate(PilotDelegate(self.squadron_model))
|
||||
self.setModel(self.squadron_model)
|
||||
self.selectionModel().setCurrentIndex(
|
||||
self.squadron_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
|
||||
)
|
||||
|
||||
# self.setIconSize(QSize(91, 24))
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
|
||||
|
||||
class SquadronDialog(QDialog):
|
||||
"""Dialog window showing a squadron."""
|
||||
|
||||
def __init__(self, squadron_model: SquadronModel, parent) -> None:
|
||||
super().__init__(parent)
|
||||
self.squadron_model = squadron_model
|
||||
|
||||
self.setMinimumSize(1000, 440)
|
||||
self.setWindowTitle(str(squadron_model.squadron))
|
||||
# TODO: self.setWindowIcon()
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
self.pilot_list = PilotList(squadron_model)
|
||||
self.pilot_list.selectionModel().selectionChanged.connect(
|
||||
self.on_selection_changed
|
||||
)
|
||||
layout.addWidget(self.pilot_list)
|
||||
|
||||
button_panel = QHBoxLayout()
|
||||
button_panel.addStretch()
|
||||
layout.addLayout(button_panel)
|
||||
|
||||
self.toggle_ai_button = QPushButton()
|
||||
self.reset_ai_toggle_state(self.pilot_list.currentIndex())
|
||||
self.toggle_ai_button.setProperty("style", "start-button")
|
||||
self.toggle_ai_button.clicked.connect(self.toggle_ai)
|
||||
button_panel.addWidget(self.toggle_ai_button, alignment=Qt.AlignRight)
|
||||
|
||||
self.toggle_leave_button = QPushButton()
|
||||
self.reset_leave_toggle_state(self.pilot_list.currentIndex())
|
||||
self.toggle_leave_button.setProperty("style", "start-button")
|
||||
self.toggle_leave_button.clicked.connect(self.toggle_leave)
|
||||
button_panel.addWidget(self.toggle_leave_button, alignment=Qt.AlignRight)
|
||||
|
||||
def check_disabled_button_states(
|
||||
self, button: QPushButton, index: QModelIndex
|
||||
) -> bool:
|
||||
if not index.isValid():
|
||||
button.setText("No pilot selected")
|
||||
button.setDisabled(True)
|
||||
return True
|
||||
pilot = self.squadron_model.pilot_at_index(index)
|
||||
if not pilot.alive:
|
||||
button.setText("Pilot is dead")
|
||||
button.setDisabled(True)
|
||||
return True
|
||||
return False
|
||||
|
||||
def toggle_ai(self) -> None:
|
||||
index = self.pilot_list.currentIndex()
|
||||
if not index.isValid():
|
||||
logging.error("Cannot toggle player/AI: no pilot is selected")
|
||||
return
|
||||
self.squadron_model.toggle_ai_state(index)
|
||||
|
||||
def reset_ai_toggle_state(self, index: QModelIndex) -> None:
|
||||
if self.check_disabled_button_states(self.toggle_ai_button, index):
|
||||
return
|
||||
if not self.squadron_model.squadron.aircraft.flyable:
|
||||
self.toggle_ai_button.setText("Not flyable")
|
||||
self.toggle_ai_button.setDisabled(True)
|
||||
return
|
||||
self.toggle_ai_button.setEnabled(True)
|
||||
pilot = self.squadron_model.pilot_at_index(index)
|
||||
self.toggle_ai_button.setText(
|
||||
"Convert to AI" if pilot.player else "Convert to player"
|
||||
)
|
||||
|
||||
def toggle_leave(self) -> None:
|
||||
index = self.pilot_list.currentIndex()
|
||||
if not index.isValid():
|
||||
logging.error("Cannot toggle on leave state: no pilot is selected")
|
||||
return
|
||||
self.squadron_model.toggle_leave_state(index)
|
||||
|
||||
def reset_leave_toggle_state(self, index: QModelIndex) -> None:
|
||||
if self.check_disabled_button_states(self.toggle_leave_button, index):
|
||||
return
|
||||
pilot = self.squadron_model.pilot_at_index(index)
|
||||
self.toggle_leave_button.setEnabled(True)
|
||||
self.toggle_leave_button.setText(
|
||||
"Return from leave" if pilot.on_leave else "Send on leave"
|
||||
)
|
||||
|
||||
def on_selection_changed(
|
||||
self, selected: QItemSelection, _deselected: QItemSelection
|
||||
) -> None:
|
||||
index = selected.indexes()[0]
|
||||
self.reset_ai_toggle_state(index)
|
||||
self.reset_leave_toggle_state(index)
|
||||
@ -4,7 +4,7 @@ import logging
|
||||
from collections import defaultdict
|
||||
from typing import Callable, Dict, Type
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtCore import Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
@ -153,6 +153,8 @@ class TransferControls(QGroupBox):
|
||||
|
||||
|
||||
class ScrollingUnitTransferGrid(QFrame):
|
||||
transfer_quantity_changed = Signal()
|
||||
|
||||
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
self.cp = cp
|
||||
@ -229,6 +231,7 @@ class ScrollingUnitTransferGrid(QFrame):
|
||||
origin_inventory -= 1
|
||||
controls.set_quantity(self.transfers[unit_type])
|
||||
origin_inventory_label.setText(str(origin_inventory))
|
||||
self.transfer_quantity_changed.emit()
|
||||
|
||||
def decrease(controls: TransferControls):
|
||||
nonlocal origin_inventory
|
||||
@ -240,6 +243,7 @@ class ScrollingUnitTransferGrid(QFrame):
|
||||
origin_inventory += 1
|
||||
controls.set_quantity(self.transfers[unit_type])
|
||||
origin_inventory_label.setText(str(origin_inventory))
|
||||
self.transfer_quantity_changed.emit()
|
||||
|
||||
transfer_controls = TransferControls("->", increase, "<-", decrease)
|
||||
|
||||
@ -276,11 +280,15 @@ class NewUnitTransferDialog(QDialog):
|
||||
layout.addLayout(self.dest_panel)
|
||||
|
||||
self.transfer_panel = ScrollingUnitTransferGrid(origin, game_model)
|
||||
self.transfer_panel.transfer_quantity_changed.connect(
|
||||
self.on_transfer_quantity_changed
|
||||
)
|
||||
layout.addWidget(self.transfer_panel)
|
||||
|
||||
self.submit_button = QPushButton("Create Transfer Order", parent=self)
|
||||
self.submit_button.clicked.connect(self.on_submit)
|
||||
self.submit_button.setProperty("style", "start-button")
|
||||
self.submit_button.setDisabled(True)
|
||||
layout.addWidget(self.submit_button)
|
||||
|
||||
def on_submit(self) -> None:
|
||||
@ -303,3 +311,7 @@ class NewUnitTransferDialog(QDialog):
|
||||
)
|
||||
self.game_model.transfer_model.new_transfer(transfer)
|
||||
self.close()
|
||||
|
||||
def on_transfer_quantity_changed(self) -> None:
|
||||
has_transfer_items = any(self.transfer_panel.transfers.values())
|
||||
self.submit_button.setDisabled(not has_transfer_items)
|
||||
|
||||
@ -95,6 +95,12 @@ class QBaseMenu2(QDialog):
|
||||
bottom_row.addWidget(transfer_button)
|
||||
transfer_button.clicked.connect(self.open_transfer_dialog)
|
||||
|
||||
if self.cheat_capturable:
|
||||
capture_button = QPushButton("CHEAT: Capture")
|
||||
capture_button.setProperty("style", "btn-danger")
|
||||
bottom_row.addWidget(capture_button)
|
||||
capture_button.clicked.connect(self.cheat_capture)
|
||||
|
||||
self.budget_display = QLabel(
|
||||
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget)
|
||||
)
|
||||
@ -104,6 +110,26 @@ class QBaseMenu2(QDialog):
|
||||
GameUpdateSignal.get_instance().budgetupdated.connect(self.update_budget)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
@property
|
||||
def cheat_capturable(self) -> bool:
|
||||
if not self.game_model.game.settings.enable_base_capture_cheat:
|
||||
return False
|
||||
if self.cp.captured:
|
||||
return False
|
||||
|
||||
for connected in self.cp.connected_points:
|
||||
if connected.captured:
|
||||
return True
|
||||
return False
|
||||
|
||||
def cheat_capture(self) -> None:
|
||||
self.cp.capture(self.game_model.game, for_player=True)
|
||||
# Reinitialized ground planners and the like. The ATO needs to be reset because
|
||||
# missions planned against the flipped base are no longer valid.
|
||||
self.game_model.game.reset_ato()
|
||||
self.game_model.game.initialize_turn()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
|
||||
@property
|
||||
def has_transfer_destinations(self) -> bool:
|
||||
return self.game_model.game.transit_network_for(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import itertools
|
||||
|
||||
from PySide2.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
@ -42,9 +43,9 @@ class ScrollingFrame(QFrame):
|
||||
|
||||
|
||||
class EconomyIntelTab(ScrollingFrame):
|
||||
def __init__(self, game: Game) -> None:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
super().__init__()
|
||||
self.addLayout(FinancesLayout(game, player=False))
|
||||
self.addLayout(FinancesLayout(game, player=player))
|
||||
|
||||
|
||||
class IntelTableLayout(QGridLayout):
|
||||
@ -93,9 +94,9 @@ class AircraftIntelLayout(IntelTableLayout):
|
||||
|
||||
|
||||
class AircraftIntelTab(ScrollingFrame):
|
||||
def __init__(self, game: Game) -> None:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
super().__init__()
|
||||
self.addLayout(AircraftIntelLayout(game, player=False))
|
||||
self.addLayout(AircraftIntelLayout(game, player=player))
|
||||
|
||||
|
||||
class ArmyIntelLayout(IntelTableLayout):
|
||||
@ -120,18 +121,18 @@ class ArmyIntelLayout(IntelTableLayout):
|
||||
|
||||
|
||||
class ArmyIntelTab(ScrollingFrame):
|
||||
def __init__(self, game: Game) -> None:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
super().__init__()
|
||||
self.addLayout(ArmyIntelLayout(game, player=False))
|
||||
self.addLayout(ArmyIntelLayout(game, player=player))
|
||||
|
||||
|
||||
class IntelTabs(QTabWidget):
|
||||
def __init__(self, game: Game):
|
||||
def __init__(self, game: Game, player: bool):
|
||||
super().__init__()
|
||||
|
||||
self.addTab(EconomyIntelTab(game), "Economy")
|
||||
self.addTab(AircraftIntelTab(game), "Air forces")
|
||||
self.addTab(ArmyIntelTab(game), "Ground forces")
|
||||
self.addTab(EconomyIntelTab(game, player), "Economy")
|
||||
self.addTab(AircraftIntelTab(game, player), "Air forces")
|
||||
self.addTab(ArmyIntelTab(game, player), "Ground forces")
|
||||
|
||||
|
||||
class IntelWindow(QDialog):
|
||||
@ -139,12 +140,42 @@ class IntelWindow(QDialog):
|
||||
super().__init__()
|
||||
|
||||
self.game = game
|
||||
self.player = True
|
||||
self.setModal(True)
|
||||
self.setWindowTitle("Intelligence")
|
||||
self.setWindowIcon(ICONS["Statistics"])
|
||||
self.setMinimumSize(600, 500)
|
||||
self.selected_intel_tab = 0
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
self.refresh_layout()
|
||||
|
||||
layout.addWidget(IntelTabs(game), stretch=1)
|
||||
def on_faction_changed(self) -> None:
|
||||
self.player = not self.player
|
||||
self.refresh_layout()
|
||||
|
||||
def refresh_layout(self) -> None:
|
||||
|
||||
# Clear the existing layout
|
||||
if self.layout():
|
||||
idx = 0
|
||||
while child := self.layout().itemAt(idx):
|
||||
self.layout().removeItem(child)
|
||||
|
||||
# Add the new layout
|
||||
own_faction = QCheckBox("Enemy Info")
|
||||
own_faction.setChecked(not self.player)
|
||||
own_faction.stateChanged.connect(self.on_faction_changed)
|
||||
|
||||
intel_tabs = IntelTabs(self.game, self.player)
|
||||
intel_tabs.currentChanged.connect(self.on_tab_changed)
|
||||
|
||||
if self.selected_intel_tab:
|
||||
intel_tabs.setCurrentIndex(self.selected_intel_tab)
|
||||
|
||||
self.layout().addWidget(own_faction)
|
||||
self.layout().addWidget(intel_tabs, stretch=1)
|
||||
|
||||
def on_tab_changed(self, idx: int) -> None:
|
||||
self.selected_intel_tab = idx
|
||||
|
||||
@ -215,7 +215,9 @@ class QNewPackageDialog(QPackageDialog):
|
||||
self, game_model: GameModel, model: AtoModel, target: MissionTarget, parent=None
|
||||
) -> None:
|
||||
super().__init__(
|
||||
game_model, PackageModel(Package(target), game_model), parent=parent
|
||||
game_model,
|
||||
PackageModel(Package(target, auto_asap=True), game_model),
|
||||
parent=parent,
|
||||
)
|
||||
self.ato_model = model
|
||||
|
||||
@ -237,6 +239,7 @@ class QNewPackageDialog(QPackageDialog):
|
||||
super().on_cancel()
|
||||
for flight in self.package_model.package.flights:
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
flight.clear_roster()
|
||||
|
||||
|
||||
class QEditPackageDialog(QPackageDialog):
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from typing import Optional
|
||||
import logging
|
||||
from typing import Optional, Type
|
||||
|
||||
from PySide2.QtCore import Qt, Signal
|
||||
from PySide2.QtWidgets import (
|
||||
@ -10,9 +11,10 @@ from PySide2.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QLineEdit,
|
||||
)
|
||||
from dcs.planes import PlaneType
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import Game
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ControlPoint, OffMapSpawn
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
@ -23,6 +25,7 @@ from qt_ui.widgets.combos.QAircraftTypeSelector import QAircraftTypeSelector
|
||||
from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelector
|
||||
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
||||
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
|
||||
from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector
|
||||
|
||||
|
||||
class QFlightCreator(QDialog):
|
||||
@ -55,6 +58,14 @@ class QFlightCreator(QDialog):
|
||||
self.aircraft_selector.currentIndexChanged.connect(self.on_aircraft_changed)
|
||||
layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector))
|
||||
|
||||
self.squadron_selector = SquadronSelector(
|
||||
self.game.air_wing_for(player=True),
|
||||
self.task_selector.currentData(),
|
||||
self.aircraft_selector.currentData(),
|
||||
)
|
||||
self.squadron_selector.setCurrentIndex(0)
|
||||
layout.addLayout(QLabeledWidget("Squadron:", self.squadron_selector))
|
||||
|
||||
self.departure = QOriginAirfieldSelector(
|
||||
self.game.aircraft_inventory,
|
||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
||||
@ -132,13 +143,16 @@ class QFlightCreator(QDialog):
|
||||
self.custom_name_text = text
|
||||
|
||||
def verify_form(self) -> Optional[str]:
|
||||
aircraft: PlaneType = self.aircraft_selector.currentData()
|
||||
origin: ControlPoint = self.departure.currentData()
|
||||
arrival: ControlPoint = self.arrival.currentData()
|
||||
divert: ControlPoint = self.divert.currentData()
|
||||
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
|
||||
squadron: Optional[Squadron] = self.squadron_selector.currentData()
|
||||
origin: Optional[ControlPoint] = self.departure.currentData()
|
||||
arrival: Optional[ControlPoint] = self.arrival.currentData()
|
||||
divert: Optional[ControlPoint] = self.divert.currentData()
|
||||
size: int = self.flight_size_spinner.value()
|
||||
if aircraft is None:
|
||||
return "You must select an aircraft type."
|
||||
if squadron is None:
|
||||
return "You must select a squadron."
|
||||
if not origin.captured:
|
||||
return f"{origin.name} is not owned by your coalition."
|
||||
if arrival is not None and not arrival.captured:
|
||||
@ -163,7 +177,7 @@ class QFlightCreator(QDialog):
|
||||
return
|
||||
|
||||
task = self.task_selector.currentData()
|
||||
aircraft = self.aircraft_selector.currentData()
|
||||
squadron = self.squadron_selector.currentData()
|
||||
origin = self.departure.currentData()
|
||||
arrival = self.arrival.currentData()
|
||||
divert = self.divert.currentData()
|
||||
@ -175,7 +189,7 @@ class QFlightCreator(QDialog):
|
||||
flight = Flight(
|
||||
self.package,
|
||||
self.country,
|
||||
aircraft,
|
||||
squadron,
|
||||
size,
|
||||
task,
|
||||
self.start_type.currentText(),
|
||||
@ -184,7 +198,14 @@ class QFlightCreator(QDialog):
|
||||
divert,
|
||||
custom_name=self.custom_name_text,
|
||||
)
|
||||
flight.client_count = self.client_slots_spinner.value()
|
||||
for pilot, idx in zip(flight.pilots, range(self.client_slots_spinner.value())):
|
||||
if pilot is None:
|
||||
logging.error(
|
||||
f"Cannot create client slot because {flight} has no pilot for "
|
||||
f"aircraft {idx}"
|
||||
)
|
||||
continue
|
||||
pilot.player = True
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.created.emit(flight)
|
||||
@ -192,6 +213,9 @@ class QFlightCreator(QDialog):
|
||||
|
||||
def on_aircraft_changed(self, index: int) -> None:
|
||||
new_aircraft = self.aircraft_selector.itemData(index)
|
||||
self.squadron_selector.update_items(
|
||||
self.task_selector.currentData(), new_aircraft
|
||||
)
|
||||
self.departure.change_aircraft(new_aircraft)
|
||||
self.arrival.change_aircraft(new_aircraft)
|
||||
self.divert.change_aircraft(new_aircraft)
|
||||
@ -211,10 +235,13 @@ class QFlightCreator(QDialog):
|
||||
self.restore_start_type = None
|
||||
|
||||
def on_task_changed(self) -> None:
|
||||
self.aircraft_selector.updateItems(
|
||||
self.aircraft_selector.update_items(
|
||||
self.task_selector.currentData(),
|
||||
self.game.aircraft_inventory.available_types_for_player,
|
||||
)
|
||||
self.squadron_selector.update_items(
|
||||
self.task_selector.currentData(), self.aircraft_selector.currentData()
|
||||
)
|
||||
|
||||
def update_max_size(self, available: int) -> None:
|
||||
self.flight_size_spinner.setMaximum(min(available, 4))
|
||||
|
||||
@ -19,6 +19,7 @@ class QFlightPlanner(QTabWidget):
|
||||
)
|
||||
self.payload_tab = QFlightPayloadTab(flight, game)
|
||||
self.waypoint_tab = QFlightWaypointTab(game, package_model.package, flight)
|
||||
self.waypoint_tab.loadout_changed.connect(self.payload_tab.reload_from_flight)
|
||||
self.addTab(self.general_settings_tab, "General Flight settings")
|
||||
self.addTab(self.payload_tab, "Payload")
|
||||
self.addTab(self.waypoint_tab, "Waypoints")
|
||||
|
||||
48
qt_ui/windows/mission/flight/SquadronSelector.py
Normal file
48
qt_ui/windows/mission/flight/SquadronSelector.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""Combo box for selecting squadrons."""
|
||||
from typing import Type, Optional
|
||||
|
||||
from PySide2.QtWidgets import QComboBox
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.squadrons import AirWing
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class SquadronSelector(QComboBox):
|
||||
"""Combo box for selecting squadrons compatible with the given requirements."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
air_wing: AirWing,
|
||||
task: Optional[FlightType],
|
||||
aircraft: Optional[Type[FlyingType]],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.air_wing = air_wing
|
||||
|
||||
self.model().sort(0)
|
||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||
self.update_items(task, aircraft)
|
||||
|
||||
def update_items(
|
||||
self, task: Optional[FlightType], aircraft: Optional[Type[FlyingType]]
|
||||
) -> None:
|
||||
current_squadron = self.currentData()
|
||||
self.clear()
|
||||
if task is None:
|
||||
self.addItem("No task selected", None)
|
||||
return
|
||||
if aircraft is None:
|
||||
self.addItem("No aircraft selected", None)
|
||||
return
|
||||
|
||||
for squadron in self.air_wing.squadrons_for(aircraft):
|
||||
if task in squadron.mission_types:
|
||||
self.addItem(f"{squadron}", squadron)
|
||||
|
||||
if self.count() == 0:
|
||||
self.addItem("No capable aircraft available", None)
|
||||
return
|
||||
|
||||
if current_squadron is not None:
|
||||
self.setCurrentText(f"{current_squadron}")
|
||||
@ -40,6 +40,9 @@ class QFlightPayloadTab(QFrame):
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def reload_from_flight(self) -> None:
|
||||
self.loadout_selector.setCurrentText(self.flight.loadout.name)
|
||||
|
||||
def on_new_loadout(self, index: int) -> None:
|
||||
self.flight.loadout = self.loadout_selector.itemData(index)
|
||||
self.payload_editor.reset_pylons()
|
||||
@ -49,5 +52,5 @@ class QFlightPayloadTab(QFrame):
|
||||
if use_custom:
|
||||
self.flight.loadout = self.flight.loadout.derive_custom("Custom")
|
||||
else:
|
||||
self.flight.loadout = Loadout.default_for(self.flight)
|
||||
self.flight.loadout = self.loadout_selector.currentData()
|
||||
self.payload_editor.reset_pylons()
|
||||
|
||||
@ -1,17 +1,174 @@
|
||||
import logging
|
||||
from typing import Optional, Callable
|
||||
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtWidgets import QLabel, QGroupBox, QSpinBox, QGridLayout
|
||||
from PySide2.QtCore import Signal, QModelIndex
|
||||
from PySide2.QtWidgets import (
|
||||
QLabel,
|
||||
QGroupBox,
|
||||
QSpinBox,
|
||||
QGridLayout,
|
||||
QComboBox,
|
||||
QHBoxLayout,
|
||||
QCheckBox,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.squadrons import Pilot
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.models import PackageModel
|
||||
|
||||
|
||||
class PilotSelector(QComboBox):
|
||||
available_pilots_changed = Signal()
|
||||
|
||||
def __init__(self, flight: Flight, idx: int) -> None:
|
||||
super().__init__()
|
||||
self.flight = flight
|
||||
self.pilot_index = idx
|
||||
self.rebuild()
|
||||
|
||||
@staticmethod
|
||||
def text_for(pilot: Pilot) -> str:
|
||||
return pilot.name
|
||||
|
||||
def _do_rebuild(self) -> None:
|
||||
self.clear()
|
||||
if self.pilot_index >= self.flight.count:
|
||||
self.addItem("No aircraft", None)
|
||||
self.setDisabled(True)
|
||||
return
|
||||
|
||||
self.setEnabled(True)
|
||||
self.addItem("Unassigned", None)
|
||||
choices = list(self.flight.squadron.available_pilots)
|
||||
current_pilot = self.flight.pilots[self.pilot_index]
|
||||
if current_pilot is not None:
|
||||
choices.append(current_pilot)
|
||||
# Put players first, otherwise alphabetically.
|
||||
for pilot in sorted(choices, key=lambda p: (not p.player, p.name)):
|
||||
self.addItem(self.text_for(pilot), pilot)
|
||||
if current_pilot is None:
|
||||
self.setCurrentText("Unassigned")
|
||||
else:
|
||||
self.setCurrentText(self.text_for(current_pilot))
|
||||
self.currentIndexChanged.connect(self.replace_pilot)
|
||||
|
||||
def rebuild(self) -> None:
|
||||
# The contents of the selector depend on the selection of the other selectors
|
||||
# for the flight, so changing the selection of one causes each selector to
|
||||
# rebuild. A rebuild causes a selection change, so if we don't block signals
|
||||
# during a rebuild we'll never stop rebuilding.
|
||||
self.blockSignals(True)
|
||||
try:
|
||||
self._do_rebuild()
|
||||
finally:
|
||||
self.blockSignals(False)
|
||||
|
||||
def replace_pilot(self, index: QModelIndex) -> None:
|
||||
if self.itemText(index) == "No aircraft":
|
||||
# The roster resize is handled separately, so we have no pilots to remove.
|
||||
return
|
||||
pilot = self.itemData(index)
|
||||
if pilot == self.flight.pilots[self.pilot_index]:
|
||||
return
|
||||
self.flight.set_pilot(self.pilot_index, pilot)
|
||||
self.available_pilots_changed.emit()
|
||||
|
||||
|
||||
class PilotControls(QHBoxLayout):
|
||||
def __init__(self, flight: Flight, idx: int) -> None:
|
||||
super().__init__()
|
||||
self.flight = flight
|
||||
self.pilot_index = idx
|
||||
|
||||
self.selector = PilotSelector(flight, idx)
|
||||
self.selector.currentIndexChanged.connect(self.on_pilot_changed)
|
||||
self.addWidget(self.selector)
|
||||
|
||||
self.player_checkbox = QCheckBox()
|
||||
self.player_checkbox.setToolTip("Checked if this pilot is a player.")
|
||||
self.on_pilot_changed(self.selector.currentIndex())
|
||||
self.addWidget(self.player_checkbox)
|
||||
|
||||
self.player_checkbox.toggled.connect(self.on_player_toggled)
|
||||
|
||||
@property
|
||||
def pilot(self) -> Optional[Pilot]:
|
||||
if self.pilot_index >= self.flight.count:
|
||||
return None
|
||||
return self.flight.pilots[self.pilot_index]
|
||||
|
||||
def on_player_toggled(self, checked: bool) -> None:
|
||||
pilot = self.pilot
|
||||
if pilot is None:
|
||||
logging.error("Cannot toggle state of a pilot when none is selected")
|
||||
return
|
||||
pilot.player = checked
|
||||
|
||||
def on_pilot_changed(self, index: int) -> None:
|
||||
pilot = self.selector.itemData(index)
|
||||
self.player_checkbox.blockSignals(True)
|
||||
try:
|
||||
self.player_checkbox.setChecked(pilot is not None and pilot.player)
|
||||
finally:
|
||||
self.player_checkbox.blockSignals(False)
|
||||
|
||||
def update_available_pilots(self) -> None:
|
||||
self.selector.rebuild()
|
||||
|
||||
def enable_and_reset(self) -> None:
|
||||
self.selector.rebuild()
|
||||
self.on_pilot_changed(self.selector.currentIndex())
|
||||
|
||||
def disable_and_clear(self) -> None:
|
||||
self.selector.rebuild()
|
||||
self.player_checkbox.blockSignals(True)
|
||||
try:
|
||||
self.player_checkbox.setEnabled(False)
|
||||
self.player_checkbox.setChecked(False)
|
||||
finally:
|
||||
self.player_checkbox.blockSignals(False)
|
||||
|
||||
|
||||
class FlightRosterEditor(QVBoxLayout):
|
||||
MAX_PILOTS = 4
|
||||
|
||||
def __init__(self, flight: Flight) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.pilot_controls = []
|
||||
for pilot_idx in range(self.MAX_PILOTS):
|
||||
|
||||
def make_reset_callback(source_idx: int) -> Callable[[int], None]:
|
||||
def callback() -> None:
|
||||
self.update_available_pilots(source_idx)
|
||||
|
||||
return callback
|
||||
|
||||
controls = PilotControls(flight, pilot_idx)
|
||||
controls.selector.available_pilots_changed.connect(
|
||||
make_reset_callback(pilot_idx)
|
||||
)
|
||||
self.pilot_controls.append(controls)
|
||||
self.addLayout(controls)
|
||||
|
||||
def update_available_pilots(self, source_idx: int) -> None:
|
||||
for idx, controls in enumerate(self.pilot_controls):
|
||||
# No need to reset the source of the reset, it was just manually selected.
|
||||
if idx != source_idx:
|
||||
controls.update_available_pilots()
|
||||
|
||||
def resize(self, new_size: int) -> None:
|
||||
if new_size > self.MAX_PILOTS:
|
||||
raise ValueError("A flight may not have more than four pilots.")
|
||||
for controls in self.pilot_controls[:new_size]:
|
||||
controls.enable_and_reset()
|
||||
for controls in self.pilot_controls[new_size:]:
|
||||
controls.disable_and_clear()
|
||||
|
||||
|
||||
class QFlightSlotEditor(QGroupBox):
|
||||
|
||||
changed = Signal()
|
||||
|
||||
def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
|
||||
super().__init__("Slots")
|
||||
self.package_model = package_model
|
||||
@ -32,52 +189,35 @@ class QFlightSlotEditor(QGroupBox):
|
||||
self.aircraft_count_spinner.setValue(flight.count)
|
||||
self.aircraft_count_spinner.valueChanged.connect(self._changed_aircraft_count)
|
||||
|
||||
self.client_count = QLabel("Client slots count:")
|
||||
self.client_count_spinner = QSpinBox()
|
||||
self.client_count_spinner.setMinimum(0)
|
||||
self.client_count_spinner.setMaximum(max_count)
|
||||
self.client_count_spinner.setValue(flight.client_count)
|
||||
self.client_count_spinner.valueChanged.connect(self._changed_client_count)
|
||||
|
||||
if not self.flight.unit_type.flyable:
|
||||
self.client_count_spinner.setValue(0)
|
||||
self.client_count_spinner.setEnabled(False)
|
||||
|
||||
layout.addWidget(self.aircraft_count, 0, 0)
|
||||
layout.addWidget(self.aircraft_count_spinner, 0, 1)
|
||||
|
||||
layout.addWidget(self.client_count, 1, 0)
|
||||
layout.addWidget(self.client_count_spinner, 1, 1)
|
||||
layout.addWidget(QLabel("Squadron:"), 1, 0)
|
||||
layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1)
|
||||
|
||||
layout.addWidget(QLabel("Assigned pilots:"), 2, 0)
|
||||
self.roster_editor = FlightRosterEditor(flight)
|
||||
layout.addLayout(self.roster_editor, 2, 1)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def _changed_aircraft_count(self):
|
||||
self.game.aircraft_inventory.return_from_flight(self.flight)
|
||||
old_count = self.flight.count
|
||||
self.flight.count = int(self.aircraft_count_spinner.value())
|
||||
new_count = int(self.aircraft_count_spinner.value())
|
||||
try:
|
||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
||||
except ValueError:
|
||||
# The UI should have prevented this, but if we ran out of aircraft
|
||||
# then roll back the inventory change.
|
||||
difference = self.flight.count - old_count
|
||||
difference = new_count - self.flight.count
|
||||
available = self.inventory.available(self.flight.unit_type)
|
||||
logging.error(
|
||||
f"Could not add {difference} additional aircraft to "
|
||||
f"{self.flight} because {self.flight.from_cp} has only "
|
||||
f"{self.flight} because {self.flight.departure} has only "
|
||||
f"{available} {self.flight.unit_type} remaining"
|
||||
)
|
||||
self.flight.count = old_count
|
||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
||||
self.changed.emit()
|
||||
return
|
||||
|
||||
def _changed_client_count(self):
|
||||
self.flight.client_count = int(self.client_count_spinner.value())
|
||||
self._cap_client_count()
|
||||
self.package_model.update_tot()
|
||||
self.changed.emit()
|
||||
|
||||
def _cap_client_count(self):
|
||||
if self.flight.client_count > self.flight.count:
|
||||
self.flight.client_count = self.flight.count
|
||||
self.client_count_spinner.setValue(self.flight.client_count)
|
||||
self.flight.resize(new_count)
|
||||
self.roster_editor.resize(new_count)
|
||||
|
||||
@ -21,9 +21,6 @@ class QFlightWaypointList(QTableView):
|
||||
|
||||
header = self.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||
|
||||
if len(self.flight.points) > 0:
|
||||
self.selectedPoint = self.flight.points[0]
|
||||
self.update_list()
|
||||
|
||||
self.selectionModel().setCurrentIndex(
|
||||
@ -31,6 +28,9 @@ class QFlightWaypointList(QTableView):
|
||||
)
|
||||
|
||||
def update_list(self):
|
||||
# We need to keep just the row and rebuild the index later because the
|
||||
# QModelIndex will not be valid after the model is cleared.
|
||||
current_index = self.currentIndex().row()
|
||||
self.model.clear()
|
||||
|
||||
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
|
||||
@ -39,7 +39,7 @@ class QFlightWaypointList(QTableView):
|
||||
for row, waypoint in enumerate(waypoints):
|
||||
self.add_waypoint_row(row, self.flight, waypoint)
|
||||
self.selectionModel().setCurrentIndex(
|
||||
self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select
|
||||
self.model.index(current_index, 0), QItemSelectionModel.Select
|
||||
)
|
||||
self.resizeColumnsToContents()
|
||||
total_column_width = self.verticalHeader().width() + self.lineWidth()
|
||||
|
||||
@ -20,6 +20,7 @@ from gen.flights.flightplan import (
|
||||
PlanningError,
|
||||
StrikeFlightPlan,
|
||||
)
|
||||
from gen.flights.loadouts import Loadout
|
||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import (
|
||||
QFlightWaypointList,
|
||||
)
|
||||
@ -29,6 +30,8 @@ from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow i
|
||||
|
||||
|
||||
class QFlightWaypointTab(QFrame):
|
||||
loadout_changed = Signal()
|
||||
|
||||
def __init__(self, game: Game, package: Package, flight: Flight):
|
||||
super(QFlightWaypointTab, self).__init__()
|
||||
self.game = game
|
||||
@ -161,6 +164,9 @@ class QFlightWaypointTab(QFrame):
|
||||
QMessageBox.critical(
|
||||
self, "Could not recreate flight", str(ex), QMessageBox.Ok
|
||||
)
|
||||
if not self.flight.loadout.is_custom:
|
||||
self.flight.loadout = Loadout.default_for(self.flight)
|
||||
self.loadout_changed.emit()
|
||||
self.flight_waypoint_list.update_list()
|
||||
self.on_change()
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ jinja_env = Environment(
|
||||
)
|
||||
|
||||
DEFAULT_BUDGET = 2000
|
||||
DEFAULT_MISSION_LENGTH: timedelta = timedelta(minutes=90)
|
||||
DEFAULT_MISSION_LENGTH: timedelta = timedelta(minutes=60)
|
||||
|
||||
|
||||
class NewGameWizard(QtWidgets.QWizard):
|
||||
|
||||
@ -23,7 +23,7 @@ from dcs.forcedoptions import ForcedOptions
|
||||
import qt_ui.uiconstants as CONST
|
||||
from game.game import Game
|
||||
from game.infos.information import Information
|
||||
from game.settings import Settings
|
||||
from game.settings import Settings, AutoAtoBehavior
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
@ -75,6 +75,110 @@ class CheatSettingsBox(QGroupBox):
|
||||
return self.base_capture_cheat_checkbox.isChecked()
|
||||
|
||||
|
||||
class AutoAtoBehaviorSelector(QComboBox):
|
||||
def __init__(self, default: AutoAtoBehavior) -> None:
|
||||
super().__init__()
|
||||
|
||||
for behavior in AutoAtoBehavior:
|
||||
self.addItem(behavior.value, behavior)
|
||||
self.setCurrentText(default.value)
|
||||
|
||||
|
||||
class HqAutomationSettingsBox(QGroupBox):
|
||||
def __init__(self, game: Game) -> None:
|
||||
super().__init__("HQ Automation")
|
||||
self.game = game
|
||||
|
||||
layout = QGridLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
runway_repair = QCheckBox()
|
||||
runway_repair.setChecked(self.game.settings.automate_runway_repair)
|
||||
runway_repair.toggled.connect(self.set_runway_automation)
|
||||
|
||||
layout.addWidget(QLabel("Automate runway repairs"), 0, 0)
|
||||
layout.addWidget(runway_repair, 0, 1, Qt.AlignRight)
|
||||
|
||||
front_line = QCheckBox()
|
||||
front_line.setChecked(self.game.settings.automate_front_line_reinforcements)
|
||||
front_line.toggled.connect(self.set_front_line_automation)
|
||||
|
||||
layout.addWidget(QLabel("Automate front-line purchases"), 1, 0)
|
||||
layout.addWidget(front_line, 1, 1, Qt.AlignRight)
|
||||
|
||||
self.automate_aircraft_reinforcements = QCheckBox()
|
||||
self.automate_aircraft_reinforcements.setChecked(
|
||||
self.game.settings.automate_aircraft_reinforcements
|
||||
)
|
||||
self.automate_aircraft_reinforcements.toggled.connect(
|
||||
self.set_aircraft_automation
|
||||
)
|
||||
|
||||
layout.addWidget(QLabel("Automate aircraft purchases"), 2, 0)
|
||||
layout.addWidget(self.automate_aircraft_reinforcements, 2, 1, Qt.AlignRight)
|
||||
|
||||
self.auto_ato_behavior = AutoAtoBehaviorSelector(
|
||||
self.game.settings.auto_ato_behavior
|
||||
)
|
||||
self.auto_ato_behavior.currentIndexChanged.connect(self.set_auto_ato_behavior)
|
||||
layout.addWidget(
|
||||
QLabel(
|
||||
"Automatic package planning behavior<br>"
|
||||
"<strong>Aircraft auto-purchase is directed by the auto-planner,<br />"
|
||||
"so disabling auto-planning disables auto-purchase.</strong>"
|
||||
),
|
||||
3,
|
||||
0,
|
||||
)
|
||||
layout.addWidget(self.auto_ato_behavior, 3, 1)
|
||||
|
||||
self.auto_ato_player_missions_asap = QCheckBox()
|
||||
self.auto_ato_player_missions_asap.setChecked(
|
||||
self.game.settings.auto_ato_player_missions_asap
|
||||
)
|
||||
self.auto_ato_player_missions_asap.toggled.connect(
|
||||
self.set_auto_ato_player_missions_asap
|
||||
)
|
||||
|
||||
layout.addWidget(
|
||||
QLabel("Automatically generated packages with players are scheduled ASAP"),
|
||||
4,
|
||||
0,
|
||||
)
|
||||
layout.addWidget(self.auto_ato_player_missions_asap, 4, 1, Qt.AlignRight)
|
||||
|
||||
def set_runway_automation(self, value: bool) -> None:
|
||||
self.game.settings.automate_runway_repair = value
|
||||
|
||||
def set_front_line_automation(self, value: bool) -> None:
|
||||
self.game.settings.automate_front_line_reinforcements = value
|
||||
|
||||
def set_aircraft_automation(self, value: bool) -> None:
|
||||
self.game.settings.automate_aircraft_reinforcements = value
|
||||
|
||||
def set_auto_ato_behavior(self, index: int) -> None:
|
||||
behavior = self.auto_ato_behavior.itemData(index)
|
||||
self.game.settings.auto_ato_behavior = behavior
|
||||
if behavior in (AutoAtoBehavior.Disabled, AutoAtoBehavior.Never):
|
||||
self.auto_ato_player_missions_asap.setChecked(False)
|
||||
self.auto_ato_player_missions_asap.setEnabled(False)
|
||||
if behavior is AutoAtoBehavior.Disabled:
|
||||
self.automate_aircraft_reinforcements.setChecked(False)
|
||||
self.automate_aircraft_reinforcements.setEnabled(False)
|
||||
else:
|
||||
self.auto_ato_player_missions_asap.setEnabled(True)
|
||||
self.auto_ato_player_missions_asap.setChecked(
|
||||
self.game.settings.auto_ato_player_missions_asap
|
||||
)
|
||||
self.automate_aircraft_reinforcements.setEnabled(True)
|
||||
self.automate_aircraft_reinforcements.setChecked(
|
||||
self.game.settings.automate_aircraft_reinforcements
|
||||
)
|
||||
|
||||
def set_auto_ato_player_missions_asap(self, value: bool) -> None:
|
||||
self.game.settings.auto_ato_player_missions_asap = value
|
||||
|
||||
|
||||
START_TYPE_TOOLTIP = "Selects the start type used for AI aircraft."
|
||||
|
||||
|
||||
@ -92,7 +196,7 @@ class StartTypeComboBox(QComboBox):
|
||||
|
||||
class QSettingsWindow(QDialog):
|
||||
def __init__(self, game: Game):
|
||||
super(QSettingsWindow, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
self.game = game
|
||||
self.pluginsPage = None
|
||||
@ -285,6 +389,23 @@ class QSettingsWindow(QDialog):
|
||||
self.ext_views.setChecked(self.game.settings.external_views_allowed)
|
||||
self.ext_views.toggled.connect(self.applySettings)
|
||||
|
||||
def set_invulnerable_player_pilots(checked: bool) -> None:
|
||||
self.game.settings.invulnerable_player_pilots = checked
|
||||
|
||||
invulnerable_player_pilots_label = QLabel(
|
||||
"Player pilots cannot be killed<br />"
|
||||
"<strong>Aircraft are vulnerable, but the player's pilot will be<br />"
|
||||
"returned to the squadron at the end of the mission</strong>"
|
||||
)
|
||||
|
||||
invulnerable_player_pilots_checkbox = QCheckBox()
|
||||
invulnerable_player_pilots_checkbox.setChecked(
|
||||
self.game.settings.invulnerable_player_pilots
|
||||
)
|
||||
invulnerable_player_pilots_checkbox.toggled.connect(
|
||||
set_invulnerable_player_pilots
|
||||
)
|
||||
|
||||
self.aiDifficultyLayout.addWidget(QLabel("Player coalition skill"), 0, 0)
|
||||
self.aiDifficultyLayout.addWidget(
|
||||
self.playerCoalitionSkill, 0, 1, Qt.AlignRight
|
||||
@ -295,6 +416,10 @@ class QSettingsWindow(QDialog):
|
||||
self.aiDifficultyLayout.addWidget(self.enemyAASkill, 2, 1, Qt.AlignRight)
|
||||
self.aiDifficultyLayout.addLayout(self.player_income, 3, 0)
|
||||
self.aiDifficultyLayout.addLayout(self.enemy_income, 4, 0)
|
||||
self.aiDifficultyLayout.addWidget(invulnerable_player_pilots_label, 5, 0)
|
||||
self.aiDifficultyLayout.addWidget(
|
||||
invulnerable_player_pilots_checkbox, 5, 1, Qt.AlignRight
|
||||
)
|
||||
self.aiDifficultySettings.setLayout(self.aiDifficultyLayout)
|
||||
self.difficultyLayout.addWidget(self.aiDifficultySettings)
|
||||
|
||||
@ -367,41 +492,7 @@ class QSettingsWindow(QDialog):
|
||||
general_layout.addWidget(old_awac_label, 1, 0)
|
||||
general_layout.addWidget(old_awac, 1, 1, Qt.AlignRight)
|
||||
|
||||
automation = QGroupBox("HQ Automation")
|
||||
campaign_layout.addWidget(automation)
|
||||
|
||||
automation_layout = QGridLayout()
|
||||
automation.setLayout(automation_layout)
|
||||
|
||||
def set_runway_automation(value: bool) -> None:
|
||||
self.game.settings.automate_runway_repair = value
|
||||
|
||||
def set_front_line_automation(value: bool) -> None:
|
||||
self.game.settings.automate_front_line_reinforcements = value
|
||||
|
||||
def set_aircraft_automation(value: bool) -> None:
|
||||
self.game.settings.automate_aircraft_reinforcements = value
|
||||
|
||||
runway_repair = QCheckBox()
|
||||
runway_repair.setChecked(self.game.settings.automate_runway_repair)
|
||||
runway_repair.toggled.connect(set_runway_automation)
|
||||
|
||||
automation_layout.addWidget(QLabel("Automate runway repairs"), 0, 0)
|
||||
automation_layout.addWidget(runway_repair, 0, 1, Qt.AlignRight)
|
||||
|
||||
front_line = QCheckBox()
|
||||
front_line.setChecked(self.game.settings.automate_front_line_reinforcements)
|
||||
front_line.toggled.connect(set_front_line_automation)
|
||||
|
||||
automation_layout.addWidget(QLabel("Automate front-line purchases"), 1, 0)
|
||||
automation_layout.addWidget(front_line, 1, 1, Qt.AlignRight)
|
||||
|
||||
aircraft = QCheckBox()
|
||||
aircraft.setChecked(self.game.settings.automate_aircraft_reinforcements)
|
||||
aircraft.toggled.connect(set_aircraft_automation)
|
||||
|
||||
automation_layout.addWidget(QLabel("Automate aircraft purchases"), 2, 0)
|
||||
automation_layout.addWidget(aircraft, 2, 1, Qt.AlignRight)
|
||||
campaign_layout.addWidget(HqAutomationSettingsBox(self.game))
|
||||
|
||||
def initGeneratorLayout(self):
|
||||
self.generatorPage = QWidget()
|
||||
|
||||
@ -5,6 +5,7 @@ certifi==2020.12.5
|
||||
cfgv==3.2.0
|
||||
click==7.1.2
|
||||
distlib==0.3.1
|
||||
Faker==8.2.1
|
||||
filelock==3.0.12
|
||||
future==0.18.2
|
||||
identify==1.5.13
|
||||
@ -23,6 +24,7 @@ pyinstaller-hooks-contrib==2021.1
|
||||
pyparsing==2.4.7
|
||||
pyproj==3.0.1
|
||||
PySide2==5.15.2
|
||||
python-dateutil==2.8.1
|
||||
pywin32-ctypes==0.2.0
|
||||
PyYAML==5.4.1
|
||||
regex==2020.11.13
|
||||
@ -30,6 +32,7 @@ Shapely==1.7.1
|
||||
shiboken2==5.15.2
|
||||
six==1.15.0
|
||||
tabulate==0.8.7
|
||||
text-unidecode==1.3
|
||||
toml==0.10.2
|
||||
typed-ast==1.4.2
|
||||
typing-extensions==3.7.4.3
|
||||
|
||||
@ -7,5 +7,5 @@
|
||||
"description": "<p>You have managed to establish a foothold at Khasab. Continue pushing south.</p>",
|
||||
"miz": "battle_of_abu_dhabi.miz",
|
||||
"performance": 2,
|
||||
"version": "4.1"
|
||||
"version": "5.0"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@ -7,5 +7,5 @@
|
||||
"description": "<p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance friendly.</p>",
|
||||
"miz": "golan_heights_lite.miz",
|
||||
"performance": 1,
|
||||
"version": 3
|
||||
"version": "5.0"
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -5,7 +5,7 @@
|
||||
"recommended_player_faction": "USA 2005",
|
||||
"recommended_enemy_faction": "Insurgents (Hard)",
|
||||
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
|
||||
"version": 4,
|
||||
"version": "5.0",
|
||||
"miz": "inherent_resolve.miz",
|
||||
"performance": 1
|
||||
"performance": 2
|
||||
}
|
||||
Binary file not shown.
@ -1,102 +0,0 @@
|
||||
{
|
||||
"name": "Caucasus - North Caucasus",
|
||||
"theater": "Caucasus",
|
||||
"authors": "Khopa",
|
||||
"description": "<p>In this scenario you will have to fight in the moutain of Caucasus</p><p><strong>Note:</strong> Running CAS in the moutains can be a bit difficult.</p>",
|
||||
"player_points": [
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Kutaisi",
|
||||
"size": 600,
|
||||
"importance": 1
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Vaziani",
|
||||
"size": 600,
|
||||
"importance": 1
|
||||
},
|
||||
{
|
||||
"type": "carrier",
|
||||
"id": 1001,
|
||||
"x": -285810.6875,
|
||||
"y": 496399.1875,
|
||||
"captured_invert": true
|
||||
},
|
||||
{
|
||||
"type": "lha",
|
||||
"id": 1002,
|
||||
"x": -326050.6875,
|
||||
"y": 519452.1875,
|
||||
"captured_invert": true
|
||||
}
|
||||
],
|
||||
"enemy_points": [
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Beslan",
|
||||
"size": 1000,
|
||||
"importance": 1
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Nalchik",
|
||||
"size": 1000,
|
||||
"importance": 1.1
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Mozdok",
|
||||
"size": 2000,
|
||||
"importance": 1.1
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Mineralnye Vody",
|
||||
"size": 2000,
|
||||
"importance": 1.3
|
||||
},
|
||||
{
|
||||
"type": "airbase",
|
||||
"id": "Maykop-Khanskaya",
|
||||
"size": 3000,
|
||||
"importance": 1.4,
|
||||
"captured_invert": true
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
"Kutaisi",
|
||||
"Vaziani"
|
||||
],
|
||||
[
|
||||
"Beslan",
|
||||
"Vaziani"
|
||||
],
|
||||
[
|
||||
"Beslan",
|
||||
"Mozdok"
|
||||
],
|
||||
[
|
||||
"Beslan",
|
||||
"Nalchik"
|
||||
],
|
||||
[
|
||||
"Mozdok",
|
||||
"Nalchik"
|
||||
],
|
||||
[
|
||||
"Mineralnye Vody",
|
||||
"Nalchik"
|
||||
],
|
||||
[
|
||||
"Mineralnye Vody",
|
||||
"Mozdok"
|
||||
],
|
||||
[
|
||||
"Maykop-Khanskaya",
|
||||
"Mineralnye Vody"
|
||||
]
|
||||
],
|
||||
"performance": 1
|
||||
}
|
||||
@ -98,102 +98,6 @@ local unitPayloads = {
|
||||
},
|
||||
},
|
||||
[3] = {
|
||||
["name"] = "SEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 10,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 R}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 L}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 5,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 10,
|
||||
},
|
||||
},
|
||||
[4] = {
|
||||
["name"] = "DEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 10,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{F14-LANTIRN-TP}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{PHXBRU3242_2*LAU10 LS}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{BRU-32 GBU-12}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{BRU-32 GBU-12}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{BRU-32 GBU-12}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "{BRU-32 GBU-12}",
|
||||
["num"] = 5,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 10,
|
||||
},
|
||||
},
|
||||
[5] = {
|
||||
["name"] = "STRIKE",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
@ -241,7 +145,7 @@ local unitPayloads = {
|
||||
[1] = 10,
|
||||
},
|
||||
},
|
||||
[6] = {
|
||||
[4] = {
|
||||
["name"] = "BAI",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
@ -289,7 +193,7 @@ local unitPayloads = {
|
||||
[1] = 10,
|
||||
},
|
||||
},
|
||||
[7] = {
|
||||
[5] = {
|
||||
["name"] = "ANTISHIP",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
@ -337,6 +241,103 @@ local unitPayloads = {
|
||||
[1] = 10,
|
||||
},
|
||||
},
|
||||
[6] = {
|
||||
["name"] = "Liberation DEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 10,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 L}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 R}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{MAK79_MK82 4}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{MAK79_MK82 3R}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{MAK79_MK82 3L}",
|
||||
["num"] = 5,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "{MAK79_MK82 4}",
|
||||
["num"] = 4,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 31,
|
||||
},
|
||||
},
|
||||
[7] = {
|
||||
["displayName"] = "Liberation SEAD",
|
||||
["name"] = "Liberation SEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 10,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 L}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 R}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 5,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 4,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 31,
|
||||
},
|
||||
},
|
||||
},
|
||||
["unitType"] = "F-14A-135-GR",
|
||||
}
|
||||
|
||||
@ -155,57 +155,6 @@ local unitPayloads = {
|
||||
},
|
||||
},
|
||||
[4] = {
|
||||
["name"] = "SEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 10,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 R}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{BRU-32 GBU-12}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{BRU-32 GBU-12}",
|
||||
["num"] = 5,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 L}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 1,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 10,
|
||||
[2] = 11,
|
||||
[3] = 18,
|
||||
[4] = 19,
|
||||
},
|
||||
},
|
||||
[5] = {
|
||||
["name"] = "ANTISHIP",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
@ -256,6 +205,103 @@ local unitPayloads = {
|
||||
[4] = 19,
|
||||
},
|
||||
},
|
||||
[5] = {
|
||||
["name"] = "Liberation DEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 10,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 L}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 R}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{MAK79_MK82 4}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{MAK79_MK82 3R}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{MAK79_MK82 3L}",
|
||||
["num"] = 5,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "{MAK79_MK82 4}",
|
||||
["num"] = 4,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 31,
|
||||
},
|
||||
},
|
||||
[6] = {
|
||||
["displayName"] = "Liberation SEAD",
|
||||
["name"] = "Liberation SEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 10,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{LAU-138 wtip - AIM-9M}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 L}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{SHOULDER AIM_54C_Mk47 R}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{F14-300gal}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 5,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "{BRU3242_ADM141}",
|
||||
["num"] = 4,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 31,
|
||||
},
|
||||
},
|
||||
},
|
||||
["unitType"] = "F-14B",
|
||||
}
|
||||
|
||||
@ -2,71 +2,88 @@ local unitPayloads = {
|
||||
["name"] = "F-15E",
|
||||
["payloads"] = {
|
||||
[1] = {
|
||||
["name"] = "CAS",
|
||||
["displayName"] = "Liberation CAS",
|
||||
["name"] = "Liberation CAS",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
|
||||
["num"] = 1,
|
||||
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
|
||||
["num"] = 18,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
||||
["num"] = 7,
|
||||
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
|
||||
["num"] = 17,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{GBU-38}",
|
||||
["num"] = 9,
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 19,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{E1F29B21-F291-4589-9FD8-3272EEC69506}",
|
||||
["num"] = 10,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{GBU-38}",
|
||||
["CLSID"] = "{CBU_105}",
|
||||
["num"] = 11,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
||||
["num"] = 13,
|
||||
["CLSID"] = "{CBU_105}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
||||
["num"] = 14,
|
||||
["CLSID"] = "{CBU_105}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[11] = {
|
||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
||||
["num"] = 16,
|
||||
["CLSID"] = "{CBU_105}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[12] = {
|
||||
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
|
||||
["num"] = 19,
|
||||
["CLSID"] = "{CBU_105}",
|
||||
["num"] = 12,
|
||||
},
|
||||
[13] = {
|
||||
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
|
||||
["num"] = 17,
|
||||
["CLSID"] = "{CBU_105}",
|
||||
["num"] = 13,
|
||||
},
|
||||
[14] = {
|
||||
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
|
||||
["num"] = 18,
|
||||
["CLSID"] = "{Mk82AIR}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[15] = {
|
||||
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
|
||||
["num"] = 2,
|
||||
["CLSID"] = "{Mk82AIR}",
|
||||
["num"] = 5,
|
||||
},
|
||||
[16] = {
|
||||
["CLSID"] = "{Mk82AIR}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[17] = {
|
||||
["CLSID"] = "{Mk82AIR}",
|
||||
["num"] = 14,
|
||||
},
|
||||
[18] = {
|
||||
["CLSID"] = "{Mk82AIR}",
|
||||
["num"] = 15,
|
||||
},
|
||||
[19] = {
|
||||
["CLSID"] = "{Mk82AIR}",
|
||||
["num"] = 16,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 32,
|
||||
[1] = 31,
|
||||
},
|
||||
},
|
||||
[2] = {
|
||||
|
||||
@ -5,7 +5,7 @@ local unitPayloads = {
|
||||
["name"] = "CAS",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
||||
["CLSID"] = "<CLEAN>",
|
||||
["num"] = 5,
|
||||
},
|
||||
[2] = {
|
||||
@ -80,7 +80,7 @@ local unitPayloads = {
|
||||
["num"] = 11,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
||||
["CLSID"] = "<CLEAN>",
|
||||
["num"] = 5,
|
||||
},
|
||||
},
|
||||
@ -91,7 +91,7 @@ local unitPayloads = {
|
||||
["name"] = "CAP",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
||||
["CLSID"] = "<CLEAN>",
|
||||
["num"] = 5,
|
||||
},
|
||||
[2] = {
|
||||
@ -166,7 +166,7 @@ local unitPayloads = {
|
||||
["num"] = 1,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
||||
["CLSID"] = "<CLEAN>",
|
||||
["num"] = 5,
|
||||
},
|
||||
[10] = {
|
||||
@ -197,7 +197,7 @@ local unitPayloads = {
|
||||
["num"] = 3,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
||||
["CLSID"] = "<CLEAN>",
|
||||
["num"] = 5,
|
||||
},
|
||||
[6] = {
|
||||
@ -220,6 +220,55 @@ local unitPayloads = {
|
||||
["tasks"] = {
|
||||
},
|
||||
},
|
||||
[6] = {
|
||||
["displayName"] = "Liberation DEAD",
|
||||
["name"] = "Liberation DEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
|
||||
["num"] = 11,
|
||||
},
|
||||
[10] = {
|
||||
["CLSID"] = "<CLEAN>",
|
||||
["num"] = 5,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 31,
|
||||
},
|
||||
},
|
||||
},
|
||||
["unitType"] = "F-16C_50",
|
||||
}
|
||||
|
||||
@ -2,39 +2,39 @@ local unitPayloads = {
|
||||
["name"] = "FA-18C_hornet",
|
||||
["payloads"] = {
|
||||
[1] = {
|
||||
["name"] = "CAS MAVERICK F",
|
||||
["name"] = "Liberation BARCAP",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "LAU_117_AGM_65F",
|
||||
["num"] = 7,
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "LAU_117_AGM_65F",
|
||||
["num"] = 8,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{AN_ASQ_228}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "LAU_117_AGM_65F",
|
||||
["num"] = 3,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "LAU_117_AGM_65F",
|
||||
["num"] = 2,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "LAU-115_2*LAU-127_AIM-120C",
|
||||
["num"] = 8,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "LAU-115_2*LAU-127_AIM-120C",
|
||||
["num"] = 2,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 9,
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 4,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
@ -42,90 +42,50 @@ local unitPayloads = {
|
||||
},
|
||||
},
|
||||
[2] = {
|
||||
["name"] = "CAS MAVERICK E",
|
||||
["name"] = "Liberation CAS",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{F16A4DE0-116C-4A71-97F0-2CF85B0313EC}",
|
||||
["num"] = 7,
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{F16A4DE0-116C-4A71-97F0-2CF85B0313EC}",
|
||||
["num"] = 8,
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 6,
|
||||
["CLSID"] = "LAU_117_AGM_65F",
|
||||
["num"] = 2,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "LAU_117_AGM_65F",
|
||||
["num"] = 8,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "LAU_117_AGM_65F",
|
||||
["num"] = 7,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "LAU_117_AGM_65F",
|
||||
["num"] = 3,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{AN_ASQ_228}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{F16A4DE0-116C-4A71-97F0-2CF85B0313EC}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{F16A4DE0-116C-4A71-97F0-2CF85B0313EC}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 9,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 11,
|
||||
},
|
||||
},
|
||||
[3] = {
|
||||
["name"] = "CAP HEAVY",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "LAU-115_2*LAU-127_AIM-120C",
|
||||
["num"] = 7,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "LAU-115_2*LAU-127_AIM-120C",
|
||||
["num"] = 8,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 4,
|
||||
},
|
||||
[5] = {
|
||||
[9] = {
|
||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||
["num"] = 5,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "LAU-115_2*LAU-127_AIM-120C",
|
||||
["num"] = 3,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "LAU-115_2*LAU-127_AIM-120C",
|
||||
["num"] = 2,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 9,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 11,
|
||||
[1] = 31,
|
||||
},
|
||||
},
|
||||
[4] = {
|
||||
[3] = {
|
||||
["name"] = "STRIKE",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
@ -165,7 +125,7 @@ local unitPayloads = {
|
||||
[1] = 11,
|
||||
},
|
||||
},
|
||||
[5] = {
|
||||
[4] = {
|
||||
["name"] = "ANTISHIP",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
@ -205,16 +165,17 @@ local unitPayloads = {
|
||||
[1] = 11,
|
||||
},
|
||||
},
|
||||
[6] = {
|
||||
["name"] = "SEAD",
|
||||
[5] = {
|
||||
["displayName"] = "Liberation SEAD",
|
||||
["name"] = "Liberation SEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
||||
["num"] = 7,
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
||||
["num"] = 8,
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
@ -226,30 +187,26 @@ local unitPayloads = {
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||
["num"] = 5,
|
||||
["num"] = 3,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
||||
["num"] = 3,
|
||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[9] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 9,
|
||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
||||
["num"] = 8,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 11,
|
||||
[1] = 29,
|
||||
},
|
||||
},
|
||||
[7] = {
|
||||
[6] = {
|
||||
["name"] = "RUNWAY_ATTACK",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
@ -293,6 +250,46 @@ local unitPayloads = {
|
||||
[1] = 34,
|
||||
},
|
||||
},
|
||||
[7] = {
|
||||
["name"] = "Liberation DEAD",
|
||||
["pylons"] = {
|
||||
[1] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 9,
|
||||
},
|
||||
[2] = {
|
||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||
["num"] = 1,
|
||||
},
|
||||
[3] = {
|
||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||
["num"] = 6,
|
||||
},
|
||||
[4] = {
|
||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||
["num"] = 7,
|
||||
},
|
||||
[5] = {
|
||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||
["num"] = 3,
|
||||
},
|
||||
[6] = {
|
||||
["CLSID"] = "{BRU55_2*AGM-154A}",
|
||||
["num"] = 8,
|
||||
},
|
||||
[7] = {
|
||||
["CLSID"] = "{BRU55_2*AGM-154A}",
|
||||
["num"] = 2,
|
||||
},
|
||||
[8] = {
|
||||
["CLSID"] = "{AN_ASQ_228}",
|
||||
["num"] = 4,
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
[1] = 11,
|
||||
},
|
||||
},
|
||||
},
|
||||
["tasks"] = {
|
||||
},
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "Canada 2005",
|
||||
"authors": "Khopa",
|
||||
"description": "<p>Canada in the 2000s, an F/A-18C Hornet focused faction.</p>",
|
||||
"locales": ["en_US", "fr_CA"],
|
||||
"aircrafts": [
|
||||
"FA_18C_hornet",
|
||||
"UH_1H",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "Canada 2005 (With C-130)",
|
||||
"authors": "Khopa, SpaceEnthusiast",
|
||||
"description": "<p>Canada in the 2000s, an F/A-18C Hornet focused faction.</p>",
|
||||
"locales": ["en_US", "fr_CA"],
|
||||
"aircrafts": [
|
||||
"FA_18C_hornet",
|
||||
"UH_1H",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "China 2010",
|
||||
"authors": "Khopa",
|
||||
"description": "<p>China in the late 2000s, early 2010s.</p>",
|
||||
"locales": ["zh_CN"],
|
||||
"aircrafts": [
|
||||
"MiG_21Bis",
|
||||
"Su_30",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "France 1985 (Frenchpack)",
|
||||
"authors": "Colonel Panic",
|
||||
"description": "<p>1980s French equipment using FrenchPack.</p>",
|
||||
"locales": ["fr_FR"],
|
||||
"doctrine": "coldwar",
|
||||
"aircrafts": [
|
||||
"M_2000C",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "France 1995",
|
||||
"authors": "Khopa",
|
||||
"description": "<p>France in the late 90s before Rafale introduction. A Mirage-2000 centric faction choice.</p>",
|
||||
"locales": ["fr_FR"],
|
||||
"aircrafts": [
|
||||
"M_2000C",
|
||||
"Mirage_2000_5",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "France 2005 (Frenchpack)",
|
||||
"authors": "HerrTom",
|
||||
"description": "<p>French equipment using the Frenchpack, but without the Rafale mod.</p>",
|
||||
"locales": ["fr_FR"],
|
||||
"aircrafts": [
|
||||
"M_2000C",
|
||||
"Mirage_2000_5",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "German Democratic Republic 1985",
|
||||
"authors": "Colonel Panic",
|
||||
"description": "<p>The German Democratic Republic in 1985.</p>",
|
||||
"locales": ["de_DE"],
|
||||
"doctrine": "coldwar",
|
||||
"aircrafts": [
|
||||
"MiG_21Bis",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "Germany 1940",
|
||||
"authors": "Khopa",
|
||||
"description": "<p>Germany 1940, Early german faction for Battle of France, or Battle of England.</p>",
|
||||
"locales": ["de_DE"],
|
||||
"aircrafts": [
|
||||
"FW_190A8",
|
||||
"FW_190D9",
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"name": "Germany 1942",
|
||||
"authors": "Khopa",
|
||||
"description": "<p>Germany 1942, is a faction that does not use the late war german units such as the Tiger tank, so it's a bit easier to perform CAS against them.</p>",
|
||||
"locales": ["de_DE"],
|
||||
"aircrafts": [
|
||||
"FW_190A8",
|
||||
"FW_190D9",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user