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:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.9
|
||||||
|
|
||||||
- name: Install environment
|
- name: Install environment
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -13,10 +13,10 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Set up Python 3.8
|
- name: Set up Python 3.9
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: 3.9
|
||||||
|
|
||||||
- name: Install environment
|
- name: Install environment
|
||||||
run: |
|
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 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]** 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]** 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]** 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).
|
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
|
||||||
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups.
|
* **[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]** 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.
|
* **[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]** Added new web based map UI. This is mostly functional but many of the old display options are a WIP. Revert to the old map with --old-map.
|
||||||
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
|
* **[UI]** 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.
|
* **[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.
|
* **[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.
|
* **[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
|
## 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]** 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]** 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 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.
|
* **[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
|
# 2.5.1
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import dcs
|
|||||||
|
|
||||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||||
"fuel",
|
"fuel",
|
||||||
"ammo",
|
|
||||||
"comms",
|
"comms",
|
||||||
"oil",
|
"oil",
|
||||||
"ware",
|
"ware",
|
||||||
|
|||||||
@ -22,14 +22,46 @@ from dcs.ships import (
|
|||||||
)
|
)
|
||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
|
|
||||||
UNITS_WITH_RADAR = [
|
TELARS = {
|
||||||
# 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_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_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_Gepard,
|
||||||
AirDefence.SPAAA_Vulcan_M163,
|
AirDefence.SPAAA_Vulcan_M163,
|
||||||
|
AirDefence.SAM_Roland_ADS,
|
||||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||||
AirDefence.EWR_1L13,
|
AirDefence.EWR_1L13,
|
||||||
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
|
||||||
@ -47,7 +79,11 @@ UNITS_WITH_RADAR = [
|
|||||||
AirDefence.SAM_Roland_EWR,
|
AirDefence.SAM_Roland_EWR,
|
||||||
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
|
||||||
AirDefence.SAM_SA_2_S_75_Fan_Song_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.HQ_7_Self_Propelled_STR,
|
||||||
|
AirDefence.EWR_FuMG_401_Freya_LZ,
|
||||||
|
AirDefence.EWR_FuSe_65_Würzburg_Riese,
|
||||||
# Ships
|
# Ships
|
||||||
CVN_70_Carl_Vinson,
|
CVN_70_Carl_Vinson,
|
||||||
FFG_Oliver_Hazzard_Perry,
|
FFG_Oliver_Hazzard_Perry,
|
||||||
@ -69,4 +105,4 @@ UNITS_WITH_RADAR = [
|
|||||||
Type_052B_Destroyer,
|
Type_052B_Destroyer,
|
||||||
Type_054A_Frigate,
|
Type_054A_Frigate,
|
||||||
Type_052C_Destroyer,
|
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
|
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:
|
def unit_type_of(unit: Unit) -> UnitType:
|
||||||
if isinstance(unit, Vehicle):
|
if isinstance(unit, Vehicle):
|
||||||
return vehicle_map[unit.type]
|
return vehicle_map[unit.type]
|
||||||
@ -1603,3 +1610,39 @@ F_16C_50.Liveries = DefaultLiveries
|
|||||||
P_51D_30_NA.Liveries = DefaultLiveries
|
P_51D_30_NA.Liveries = DefaultLiveries
|
||||||
Ju_88A4.Liveries = DefaultLiveries
|
Ju_88A4.Liveries = DefaultLiveries
|
||||||
B_17G.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,
|
FrontLineUnit,
|
||||||
GroundObjectUnit,
|
GroundObjectUnit,
|
||||||
UnitMap,
|
UnitMap,
|
||||||
|
FlyingUnit,
|
||||||
)
|
)
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
@ -41,24 +42,24 @@ DEBRIEFING_LOG_EXTENSION = "log"
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AirLosses:
|
class AirLosses:
|
||||||
player: List[Flight]
|
player: List[FlyingUnit]
|
||||||
enemy: List[Flight]
|
enemy: List[FlyingUnit]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def losses(self) -> Iterator[Flight]:
|
def losses(self) -> Iterator[FlyingUnit]:
|
||||||
return itertools.chain(self.player, self.enemy)
|
return itertools.chain(self.player, self.enemy)
|
||||||
|
|
||||||
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
|
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
|
||||||
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
|
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
|
||||||
losses = self.player if player else self.enemy
|
losses = self.player if player else self.enemy
|
||||||
for loss in losses:
|
for loss in losses:
|
||||||
losses_by_type[loss.unit_type] += 1
|
losses_by_type[loss.flight.unit_type] += 1
|
||||||
return losses_by_type
|
return losses_by_type
|
||||||
|
|
||||||
def surviving_flight_members(self, flight: Flight) -> int:
|
def surviving_flight_members(self, flight: Flight) -> int:
|
||||||
losses = 0
|
losses = 0
|
||||||
for loss in self.losses:
|
for loss in self.losses:
|
||||||
if loss == flight:
|
if loss.flight == flight:
|
||||||
losses += 1
|
losses += 1
|
||||||
return flight.count - losses
|
return flight.count - losses
|
||||||
|
|
||||||
@ -239,14 +240,14 @@ class Debriefing:
|
|||||||
player_losses = []
|
player_losses = []
|
||||||
enemy_losses = []
|
enemy_losses = []
|
||||||
for unit_name in self.state_data.killed_aircraft:
|
for unit_name in self.state_data.killed_aircraft:
|
||||||
flight = self.unit_map.flight(unit_name)
|
aircraft = self.unit_map.flight(unit_name)
|
||||||
if flight is None:
|
if aircraft is None:
|
||||||
logging.error(f"Could not find Flight matching {unit_name}")
|
logging.error(f"Could not find Flight matching {unit_name}")
|
||||||
continue
|
continue
|
||||||
if flight.departure.captured:
|
if aircraft.flight.departure.captured:
|
||||||
player_losses.append(flight)
|
player_losses.append(aircraft)
|
||||||
else:
|
else:
|
||||||
enemy_losses.append(flight)
|
enemy_losses.append(aircraft)
|
||||||
return AirLosses(player_losses, enemy_losses)
|
return AirLosses(player_losses, enemy_losses)
|
||||||
|
|
||||||
def dead_ground_units(self) -> GroundLosses:
|
def dead_ground_units(self) -> GroundLosses:
|
||||||
|
|||||||
@ -120,11 +120,15 @@ class Event:
|
|||||||
self.game.red_ato, debriefing.air_losses, for_player=False
|
self.game.red_ato, debriefing.air_losses, for_player=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||||
def commit_air_losses(debriefing: Debriefing) -> None:
|
|
||||||
for loss in debriefing.air_losses.losses:
|
for loss in debriefing.air_losses.losses:
|
||||||
aircraft = loss.unit_type
|
if (
|
||||||
cp = loss.departure
|
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)
|
available = cp.base.total_units_of_type(aircraft)
|
||||||
if available <= 0:
|
if available <= 0:
|
||||||
logging.error(
|
logging.error(
|
||||||
@ -136,6 +140,23 @@ class Event:
|
|||||||
logging.info(f"{aircraft} destroyed from {cp}")
|
logging.info(f"{aircraft} destroyed from {cp}")
|
||||||
cp.base.aircraft[aircraft] -= 1
|
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
|
@staticmethod
|
||||||
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
||||||
for loss in debriefing.front_line_losses:
|
for loss in debriefing.front_line_losses:
|
||||||
@ -249,6 +270,7 @@ class Event:
|
|||||||
logging.info("Committing mission results")
|
logging.info("Committing mission results")
|
||||||
|
|
||||||
self.commit_air_losses(debriefing)
|
self.commit_air_losses(debriefing)
|
||||||
|
self.commit_pilot_experience()
|
||||||
self.commit_front_line_losses(debriefing)
|
self.commit_front_line_losses(debriefing)
|
||||||
self.commit_convoy_losses(debriefing)
|
self.commit_convoy_losses(debriefing)
|
||||||
self.commit_airlift_losses(debriefing)
|
self.commit_airlift_losses(debriefing)
|
||||||
|
|||||||
@ -27,6 +27,9 @@ from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Faction:
|
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 used by this faction
|
||||||
country: str = field(default="")
|
country: str = field(default="")
|
||||||
@ -132,8 +135,7 @@ class Faction:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||||
|
faction = Faction(locales=json.get("locales"))
|
||||||
faction = Faction()
|
|
||||||
|
|
||||||
faction.country = json.get("country", "/")
|
faction.country = json.get("country", "/")
|
||||||
if faction.country not in [c.name for c in country_dict.values()]:
|
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
|
import sys
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Iterator
|
||||||
|
|
||||||
from dcs.action import Coalition
|
from dcs.action import Coalition
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.task import CAP, CAS, PinpointStrike
|
from dcs.task import CAP, CAS, PinpointStrike
|
||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from game.inventory import GlobalAircraftInventory
|
from game.inventory import GlobalAircraftInventory
|
||||||
@ -32,7 +33,8 @@ from .infos.information import Information
|
|||||||
from .navmesh import NavMesh
|
from .navmesh import NavMesh
|
||||||
from .procurement import AircraftProcurementRequest, ProcurementAi
|
from .procurement import AircraftProcurementRequest, ProcurementAi
|
||||||
from .profiling import logged_duration
|
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 import ConflictTheater
|
||||||
from .theater.bullseye import Bullseye
|
from .theater.bullseye import Bullseye
|
||||||
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||||
@ -140,6 +142,12 @@ class Game:
|
|||||||
|
|
||||||
self.sanitize_sides()
|
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()
|
self.on_load()
|
||||||
|
|
||||||
def __getstate__(self) -> Dict[str, Any]:
|
def __getstate__(self) -> Dict[str, Any]:
|
||||||
@ -150,6 +158,8 @@ class Game:
|
|||||||
del state["red_threat_zone"]
|
del state["red_threat_zone"]
|
||||||
del state["blue_navmesh"]
|
del state["blue_navmesh"]
|
||||||
del state["red_navmesh"]
|
del state["red_navmesh"]
|
||||||
|
del state["blue_faker"]
|
||||||
|
del state["red_faker"]
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||||
@ -205,6 +215,21 @@ class Game:
|
|||||||
return self.player_faction
|
return self.player_faction
|
||||||
return self.enemy_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:
|
def bullseye_for(self, player: bool) -> Bullseye:
|
||||||
if player:
|
if player:
|
||||||
return self.blue_bullseye
|
return self.blue_bullseye
|
||||||
@ -281,6 +306,8 @@ class Game:
|
|||||||
ObjectiveDistanceCache.set_theater(self.theater)
|
ObjectiveDistanceCache.set_theater(self.theater)
|
||||||
self.compute_conflicts_position()
|
self.compute_conflicts_position()
|
||||||
self.compute_threat_zones()
|
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:
|
def reset_ato(self) -> None:
|
||||||
self.blue_ato.clear()
|
self.blue_ato.clear()
|
||||||
@ -325,8 +352,10 @@ class Game:
|
|||||||
|
|
||||||
def pass_turn(self, no_action: bool = False) -> None:
|
def pass_turn(self, no_action: bool = False) -> None:
|
||||||
logging.info("Pass turn")
|
logging.info("Pass turn")
|
||||||
self.finish_turn(no_action)
|
with logged_duration("Turn finalization"):
|
||||||
self.initialize_turn()
|
self.finish_turn(no_action)
|
||||||
|
with logged_duration("Turn initialization"):
|
||||||
|
self.initialize_turn()
|
||||||
|
|
||||||
# Autosave progress
|
# Autosave progress
|
||||||
persistency.autosave(self)
|
persistency.autosave(self)
|
||||||
@ -360,6 +389,8 @@ class Game:
|
|||||||
# Update statistics
|
# Update statistics
|
||||||
self.game_stats.update(self)
|
self.game_stats.update(self)
|
||||||
|
|
||||||
|
self.blue_air_wing.reset()
|
||||||
|
self.red_air_wing.reset()
|
||||||
self.aircraft_inventory.reset()
|
self.aircraft_inventory.reset()
|
||||||
for cp in self.theater.controlpoints:
|
for cp in self.theater.controlpoints:
|
||||||
self.aircraft_inventory.set_from_control_point(cp)
|
self.aircraft_inventory.set_from_control_point(cp)
|
||||||
@ -370,18 +401,28 @@ class Game:
|
|||||||
return self.process_win_loss(turn_state)
|
return self.process_win_loss(turn_state)
|
||||||
|
|
||||||
# Plan flights & combat for next turn
|
# Plan flights & combat for next turn
|
||||||
self.compute_conflicts_position()
|
with logged_duration("Computing conflict positions"):
|
||||||
self.compute_threat_zones()
|
self.compute_conflicts_position()
|
||||||
self.compute_transit_networks()
|
with logged_duration("Threat zone computation"):
|
||||||
|
self.compute_threat_zones()
|
||||||
|
with logged_duration("Transit network identification"):
|
||||||
|
self.compute_transit_networks()
|
||||||
self.ground_planners = {}
|
self.ground_planners = {}
|
||||||
|
|
||||||
self.transfers.order_airlift_assets()
|
self.blue_procurement_requests.clear()
|
||||||
self.transfers.plan_transports()
|
self.red_procurement_requests.clear()
|
||||||
|
|
||||||
with logged_duration("Mission planning"):
|
with logged_duration("Procurement of airlift assets"):
|
||||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
self.transfers.order_airlift_assets()
|
||||||
blue_planner.plan_missions()
|
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 = CoalitionMissionPlanner(self, is_player=False)
|
||||||
red_planner.plan_missions()
|
red_planner.plan_missions()
|
||||||
|
|
||||||
@ -408,7 +449,7 @@ class Game:
|
|||||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||||
front_line_budget_share=ground_portion,
|
front_line_budget_share=ground_portion,
|
||||||
).spend_budget(self.budget, self.blue_procurement_requests)
|
).spend_budget(self.budget)
|
||||||
|
|
||||||
self.enemy_budget = ProcurementAi(
|
self.enemy_budget = ProcurementAi(
|
||||||
self,
|
self,
|
||||||
@ -418,7 +459,7 @@ class Game:
|
|||||||
manage_front_line=True,
|
manage_front_line=True,
|
||||||
manage_aircraft=True,
|
manage_aircraft=True,
|
||||||
front_line_budget_share=ground_portion,
|
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:
|
def message(self, text: str) -> None:
|
||||||
self.informations.append(Information(text, turn=self.turn))
|
self.informations.append(Information(text, turn=self.turn))
|
||||||
|
|||||||
@ -103,7 +103,7 @@ class NavMesh:
|
|||||||
# currently.
|
# currently.
|
||||||
p = ShapelyPoint(point.x, point.y)
|
p = ShapelyPoint(point.x, point.y)
|
||||||
for navpoly in self.polys:
|
for navpoly in self.polys:
|
||||||
if navpoly.poly.contains(p):
|
if navpoly.poly.intersects(p):
|
||||||
return navpoly
|
return navpoly
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@ -50,6 +50,7 @@ class ProcurementAi:
|
|||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.is_player = for_player
|
self.is_player = for_player
|
||||||
|
self.air_wing = game.air_wing_for(for_player)
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
self.manage_runways = manage_runways
|
self.manage_runways = manage_runways
|
||||||
self.manage_front_line = manage_front_line
|
self.manage_front_line = manage_front_line
|
||||||
@ -57,9 +58,7 @@ class ProcurementAi:
|
|||||||
self.front_line_budget_share = front_line_budget_share
|
self.front_line_budget_share = front_line_budget_share
|
||||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
def spend_budget(
|
def spend_budget(self, budget: float) -> float:
|
||||||
self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
|
|
||||||
) -> float:
|
|
||||||
if self.manage_runways:
|
if self.manage_runways:
|
||||||
budget = self.repair_runways(budget)
|
budget = self.repair_runways(budget)
|
||||||
if self.manage_front_line:
|
if self.manage_front_line:
|
||||||
@ -163,23 +162,31 @@ class ProcurementAi:
|
|||||||
|
|
||||||
return budget
|
return budget
|
||||||
|
|
||||||
def _affordable_aircraft_of_types(
|
def _affordable_aircraft_for_task(
|
||||||
self,
|
self,
|
||||||
types: List[Type[FlyingType]],
|
task: FlightType,
|
||||||
airbase: ControlPoint,
|
airbase: ControlPoint,
|
||||||
number: int,
|
number: int,
|
||||||
max_price: float,
|
max_price: float,
|
||||||
) -> Optional[Type[FlyingType]]:
|
) -> Optional[Type[FlyingType]]:
|
||||||
best_choice: Optional[Type[FlyingType]] = None
|
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:
|
if db.PRICES[unit] * number > max_price:
|
||||||
continue
|
continue
|
||||||
if not airbase.can_operate(unit):
|
if not airbase.can_operate(unit):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Affordable and compatible. To keep some variety, skip with a 50/50
|
for squadron in self.air_wing.squadrons_for(unit):
|
||||||
# chance. Might be a good idea to have the chance to skip based on
|
if task in squadron.mission_types:
|
||||||
# the price compared to the rest of the choices.
|
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
|
best_choice = unit
|
||||||
if random.choice([True, False]):
|
if random.choice([True, False]):
|
||||||
break
|
break
|
||||||
@ -188,8 +195,8 @@ class ProcurementAi:
|
|||||||
def affordable_aircraft_for(
|
def affordable_aircraft_for(
|
||||||
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
|
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
|
||||||
) -> Optional[Type[FlyingType]]:
|
) -> Optional[Type[FlyingType]]:
|
||||||
return self._affordable_aircraft_of_types(
|
return self._affordable_aircraft_for_task(
|
||||||
aircraft_for_task(request.task_capability), airbase, request.number, budget
|
request.task_capability, airbase, request.number, budget
|
||||||
)
|
)
|
||||||
|
|
||||||
def fulfill_aircraft_request(
|
def fulfill_aircraft_request(
|
||||||
@ -255,10 +262,19 @@ class ProcurementAi:
|
|||||||
# Prefer to buy front line units at active front lines that are not
|
# Prefer to buy front line units at active front lines that are not
|
||||||
# already overloaded.
|
# already overloaded.
|
||||||
for cp in self.owned_points:
|
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):
|
if not cp.has_ground_unit_source(self.game):
|
||||||
continue
|
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.
|
# Control point is already sufficiently defended.
|
||||||
continue
|
continue
|
||||||
for connected in cp.connected_points:
|
for connected in cp.connected_points:
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import timeit
|
import timeit
|
||||||
|
from collections import defaultdict
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
@ -11,3 +14,22 @@ def logged_duration(event: str) -> Iterator[None]:
|
|||||||
yield
|
yield
|
||||||
end = timeit.default_timer()
|
end = timeit.default_timer()
|
||||||
logging.debug("%s took %s", event, timedelta(seconds=end - start))
|
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 dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from enum import Enum, unique
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from dcs.forcedoptions import ForcedOptions
|
from dcs.forcedoptions import ForcedOptions
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class AutoAtoBehavior(Enum):
|
||||||
|
Disabled = "Disabled"
|
||||||
|
Never = "Never assign player pilots"
|
||||||
|
Default = "No preference"
|
||||||
|
Prefer = "Prefer player pilots"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Settings:
|
class Settings:
|
||||||
|
|
||||||
@ -27,7 +36,7 @@ class Settings:
|
|||||||
default_start_type: str = "Cold"
|
default_start_type: str = "Cold"
|
||||||
|
|
||||||
# Mission specific
|
# Mission specific
|
||||||
desired_player_mission_duration: timedelta = timedelta(minutes=90)
|
desired_player_mission_duration: timedelta = timedelta(minutes=60)
|
||||||
|
|
||||||
# Campaign management
|
# Campaign management
|
||||||
automate_runway_repair: bool = False
|
automate_runway_repair: bool = False
|
||||||
@ -36,6 +45,9 @@ class Settings:
|
|||||||
restrict_weapons_by_date: bool = False
|
restrict_weapons_by_date: bool = False
|
||||||
disable_legacy_aewc: bool = False
|
disable_legacy_aewc: bool = False
|
||||||
generate_dark_kneeboard: 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
|
# Performance oriented
|
||||||
perf_red_alert_state: bool = True
|
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,
|
DDG_Arleigh_Burke_IIa,
|
||||||
LHA_1_Tarawa,
|
LHA_1_Tarawa,
|
||||||
)
|
)
|
||||||
from dcs.statics import Fortification
|
from dcs.statics import Fortification, Warehouse
|
||||||
from dcs.terrain import (
|
from dcs.terrain import (
|
||||||
caucasus,
|
caucasus,
|
||||||
nevada,
|
nevada,
|
||||||
@ -129,6 +129,8 @@ class MizCampaignLoader:
|
|||||||
|
|
||||||
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
|
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
|
||||||
|
|
||||||
|
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
|
||||||
|
|
||||||
REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
||||||
|
|
||||||
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
||||||
@ -321,6 +323,12 @@ class MizCampaignLoader:
|
|||||||
if group.units[0].type in self.FACTORY_UNIT_TYPE:
|
if group.units[0].type in self.FACTORY_UNIT_TYPE:
|
||||||
yield group
|
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
|
@property
|
||||||
def required_strike_targets(self) -> Iterator[StaticGroup]:
|
def required_strike_targets(self) -> Iterator[StaticGroup]:
|
||||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
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)
|
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:
|
for group in self.required_strike_targets:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(group)
|
||||||
closest.preset_locations.required_strike_locations.append(
|
closest.preset_locations.required_strike_locations.append(
|
||||||
|
|||||||
@ -8,7 +8,20 @@ from abc import ABC, abstractmethod
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import total_ordering
|
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 import helicopters
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
@ -48,6 +61,9 @@ if TYPE_CHECKING:
|
|||||||
from game import Game
|
from game import Game
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
|
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
|
||||||
|
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
|
||||||
|
|
||||||
|
|
||||||
class ControlPointType(Enum):
|
class ControlPointType(Enum):
|
||||||
#: An airbase with slots for everything.
|
#: An airbase with slots for everything.
|
||||||
@ -150,6 +166,9 @@ class PresetLocations:
|
|||||||
#: Locations of factories for producing ground units. These will always be spawned.
|
#: Locations of factories for producing ground units. These will always be spawned.
|
||||||
factories: List[PointWithHeading] = field(default_factory=list)
|
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.
|
#: Locations of stationary armor groups. These will always be spawned.
|
||||||
armor_groups: List[PointWithHeading] = field(default_factory=list)
|
armor_groups: List[PointWithHeading] = field(default_factory=list)
|
||||||
|
|
||||||
@ -308,8 +327,8 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
self.has_frontline = has_frontline
|
self.has_frontline = has_frontline
|
||||||
self.connected_points: List[ControlPoint] = []
|
self.connected_points: List[ControlPoint] = []
|
||||||
self.convoy_routes: Dict[ControlPoint, List[Point]] = {}
|
self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {}
|
||||||
self.shipping_lanes: Dict[ControlPoint, List[Point]] = {}
|
self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {}
|
||||||
self.base: Base = Base()
|
self.base: Base = Base()
|
||||||
self.cptype = cptype
|
self.cptype = cptype
|
||||||
# TODO: Should be Airbase specific.
|
# 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:
|
def convoy_origin_for(self, destination: ControlPoint) -> Point:
|
||||||
return self.convoy_route_to(destination)[0]
|
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]
|
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.connected_points.append(to)
|
||||||
self.stances[to.id] = CombatStance.DEFENSIVE
|
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:
|
def create_shipping_lane(
|
||||||
self.shipping_lanes[to] = waypoints
|
self, to: ControlPoint, waypoints: Iterable[Point]
|
||||||
|
) -> None:
|
||||||
|
self.shipping_lanes[to] = tuple(waypoints)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def runway_is_operational(self) -> bool:
|
def runway_is_operational(self) -> bool:
|
||||||
@ -788,15 +804,6 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
def income_per_turn(self) -> int:
|
def income_per_turn(self) -> int:
|
||||||
return 0
|
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
|
@property
|
||||||
def has_active_frontline(self) -> bool:
|
def has_active_frontline(self) -> bool:
|
||||||
return any(not c.is_friendly(self.captured) for c in self.connected_points)
|
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
|
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
|
@property
|
||||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def category(self) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class Airfield(ControlPoint):
|
class Airfield(ControlPoint):
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -840,18 +866,21 @@ class Airfield(ControlPoint):
|
|||||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if self.is_friendly(for_player):
|
if not self.is_friendly(for_player):
|
||||||
yield from [
|
|
||||||
# TODO: FlightType.INTERCEPTION
|
|
||||||
# TODO: FlightType.LOGISTICS
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
yield from [
|
yield from [
|
||||||
FlightType.OCA_AIRCRAFT,
|
FlightType.OCA_AIRCRAFT,
|
||||||
FlightType.OCA_RUNWAY,
|
FlightType.OCA_RUNWAY,
|
||||||
]
|
]
|
||||||
|
|
||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
if self.is_friendly(for_player):
|
||||||
|
yield from [
|
||||||
|
FlightType.AEWC,
|
||||||
|
# TODO: FlightType.INTERCEPTION
|
||||||
|
# TODO: FlightType.LOGISTICS
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_aircraft_parking(self) -> int:
|
def total_aircraft_parking(self) -> int:
|
||||||
return len(self.airport.parking_slots)
|
return len(self.airport.parking_slots)
|
||||||
@ -888,6 +917,10 @@ class Airfield(ControlPoint):
|
|||||||
def income_per_turn(self) -> int:
|
def income_per_turn(self) -> int:
|
||||||
return 20
|
return 20
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category(self) -> str:
|
||||||
|
return "airfield"
|
||||||
|
|
||||||
|
|
||||||
class NavalControlPoint(ControlPoint, ABC):
|
class NavalControlPoint(ControlPoint, ABC):
|
||||||
@property
|
@property
|
||||||
@ -967,6 +1000,13 @@ class Carrier(NavalControlPoint):
|
|||||||
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
|
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:
|
def capture(self, game: Game, for_player: bool) -> None:
|
||||||
raise RuntimeError("Carriers cannot be captured")
|
raise RuntimeError("Carriers cannot be captured")
|
||||||
|
|
||||||
@ -981,6 +1021,10 @@ class Carrier(NavalControlPoint):
|
|||||||
def total_aircraft_parking(self) -> int:
|
def total_aircraft_parking(self) -> int:
|
||||||
return 90
|
return 90
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category(self) -> str:
|
||||||
|
return "cv"
|
||||||
|
|
||||||
|
|
||||||
class Lha(NavalControlPoint):
|
class Lha(NavalControlPoint):
|
||||||
def __init__(self, name: str, at: Point, cp_id: int):
|
def __init__(self, name: str, at: Point, cp_id: int):
|
||||||
@ -1011,6 +1055,10 @@ class Lha(NavalControlPoint):
|
|||||||
def total_aircraft_parking(self) -> int:
|
def total_aircraft_parking(self) -> int:
|
||||||
return 20
|
return 20
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category(self) -> str:
|
||||||
|
return "lha"
|
||||||
|
|
||||||
|
|
||||||
class OffMapSpawn(ControlPoint):
|
class OffMapSpawn(ControlPoint):
|
||||||
def runway_is_operational(self) -> bool:
|
def runway_is_operational(self) -> bool:
|
||||||
@ -1061,6 +1109,10 @@ class OffMapSpawn(ControlPoint):
|
|||||||
def can_deploy_ground_units(self) -> bool:
|
def can_deploy_ground_units(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category(self) -> str:
|
||||||
|
return "offmap"
|
||||||
|
|
||||||
|
|
||||||
class Fob(ControlPoint):
|
class Fob(ControlPoint):
|
||||||
def __init__(self, name: str, at: Point, cp_id: int):
|
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]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
if self.is_friendly(for_player):
|
if not self.is_friendly(for_player):
|
||||||
yield from [
|
yield FlightType.STRIKE
|
||||||
FlightType.BARCAP,
|
|
||||||
# TODO: FlightType.LOGISTICS
|
yield from super().mission_types(for_player)
|
||||||
]
|
|
||||||
else:
|
|
||||||
yield from [
|
|
||||||
FlightType.STRIKE,
|
|
||||||
FlightType.SWEEP,
|
|
||||||
FlightType.ESCORT,
|
|
||||||
FlightType.SEAD,
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_aircraft_parking(self) -> int:
|
def total_aircraft_parking(self) -> int:
|
||||||
@ -1128,3 +1172,7 @@ class Fob(ControlPoint):
|
|||||||
@property
|
@property
|
||||||
def income_per_turn(self) -> int:
|
def income_per_turn(self) -> int:
|
||||||
return 10
|
return 10
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category(self) -> str:
|
||||||
|
return "fob"
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class FrontLine(MissionTarget):
|
|||||||
self.blue_cp = blue_point
|
self.blue_cp = blue_point
|
||||||
self.red_cp = red_point
|
self.red_cp = red_point
|
||||||
try:
|
try:
|
||||||
route = blue_point.convoy_route_to(red_point)
|
route = list(blue_point.convoy_route_to(red_point))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# Some campaigns are air only and the mission generator currently relies on
|
# 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
|
# *some* "front line" being drawn between these two. In this case there will
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -9,3 +9,26 @@ class LatLon:
|
|||||||
|
|
||||||
def as_list(self) -> List[float]:
|
def as_list(self) -> List[float]:
|
||||||
return [self.latitude, self.longitude]
|
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 [
|
yield from [
|
||||||
FlightType.ESCORT,
|
FlightType.ESCORT,
|
||||||
FlightType.TARCAP,
|
FlightType.TARCAP,
|
||||||
FlightType.SEAD,
|
FlightType.SEAD_ESCORT,
|
||||||
FlightType.SWEEP,
|
FlightType.SWEEP,
|
||||||
# TODO: FlightType.ELINT,
|
# TODO: FlightType.ELINT,
|
||||||
# TODO: FlightType.EWAR,
|
# TODO: FlightType.EWAR,
|
||||||
|
|||||||
@ -471,6 +471,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
self.generate_strike_targets()
|
self.generate_strike_targets()
|
||||||
self.generate_offshore_strike_targets()
|
self.generate_offshore_strike_targets()
|
||||||
self.generate_factories()
|
self.generate_factories()
|
||||||
|
self.generate_ammunition_depots()
|
||||||
|
|
||||||
if self.faction.missiles:
|
if self.faction.missiles:
|
||||||
self.generate_missile_sites()
|
self.generate_missile_sites()
|
||||||
@ -629,6 +630,10 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
|
|
||||||
self.control_point.connected_objectives.append(g)
|
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:
|
def generate_factories(self) -> None:
|
||||||
"""Generates the factories that are required by the campaign."""
|
"""Generates the factories that are required by the campaign."""
|
||||||
for position in self.control_point.preset_locations.factories:
|
for position in self.control_point.preset_locations.factories:
|
||||||
@ -828,6 +833,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
|||||||
FobDefenseGenerator(self.game, self.control_point).generate()
|
FobDefenseGenerator(self.game, self.control_point).generate()
|
||||||
self.generate_armor_groups()
|
self.generate_armor_groups()
|
||||||
self.generate_factories()
|
self.generate_factories()
|
||||||
|
self.generate_ammunition_depots()
|
||||||
self.generate_required_aa()
|
self.generate_required_aa()
|
||||||
self.generate_required_ewr()
|
self.generate_required_ewr()
|
||||||
self.generate_scenery_sites()
|
self.generate_scenery_sites()
|
||||||
|
|||||||
@ -8,9 +8,15 @@ from dcs.mapping import Point
|
|||||||
from dcs.triggers import TriggerZone
|
from dcs.triggers import TriggerZone
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from dcs.unitgroup import Group
|
from dcs.unitgroup import Group
|
||||||
|
from dcs.unittype import VehicleType
|
||||||
|
|
||||||
from .. import db
|
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
|
from ..utils import Distance, meters
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -137,12 +143,11 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_radar(self) -> bool:
|
def has_live_radar_sam(self) -> bool:
|
||||||
"""Returns True if the ground object contains a unit with radar."""
|
"""Returns True if the ground object contains a unit with working radar SAM."""
|
||||||
for group in self.groups:
|
for group in self.groups:
|
||||||
for unit in group.units:
|
if self.threat_range(group, radar_only=True):
|
||||||
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
|
return True
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
|
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))
|
max_range = max(max_range, meters(unit_range))
|
||||||
return max_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:
|
def detection_range(self, group: Group) -> Distance:
|
||||||
return self._max_range_of_type(group, "detection_range")
|
return self._max_range_of_type(group, "detection_range")
|
||||||
|
|
||||||
def threat_range(self, group: Group) -> Distance:
|
def max_threat_range(self) -> Distance:
|
||||||
if not self.detection_range(group):
|
return max(self.threat_range(g) for g in self.groups)
|
||||||
# For simple SAMs like shilkas, the unit has both a threat and
|
|
||||||
# detection range. For complex sites like SA-2s, the launcher has a
|
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
||||||
# 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)
|
|
||||||
return self._max_range_of_type(group, "threat_range")
|
return self._max_range_of_type(group, "threat_range")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -452,12 +456,45 @@ class SamGroundObject(BaseDefenseGroundObject):
|
|||||||
|
|
||||||
if not self.is_friendly(for_player):
|
if not self.is_friendly(for_player):
|
||||||
yield FlightType.DEAD
|
yield FlightType.DEAD
|
||||||
|
yield FlightType.SEAD
|
||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def might_have_aa(self) -> bool:
|
def might_have_aa(self) -> bool:
|
||||||
return True
|
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):
|
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@ -88,9 +88,27 @@ class TransitNetwork:
|
|||||||
TransitConnection.Airlift: a.position.distance_to_point(b.position),
|
TransitConnection.Airlift: a.position.distance_to_point(b.position),
|
||||||
}[self.link_type(a, b)]
|
}[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(
|
def shortest_path_between(
|
||||||
self, origin: ControlPoint, destination: ControlPoint
|
self, origin: ControlPoint, destination: ControlPoint
|
||||||
) -> List[ControlPoint]:
|
) -> list[ControlPoint]:
|
||||||
return self.shortest_path_with_cost(origin, destination)[0]
|
return self.shortest_path_with_cost(origin, destination)[0]
|
||||||
|
|
||||||
def shortest_path_with_cost(
|
def shortest_path_with_cost(
|
||||||
@ -127,7 +145,7 @@ class TransitNetwork:
|
|||||||
path: List[ControlPoint] = []
|
path: List[ControlPoint] = []
|
||||||
while current != origin:
|
while current != origin:
|
||||||
path.append(current)
|
path.append(current)
|
||||||
previous = came_from[current]
|
previous = came_from.get(current)
|
||||||
if previous is None:
|
if previous is None:
|
||||||
raise NoPathError(origin, destination)
|
raise NoPathError(origin, destination)
|
||||||
current = previous
|
current = previous
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from functools import singledispatchmethod
|
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 dcs.mapping import Point as DcsPoint
|
||||||
from shapely.geometry import (
|
from shapely.geometry import (
|
||||||
@ -13,11 +13,10 @@ from shapely.geometry import (
|
|||||||
from shapely.geometry.base import BaseGeometry
|
from shapely.geometry.base import BaseGeometry
|
||||||
from shapely.ops import nearest_points, unary_union
|
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 game.utils import Distance, meters, nautical_miles
|
||||||
from gen import Conflict
|
|
||||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight, FlightWaypoint
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@ -27,9 +26,12 @@ ThreatPoly = Union[MultiPolygon, Polygon]
|
|||||||
|
|
||||||
|
|
||||||
class ThreatZones:
|
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.airbases = airbases
|
||||||
self.air_defenses = air_defenses
|
self.air_defenses = air_defenses
|
||||||
|
self.radar_sam_threats = radar_sam_threats
|
||||||
self.all = unary_union([airbases, air_defenses])
|
self.all = unary_union([airbases, air_defenses])
|
||||||
|
|
||||||
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
|
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))
|
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
|
@singledispatchmethod
|
||||||
def threatened_by_air_defense(self, target) -> bool:
|
def threatened_by_air_defense(self, target) -> bool:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -83,6 +92,33 @@ class ThreatZones:
|
|||||||
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
|
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
|
@classmethod
|
||||||
def closest_enemy_airbase(
|
def closest_enemy_airbase(
|
||||||
cls, location: ControlPoint, max_distance: Distance
|
cls, location: ControlPoint, max_distance: Distance
|
||||||
@ -134,6 +170,7 @@ class ThreatZones:
|
|||||||
"""
|
"""
|
||||||
air_threats = []
|
air_threats = []
|
||||||
air_defenses = []
|
air_defenses = []
|
||||||
|
radar_sam_threats = []
|
||||||
for control_point in game.theater.controlpoints:
|
for control_point in game.theater.controlpoints:
|
||||||
if control_point.captured != player:
|
if control_point.captured != player:
|
||||||
continue
|
continue
|
||||||
@ -151,9 +188,16 @@ class ThreatZones:
|
|||||||
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
point = ShapelyPoint(tgo.position.x, tgo.position.y)
|
||||||
threat_zone = point.buffer(threat_range.meters)
|
threat_zone = point.buffer(threat_range.meters)
|
||||||
air_defenses.append(threat_zone)
|
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(
|
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
|
@staticmethod
|
||||||
|
|||||||
@ -4,12 +4,23 @@ import logging
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import singledispatchmethod
|
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.mapping import Point
|
||||||
from dcs.unittype import FlyingType, VehicleType
|
from dcs.unittype import FlyingType, VehicleType
|
||||||
|
|
||||||
from game.procurement import AircraftProcurementRequest
|
from game.procurement import AircraftProcurementRequest
|
||||||
|
from game.squadrons import Squadron
|
||||||
from game.theater import ControlPoint, MissionTarget
|
from game.theater import ControlPoint, MissionTarget
|
||||||
from game.theater.transitnetwork import (
|
from game.theater.transitnetwork import (
|
||||||
TransitConnection,
|
TransitConnection,
|
||||||
@ -222,17 +233,27 @@ class AirliftPlanner:
|
|||||||
|
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
||||||
for unit_type, available in inventory.all_aircraft:
|
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):
|
if self.compatible_with_mission(unit_type, cp):
|
||||||
while available and self.transfer.transport is None:
|
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
|
available -= flight_size
|
||||||
if self.package.flights:
|
if self.package.flights:
|
||||||
self.game.ato_for(self.for_player).add_package(self.package)
|
self.game.ato_for(self.for_player).add_package(self.package)
|
||||||
|
|
||||||
def create_airlift_flight(
|
def create_airlift_flight(
|
||||||
self, unit_type: Type[FlyingType], inventory: ControlPointAircraftInventory
|
self, squadron: Squadron, inventory: ControlPointAircraftInventory
|
||||||
) -> int:
|
) -> int:
|
||||||
available = inventory.available(unit_type)
|
available = inventory.available(squadron.aircraft)
|
||||||
# 4 is the max flight size in DCS.
|
# 4 is the max flight size in DCS.
|
||||||
flight_size = min(self.transfer.size, available, 4)
|
flight_size = min(self.transfer.size, available, 4)
|
||||||
|
|
||||||
@ -241,10 +262,11 @@ class AirliftPlanner:
|
|||||||
else:
|
else:
|
||||||
transfer = self.transfer
|
transfer = self.transfer
|
||||||
|
|
||||||
|
player = inventory.control_point.captured
|
||||||
flight = Flight(
|
flight = Flight(
|
||||||
self.package,
|
self.package,
|
||||||
self.game.player_country,
|
self.game.country_for(player),
|
||||||
unit_type,
|
squadron,
|
||||||
flight_size,
|
flight_size,
|
||||||
FlightType.TRANSPORT,
|
FlightType.TRANSPORT,
|
||||||
self.game.settings.default_start_type,
|
self.game.settings.default_start_type,
|
||||||
@ -363,7 +385,7 @@ class CargoShip(MultiGroupTransport):
|
|||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def route(self) -> List[Point]:
|
def route(self) -> Sequence[Point]:
|
||||||
return self.origin.shipping_lanes[self.destination]
|
return self.origin.shipping_lanes[self.destination]
|
||||||
|
|
||||||
def description(self) -> str:
|
def description(self) -> str:
|
||||||
@ -518,6 +540,7 @@ class PendingTransfers:
|
|||||||
flight = transport.flight
|
flight = transport.flight
|
||||||
flight.package.remove_flight(flight)
|
flight.package.remove_flight(flight)
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
self.game.aircraft_inventory.return_from_flight(flight)
|
||||||
|
flight.clear_roster()
|
||||||
|
|
||||||
@cancel_transport.register
|
@cancel_transport.register
|
||||||
def _cancel_transport_convoy(
|
def _cancel_transport_convoy(
|
||||||
@ -562,10 +585,14 @@ class PendingTransfers:
|
|||||||
|
|
||||||
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
|
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(control_point)
|
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(
|
return sum(
|
||||||
count
|
count
|
||||||
for unit_type, count in inventory.all_aircraft
|
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:
|
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
||||||
|
|||||||
@ -139,7 +139,9 @@ class PendingUnitDeliveries:
|
|||||||
) -> Optional[ControlPoint]:
|
) -> Optional[ControlPoint]:
|
||||||
sources = []
|
sources = []
|
||||||
for control_point in game.theater.control_points_for(self.destination.captured):
|
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)
|
sources.append(control_point)
|
||||||
|
|
||||||
if not sources:
|
if not sources:
|
||||||
|
|||||||
@ -7,12 +7,19 @@ from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
|||||||
from dcs.unittype import VehicleType
|
from dcs.unittype import VehicleType
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
|
from game.squadrons import Pilot
|
||||||
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
from game.theater import Airfield, ControlPoint, TheaterGroundObject
|
||||||
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
|
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
|
||||||
from game.transfers import CargoShip, Convoy, TransferOrder
|
from game.transfers import CargoShip, Convoy, TransferOrder
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class FlyingUnit:
|
||||||
|
flight: Flight
|
||||||
|
pilot: Pilot
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class FrontLineUnit:
|
class FrontLineUnit:
|
||||||
unit_type: Type[VehicleType]
|
unit_type: Type[VehicleType]
|
||||||
@ -45,7 +52,7 @@ class Building:
|
|||||||
|
|
||||||
class UnitMap:
|
class UnitMap:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.aircraft: Dict[str, Flight] = {}
|
self.aircraft: Dict[str, FlyingUnit] = {}
|
||||||
self.airfields: Dict[str, Airfield] = {}
|
self.airfields: Dict[str, Airfield] = {}
|
||||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||||
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
||||||
@ -55,17 +62,19 @@ class UnitMap:
|
|||||||
self.airlifts: Dict[str, AirliftUnit] = {}
|
self.airlifts: Dict[str, AirliftUnit] = {}
|
||||||
|
|
||||||
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
|
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
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
name = str(unit.name)
|
name = str(unit.name)
|
||||||
if name in self.aircraft:
|
if name in self.aircraft:
|
||||||
raise RuntimeError(f"Duplicate unit name: {name}")
|
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:
|
if flight.cargo is not None:
|
||||||
self.add_airlift_units(group, flight.cargo)
|
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)
|
return self.aircraft.get(unit_name, None)
|
||||||
|
|
||||||
def add_airfield(self, airfield: Airfield) -> None:
|
def add_airfield(self, airfield: Airfield) -> None:
|
||||||
|
|||||||
@ -73,4 +73,12 @@ VERSION = _build_version_string()
|
|||||||
#: * AAA_8_8cm_Flak_18,
|
#: * AAA_8_8cm_Flak_18,
|
||||||
#: * SPAAA_Vulcan_M163,
|
#: * SPAAA_Vulcan_M163,
|
||||||
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
#: * 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,
|
OptRestrictJettison,
|
||||||
OrbitAction,
|
OrbitAction,
|
||||||
RunwayAttack,
|
RunwayAttack,
|
||||||
SEAD,
|
|
||||||
StartCommand,
|
StartCommand,
|
||||||
Targets,
|
Targets,
|
||||||
Transport,
|
Transport,
|
||||||
WeaponType,
|
WeaponType,
|
||||||
|
TargetType,
|
||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
from dcs.terrain.terrain import Airport, NoParkingSlotError
|
||||||
from dcs.triggers import Event, TriggerOnce, TriggerRule
|
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.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||||
from dcs.unittype import FlyingType, UnitType
|
from dcs.unittype import FlyingType, UnitType
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||||
from game.data.weapons import Pylon
|
from game.data.weapons import Pylon
|
||||||
|
from game.db import GUN_RELIANT_AIRFRAMES
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from game.settings import Settings
|
from game.settings import Settings
|
||||||
|
from game.squadrons import Pilot, Squadron
|
||||||
from game.theater.controlpoint import (
|
from game.theater.controlpoint import (
|
||||||
Airfield,
|
Airfield,
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
@ -725,6 +727,70 @@ class AircraftConflictGenerator:
|
|||||||
return StartType.Cold
|
return StartType.Cold
|
||||||
return StartType.Warm
|
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(
|
def _setup_group(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup,
|
||||||
@ -735,34 +801,16 @@ class AircraftConflictGenerator:
|
|||||||
unit_type = group.units[0].unit_type
|
unit_type = group.units[0].unit_type
|
||||||
|
|
||||||
self._setup_payload(flight, group)
|
self._setup_payload(flight, group)
|
||||||
|
self._setup_livery(flight, group)
|
||||||
|
|
||||||
if unit_type in db.PLANE_LIVERY_OVERRIDES:
|
for unit, pilot in zip(group.units, flight.pilots):
|
||||||
for unit_instance in group.units:
|
player = pilot is not None and pilot.player
|
||||||
unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]
|
self.set_skill(unit, pilot, blue=flight.departure.captured)
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Do not generate player group with late activation.
|
# Do not generate player group with late activation.
|
||||||
if group.late_activation:
|
if player and group.late_activation:
|
||||||
group.late_activation = False
|
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:
|
if unit_type is F_14B:
|
||||||
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
|
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
|
||||||
|
|
||||||
@ -783,7 +831,7 @@ class AircraftConflictGenerator:
|
|||||||
self.flights.append(
|
self.flights.append(
|
||||||
FlightData(
|
FlightData(
|
||||||
package=package,
|
package=package,
|
||||||
country=faction.country,
|
country=self.game.faction_for(player=flight.departure.captured).country,
|
||||||
flight_type=flight.flight_type,
|
flight_type=flight.flight_type,
|
||||||
units=group.units,
|
units=group.units,
|
||||||
size=len(group.units),
|
size=len(group.units),
|
||||||
@ -1019,7 +1067,7 @@ class AircraftConflictGenerator:
|
|||||||
flight = Flight(
|
flight = Flight(
|
||||||
Package(control_point),
|
Package(control_point),
|
||||||
faction.country,
|
faction.country,
|
||||||
aircraft,
|
self.game.air_wing_for(control_point.captured).squadron_for(aircraft),
|
||||||
1,
|
1,
|
||||||
FlightType.BARCAP,
|
FlightType.BARCAP,
|
||||||
"Cold",
|
"Cold",
|
||||||
@ -1179,12 +1227,23 @@ class AircraftConflictGenerator:
|
|||||||
raise RuntimeError(f"No reduced fuel case for type {unit_type}")
|
raise RuntimeError(f"No reduced fuel case for type {unit_type}")
|
||||||
|
|
||||||
@staticmethod
|
@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(
|
def configure_behavior(
|
||||||
|
self,
|
||||||
|
flight: Flight,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup,
|
||||||
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
||||||
roe: Optional[OptROE.Values] = None,
|
roe: Optional[OptROE.Values] = None,
|
||||||
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
||||||
restrict_jettison: Optional[bool] = None,
|
restrict_jettison: Optional[bool] = None,
|
||||||
|
mission_uses_gun: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
group.points[0].tasks.clear()
|
group.points[0].tasks.clear()
|
||||||
if react_on_threat is not None:
|
if react_on_threat is not None:
|
||||||
@ -1196,6 +1255,17 @@ class AircraftConflictGenerator:
|
|||||||
if rtb_winchester is not None:
|
if rtb_winchester is not None:
|
||||||
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
|
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))
|
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
|
||||||
# Do not restrict afterburner.
|
# 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
|
# 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:
|
else:
|
||||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
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(
|
def configure_sweep(
|
||||||
self,
|
self,
|
||||||
@ -1238,7 +1308,7 @@ class AircraftConflictGenerator:
|
|||||||
else:
|
else:
|
||||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
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(
|
def configure_cas(
|
||||||
self,
|
self,
|
||||||
@ -1250,6 +1320,7 @@ class AircraftConflictGenerator:
|
|||||||
group.task = CAS.name
|
group.task = CAS.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.OpenFire,
|
roe=OptROE.Values.OpenFire,
|
||||||
@ -1273,11 +1344,13 @@ class AircraftConflictGenerator:
|
|||||||
group.task = CAS.name
|
group.task = CAS.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.OpenFire,
|
roe=OptROE.Values.OpenFire,
|
||||||
rtb_winchester=OptRTBOnOutOfAmmo.Values.All,
|
rtb_winchester=OptRTBOnOutOfAmmo.Values.All,
|
||||||
restrict_jettison=True,
|
restrict_jettison=True,
|
||||||
|
mission_uses_gun=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure_sead(
|
def configure_sead(
|
||||||
@ -1287,14 +1360,21 @@ class AircraftConflictGenerator:
|
|||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
) -> None:
|
) -> 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._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.OpenFire,
|
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,
|
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
|
||||||
restrict_jettison=True,
|
restrict_jettison=True,
|
||||||
|
mission_uses_gun=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure_strike(
|
def configure_strike(
|
||||||
@ -1307,10 +1387,12 @@ class AircraftConflictGenerator:
|
|||||||
group.task = GroundAttack.name
|
group.task = GroundAttack.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.OpenFire,
|
roe=OptROE.Values.OpenFire,
|
||||||
restrict_jettison=True,
|
restrict_jettison=True,
|
||||||
|
mission_uses_gun=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure_anti_ship(
|
def configure_anti_ship(
|
||||||
@ -1323,10 +1405,12 @@ class AircraftConflictGenerator:
|
|||||||
group.task = AntishipStrike.name
|
group.task = AntishipStrike.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.OpenFire,
|
roe=OptROE.Values.OpenFire,
|
||||||
restrict_jettison=True,
|
restrict_jettison=True,
|
||||||
|
mission_uses_gun=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure_runway_attack(
|
def configure_runway_attack(
|
||||||
@ -1339,10 +1423,12 @@ class AircraftConflictGenerator:
|
|||||||
group.task = RunwayAttack.name
|
group.task = RunwayAttack.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.OpenFire,
|
roe=OptROE.Values.OpenFire,
|
||||||
restrict_jettison=True,
|
restrict_jettison=True,
|
||||||
|
mission_uses_gun=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure_oca_strike(
|
def configure_oca_strike(
|
||||||
@ -1355,6 +1441,7 @@ class AircraftConflictGenerator:
|
|||||||
group.task = CAS.name
|
group.task = CAS.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.OpenFire,
|
roe=OptROE.Values.OpenFire,
|
||||||
@ -1380,6 +1467,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
# Awacs task action
|
# Awacs task action
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.WeaponHold,
|
roe=OptROE.Values.WeaponHold,
|
||||||
@ -1401,7 +1489,30 @@ class AircraftConflictGenerator:
|
|||||||
group.task = CAP.name
|
group.task = CAP.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
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(
|
def configure_transport(
|
||||||
@ -1414,6 +1525,7 @@ class AircraftConflictGenerator:
|
|||||||
group.task = Transport.name
|
group.task = Transport.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
self.configure_behavior(
|
self.configure_behavior(
|
||||||
|
flight,
|
||||||
group,
|
group,
|
||||||
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
react_on_threat=OptReactOnThreat.Values.EvadeFire,
|
||||||
roe=OptROE.Values.WeaponHold,
|
roe=OptROE.Values.WeaponHold,
|
||||||
@ -1422,7 +1534,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
|
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
|
||||||
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||||
self.configure_behavior(group)
|
self.configure_behavior(flight, group)
|
||||||
|
|
||||||
def setup_flight_group(
|
def setup_flight_group(
|
||||||
self,
|
self,
|
||||||
@ -1448,6 +1560,8 @@ class AircraftConflictGenerator:
|
|||||||
self.configure_dead(group, package, flight, dynamic_runways)
|
self.configure_dead(group, package, flight, dynamic_runways)
|
||||||
elif flight_type == FlightType.SEAD:
|
elif flight_type == FlightType.SEAD:
|
||||||
self.configure_sead(group, package, flight, dynamic_runways)
|
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:
|
elif flight_type == FlightType.STRIKE:
|
||||||
self.configure_strike(group, package, flight, dynamic_runways)
|
self.configure_strike(group, package, flight, dynamic_runways)
|
||||||
elif flight_type == FlightType.ANTISHIP:
|
elif flight_type == FlightType.ANTISHIP:
|
||||||
@ -1695,29 +1809,32 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
|
|||||||
waypoint = super().build()
|
waypoint = super().build()
|
||||||
|
|
||||||
# TODO: Add common "UnitGroupTarget" base type.
|
# TODO: Add common "UnitGroupTarget" base type.
|
||||||
target_group = self.package.target
|
group_names = []
|
||||||
if isinstance(target_group, TheaterGroundObject):
|
target = self.package.target
|
||||||
group_name = target_group.group_name
|
if isinstance(target, TheaterGroundObject):
|
||||||
elif isinstance(target_group, MultiGroupTransport):
|
for group in target.groups:
|
||||||
group_name = target_group.name
|
group_names.append(group.name)
|
||||||
|
elif isinstance(target, MultiGroupTransport):
|
||||||
|
group_names.append(target.name)
|
||||||
else:
|
else:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Unexpected target type for BAI mission: %s",
|
"Unexpected target type for BAI mission: %s",
|
||||||
target_group.__class__.__name__,
|
target.__class__.__name__,
|
||||||
)
|
)
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
group = self.mission.find_group(group_name)
|
for group_name in group_names:
|
||||||
if group is None:
|
group = self.mission.find_group(group_name)
|
||||||
logging.error("Could not find group for BAI mission %s", group_name)
|
if group is None:
|
||||||
return waypoint
|
logging.error("Could not find group for BAI mission %s", group_name)
|
||||||
|
continue
|
||||||
|
|
||||||
task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
|
task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
|
||||||
task.params["attackQtyLimit"] = False
|
task.params["attackQtyLimit"] = False
|
||||||
task.params["directionEnabled"] = False
|
task.params["directionEnabled"] = False
|
||||||
task.params["altitudeEnabled"] = False
|
task.params["altitudeEnabled"] = False
|
||||||
task.params["groupAttack"] = True
|
task.params["groupAttack"] = True
|
||||||
waypoint.tasks.append(task)
|
waypoint.tasks.append(task)
|
||||||
return waypoint
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
@ -1754,23 +1871,29 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
|||||||
class DeadIngressBuilder(PydcsWaypointBuilder):
|
class DeadIngressBuilder(PydcsWaypointBuilder):
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
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)
|
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
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
@ -1822,25 +1945,29 @@ class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
|
|||||||
class SeadIngressBuilder(PydcsWaypointBuilder):
|
class SeadIngressBuilder(PydcsWaypointBuilder):
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
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)
|
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
|
return waypoint
|
||||||
|
|
||||||
|
|
||||||
@ -1903,7 +2030,10 @@ class SweepIngressBuilder(PydcsWaypointBuilder):
|
|||||||
waypoint.tasks.append(
|
waypoint.tasks.append(
|
||||||
EngageTargets(
|
EngageTargets(
|
||||||
max_distance=int(nautical_miles(50).meters),
|
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:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
waypoint = super().build()
|
||||||
if self.flight.flight_type == FlightType.ESCORT:
|
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
|
return waypoint
|
||||||
|
|
||||||
@staticmethod
|
@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
|
# 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
|
# the AI automatically but the AI only escorts AI flights while they are
|
||||||
# traveling between waypoints. When an AI flight performs an attack
|
# 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
|
# for the target area that is set to end on a flag flip that occurs when
|
||||||
# the strike aircraft finish their attack task.
|
# 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(
|
waypoint.add_task(
|
||||||
ControlledTask(
|
ControlledTask(
|
||||||
EngageTargets(
|
EngageTargets(
|
||||||
# TODO: From doctrine.
|
# TODO: From doctrine.
|
||||||
max_distance=int(nautical_miles(30).meters),
|
max_distance=int(nautical_miles(30).meters),
|
||||||
targets=[
|
targets=target_types,
|
||||||
Targets.All.Air.Planes.Fighters,
|
|
||||||
Targets.All.Air.Planes.MultiroleFighters,
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -620,9 +620,9 @@ AIRFIELD_DATA = {
|
|||||||
tacan=TacanChannel(78, TacanBand.X),
|
tacan=TacanChannel(78, TacanBand.X),
|
||||||
tacan_callsign="BND",
|
tacan_callsign="BND",
|
||||||
vor=("BND", MHz(117, 200)),
|
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={
|
ils={
|
||||||
"21": ("IBND", MHz(333, 800)),
|
"21": ("IBND", MHz(109, 900)),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"Jiroft": AirfieldData(
|
"Jiroft": AirfieldData(
|
||||||
|
|||||||
@ -67,6 +67,10 @@ class Package:
|
|||||||
|
|
||||||
waypoints: Optional[PackageWaypoints] = field(default=None)
|
waypoints: Optional[PackageWaypoints] = field(default=None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_players(self) -> bool:
|
||||||
|
return any(flight.client_count for flight in self.flights)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def formation_speed(self) -> Optional[Speed]:
|
def formation_speed(self) -> Optional[Speed]:
|
||||||
"""The speed of the package when in formation.
|
"""The speed of the package when in formation.
|
||||||
|
|||||||
@ -17,12 +17,17 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Tuple,
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dcs.unittype import FlyingType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
|
from game.factions.faction import Faction
|
||||||
from game.infos.information import Information
|
from game.infos.information import Information
|
||||||
from game.procurement import AircraftProcurementRequest
|
from game.procurement import AircraftProcurementRequest
|
||||||
|
from game.profiling import logged_duration, MultiEventTracer
|
||||||
|
from game.squadrons import AirWing, Squadron
|
||||||
from game.theater import (
|
from game.theater import (
|
||||||
Airfield,
|
Airfield,
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
@ -40,8 +45,7 @@ from game.theater.theatergroundobject import (
|
|||||||
VehicleGroupGroundObject,
|
VehicleGroupGroundObject,
|
||||||
)
|
)
|
||||||
from game.transfers import CargoShip, Convoy
|
from game.transfers import CargoShip, Convoy
|
||||||
from game.utils import Distance, nautical_miles
|
from game.utils import Distance, nautical_miles, meters
|
||||||
from gen import Conflict
|
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||||
from gen.flights.closestairfields import (
|
from gen.flights.closestairfields import (
|
||||||
@ -109,6 +113,8 @@ class ProposedMission:
|
|||||||
#: The proposed flights that are required for the mission.
|
#: The proposed flights that are required for the mission.
|
||||||
flights: List[ProposedFlight]
|
flights: List[ProposedFlight]
|
||||||
|
|
||||||
|
asap: bool = field(default=False)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
flights = ", ".join([str(f) for f in self.flights])
|
flights = ", ".join([str(f) for f in self.flights])
|
||||||
return f"{self.location.name}: {flights}"
|
return f"{self.location.name}: {flights}"
|
||||||
@ -119,17 +125,19 @@ class AircraftAllocator:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
air_wing: AirWing,
|
||||||
closest_airfields: ClosestAirfields,
|
closest_airfields: ClosestAirfields,
|
||||||
global_inventory: GlobalAircraftInventory,
|
global_inventory: GlobalAircraftInventory,
|
||||||
is_player: bool,
|
is_player: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self.air_wing = air_wing
|
||||||
self.closest_airfields = closest_airfields
|
self.closest_airfields = closest_airfields
|
||||||
self.global_inventory = global_inventory
|
self.global_inventory = global_inventory
|
||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
|
|
||||||
def find_aircraft_for_flight(
|
def find_squadron_for_flight(
|
||||||
self, flight: ProposedFlight
|
self, flight: ProposedFlight
|
||||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||||
"""Finds aircraft suitable for the given mission.
|
"""Finds aircraft suitable for the given mission.
|
||||||
|
|
||||||
Searches for aircraft capable of performing the given mission within the
|
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
|
on subsequent calls. If the found aircraft are not used, the caller is
|
||||||
responsible for returning them to the inventory.
|
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(
|
def find_aircraft_for_task(
|
||||||
self,
|
self, flight: ProposedFlight, task: FlightType
|
||||||
flight: ProposedFlight,
|
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||||
types: List[Type[FlyingType]],
|
types = aircraft_for_task(task)
|
||||||
) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
|
|
||||||
airfields_in_range = self.closest_airfields.airfields_within(
|
airfields_in_range = self.closest_airfields.airfields_within(
|
||||||
flight.max_distance
|
flight.max_distance
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Prefer using squadrons with pilots first
|
||||||
|
best_understaffed: Optional[Tuple[ControlPoint, Squadron]] = None
|
||||||
for airfield in airfields_in_range:
|
for airfield in airfields_in_range:
|
||||||
if not airfield.is_friendly(self.is_player):
|
if not airfield.is_friendly(self.is_player):
|
||||||
continue
|
continue
|
||||||
@ -165,11 +175,28 @@ class AircraftAllocator:
|
|||||||
for aircraft in types:
|
for aircraft in types:
|
||||||
if not airfield.can_operate(aircraft):
|
if not airfield.can_operate(aircraft):
|
||||||
continue
|
continue
|
||||||
if inventory.available(aircraft) >= flight.num_aircraft:
|
if inventory.available(aircraft) < flight.num_aircraft:
|
||||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
continue
|
||||||
return airfield, aircraft
|
# 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:
|
class PackageBuilder:
|
||||||
@ -180,16 +207,18 @@ class PackageBuilder:
|
|||||||
location: MissionTarget,
|
location: MissionTarget,
|
||||||
closest_airfields: ClosestAirfields,
|
closest_airfields: ClosestAirfields,
|
||||||
global_inventory: GlobalAircraftInventory,
|
global_inventory: GlobalAircraftInventory,
|
||||||
|
air_wing: AirWing,
|
||||||
is_player: bool,
|
is_player: bool,
|
||||||
package_country: str,
|
package_country: str,
|
||||||
start_type: str,
|
start_type: str,
|
||||||
|
asap: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.closest_airfields = closest_airfields
|
self.closest_airfields = closest_airfields
|
||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
self.package_country = package_country
|
self.package_country = package_country
|
||||||
self.package = Package(location)
|
self.package = Package(location, auto_asap=asap)
|
||||||
self.allocator = AircraftAllocator(
|
self.allocator = AircraftAllocator(
|
||||||
closest_airfields, global_inventory, is_player
|
air_wing, closest_airfields, global_inventory, is_player
|
||||||
)
|
)
|
||||||
self.global_inventory = global_inventory
|
self.global_inventory = global_inventory
|
||||||
self.start_type = start_type
|
self.start_type = start_type
|
||||||
@ -202,10 +231,10 @@ class PackageBuilder:
|
|||||||
caller should return any previously planned flights to the inventory
|
caller should return any previously planned flights to the inventory
|
||||||
using release_planned_aircraft.
|
using release_planned_aircraft.
|
||||||
"""
|
"""
|
||||||
assignment = self.allocator.find_aircraft_for_flight(plan)
|
assignment = self.allocator.find_squadron_for_flight(plan)
|
||||||
if assignment is None:
|
if assignment is None:
|
||||||
return False
|
return False
|
||||||
airfield, aircraft = assignment
|
airfield, squadron = assignment
|
||||||
if isinstance(airfield, OffMapSpawn):
|
if isinstance(airfield, OffMapSpawn):
|
||||||
start_type = "In Flight"
|
start_type = "In Flight"
|
||||||
else:
|
else:
|
||||||
@ -214,13 +243,13 @@ class PackageBuilder:
|
|||||||
flight = Flight(
|
flight = Flight(
|
||||||
self.package,
|
self.package,
|
||||||
self.package_country,
|
self.package_country,
|
||||||
aircraft,
|
squadron,
|
||||||
plan.num_aircraft,
|
plan.num_aircraft,
|
||||||
plan.task,
|
plan.task,
|
||||||
start_type,
|
start_type,
|
||||||
departure=airfield,
|
departure=airfield,
|
||||||
arrival=airfield,
|
arrival=airfield,
|
||||||
divert=self.find_divert_field(aircraft, airfield),
|
divert=self.find_divert_field(squadron.aircraft, airfield),
|
||||||
)
|
)
|
||||||
self.package.add_flight(flight)
|
self.package.add_flight(flight)
|
||||||
return True
|
return True
|
||||||
@ -250,9 +279,13 @@ class PackageBuilder:
|
|||||||
flights = list(self.package.flights)
|
flights = list(self.package.flights)
|
||||||
for flight in flights:
|
for flight in flights:
|
||||||
self.global_inventory.return_from_flight(flight)
|
self.global_inventory.return_from_flight(flight)
|
||||||
|
flight.clear_roster()
|
||||||
self.package.remove_flight(flight)
|
self.package.remove_flight(flight)
|
||||||
|
|
||||||
|
|
||||||
|
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
||||||
|
|
||||||
|
|
||||||
class ObjectiveFinder:
|
class ObjectiveFinder:
|
||||||
"""Identifies potential objectives for the mission planner."""
|
"""Identifies potential objectives for the mission planner."""
|
||||||
|
|
||||||
@ -264,41 +297,53 @@ class ObjectiveFinder:
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.is_player = is_player
|
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."""
|
"""Iterates over all enemy SAM sites."""
|
||||||
# Control points might have the same ground object several times, for
|
doctrine = self.game.faction_for(self.is_player).doctrine
|
||||||
# some reason.
|
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
found_targets: Set[str] = set()
|
|
||||||
for cp in self.enemy_control_points():
|
for cp in self.enemy_control_points():
|
||||||
for ground_object in cp.ground_objects:
|
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:
|
if ground_object.is_dead:
|
||||||
continue
|
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
|
continue
|
||||||
|
|
||||||
if not ground_object.has_radar:
|
yield ground_object, target_range
|
||||||
continue
|
|
||||||
|
|
||||||
# TODO: Yield in order of most threatening.
|
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject]:
|
||||||
# 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]:
|
|
||||||
"""Iterates over enemy SAMs in threat range of friendly control points.
|
"""Iterates over enemy SAMs in threat range of friendly control points.
|
||||||
|
|
||||||
SAM sites are sorted by their closest proximity to any friendly control
|
SAM sites are sorted by their closest proximity to any friendly control
|
||||||
point (airfield or fleet).
|
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]:
|
def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]:
|
||||||
"""Iterates over all enemy vehicle groups."""
|
"""Iterates over all enemy vehicle groups."""
|
||||||
@ -340,9 +385,9 @@ class ObjectiveFinder:
|
|||||||
return self._targets_by_range(self.enemy_ships())
|
return self._targets_by_range(self.enemy_ships())
|
||||||
|
|
||||||
def _targets_by_range(
|
def _targets_by_range(
|
||||||
self, targets: Iterable[MissionTarget]
|
self, targets: Iterable[MissionTargetType]
|
||||||
) -> Iterator[MissionTarget]:
|
) -> Iterator[MissionTargetType]:
|
||||||
target_ranges: List[Tuple[MissionTarget, int]] = []
|
target_ranges: List[Tuple[MissionTargetType, int]] = []
|
||||||
for target in targets:
|
for target in targets:
|
||||||
ranges: List[int] = []
|
ranges: List[int] = []
|
||||||
for cp in self.friendly_control_points():
|
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.procurement_requests = self.game.procurement_requests_for(self.is_player)
|
||||||
self.faction = self.game.faction_for(self.is_player)
|
self.faction = self.game.faction_for(self.is_player)
|
||||||
|
|
||||||
def faction_can_plan(self, mission_type: FlightType) -> bool:
|
def air_wing_can_plan(self, mission_type: FlightType) -> bool:
|
||||||
"""Returns True if it is possible for the faction to plan this mission type.
|
"""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
|
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.
|
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)
|
all_compatible = aircraft_for_task(mission_type)
|
||||||
for aircraft in self.faction.aircrafts:
|
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
|
||||||
if aircraft in all_compatible:
|
if (
|
||||||
|
squadron.aircraft in all_compatible
|
||||||
|
and mission_type in squadron.mission_types
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -578,7 +628,10 @@ class CoalitionMissionPlanner:
|
|||||||
cp = self.objective_finder.farthest_friendly_control_point()
|
cp = self.objective_finder.farthest_friendly_control_point()
|
||||||
if cp is not None:
|
if cp is not None:
|
||||||
yield ProposedMission(
|
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.
|
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||||
@ -630,17 +683,36 @@ class CoalitionMissionPlanner:
|
|||||||
# or objects, plan DEAD.
|
# or objects, plan DEAD.
|
||||||
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
||||||
# friendly CPs, front, lines, or objects, plan DEAD.
|
# friendly CPs, front, lines, or objects, plan DEAD.
|
||||||
for sam in self.objective_finder.threatening_sams():
|
for sam in self.objective_finder.threatening_air_defenses():
|
||||||
yield ProposedMission(
|
flights = [ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE)]
|
||||||
sam,
|
|
||||||
[
|
# Only include SEAD against SAMs that still have emitters. No need to
|
||||||
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
|
# suppress an EWR, and SEAD isn't useful against a SAM that no longer has a
|
||||||
# TODO: Max escort range.
|
# 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(
|
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,
|
# 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
|
# 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
|
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
|
||||||
),
|
),
|
||||||
ProposedFlight(
|
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
|
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
|
||||||
),
|
),
|
||||||
ProposedFlight(
|
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
|
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
|
||||||
),
|
),
|
||||||
ProposedFlight(
|
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
|
FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
|
||||||
),
|
),
|
||||||
ProposedFlight(
|
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
|
FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
|
||||||
),
|
),
|
||||||
ProposedFlight(
|
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:
|
def plan_missions(self) -> None:
|
||||||
"""Identifies and plans mission for the turn."""
|
"""Identifies and plans mission for the turn."""
|
||||||
for proposed_mission in self.propose_missions():
|
player = "Blue" if self.is_player else "Red"
|
||||||
self.plan_mission(proposed_mission)
|
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():
|
with logged_duration(f"{player} reserve mission planning"):
|
||||||
self.plan_mission(critical_mission, reserves=True)
|
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():
|
for cp in self.objective_finder.friendly_control_points():
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(cp)
|
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]:
|
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
||||||
threats = defaultdict(bool)
|
threats = defaultdict(bool)
|
||||||
for flight in builder.package.flights:
|
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
|
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
|
threats[EscortType.Sead] = True
|
||||||
return threats
|
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."""
|
"""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(
|
builder = PackageBuilder(
|
||||||
mission.location,
|
mission.location,
|
||||||
self.objective_finder.closest_airfields_to(mission.location),
|
self.objective_finder.closest_airfields_to(mission.location),
|
||||||
self.game.aircraft_inventory,
|
self.game.aircraft_inventory,
|
||||||
|
self.game.air_wing_for(self.is_player),
|
||||||
self.is_player,
|
self.is_player,
|
||||||
package_country,
|
self.game.country_for(self.is_player),
|
||||||
self.game.settings.default_start_type,
|
self.game.settings.default_start_type,
|
||||||
|
mission.asap,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attempt to plan all the main elements of the mission first. Escorts
|
# Attempt to plan all the main elements of the mission first. Escorts
|
||||||
@ -846,17 +929,20 @@ class CoalitionMissionPlanner:
|
|||||||
missing_types: Set[FlightType] = set()
|
missing_types: Set[FlightType] = set()
|
||||||
escorts = []
|
escorts = []
|
||||||
for proposed_flight in mission.flights:
|
for proposed_flight in mission.flights:
|
||||||
if not self.faction_can_plan(proposed_flight.task):
|
if not self.air_wing_can_plan(proposed_flight.task):
|
||||||
# This faction can never plan this mission type because they do not have
|
# This air wing can never plan this mission type because they do not
|
||||||
# compatible aircraft. Skip fulfillment so that we don't place the
|
# have compatible aircraft or squadrons. Skip fulfillment so that we
|
||||||
# purchase request.
|
# don't place the purchase request.
|
||||||
continue
|
continue
|
||||||
if proposed_flight.escort_type is not None:
|
if proposed_flight.escort_type is not None:
|
||||||
# Escorts are planned after the primary elements of the package.
|
# Escorts are planned after the primary elements of the package.
|
||||||
# If the package does not need escorts they may be pruned.
|
# If the package does not need escorts they may be pruned.
|
||||||
escorts.append(proposed_flight)
|
escorts.append(proposed_flight)
|
||||||
continue
|
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:
|
if missing_types:
|
||||||
self.scrub_mission_missing_aircraft(
|
self.scrub_mission_missing_aircraft(
|
||||||
@ -880,7 +966,8 @@ class CoalitionMissionPlanner:
|
|||||||
self.game, builder.package, self.is_player
|
self.game, builder.package, self.is_player
|
||||||
)
|
)
|
||||||
for flight in builder.package.flights:
|
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)
|
needed_escorts = self.check_needed_escorts(builder)
|
||||||
for escort in escorts:
|
for escort in escorts:
|
||||||
@ -888,7 +975,8 @@ class CoalitionMissionPlanner:
|
|||||||
# impossible.
|
# impossible.
|
||||||
assert escort.escort_type is not None
|
assert escort.escort_type is not None
|
||||||
if needed_escorts[escort.escort_type]:
|
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
|
# Check again for unavailable aircraft. If the escort was required and
|
||||||
# none were found, scrub the mission.
|
# none were found, scrub the mission.
|
||||||
@ -908,7 +996,13 @@ class CoalitionMissionPlanner:
|
|||||||
# Add flight plans for escorts.
|
# Add flight plans for escorts.
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
if not flight.flight_plan.waypoints:
|
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)
|
self.ato.add_package(package)
|
||||||
|
|
||||||
def stagger_missions(self) -> None:
|
def stagger_missions(self) -> None:
|
||||||
@ -956,6 +1050,8 @@ class CoalitionMissionPlanner:
|
|||||||
logging.error(f"Could not determine mission end time for {package}")
|
logging.error(f"Could not determine mission end time for {package}")
|
||||||
continue
|
continue
|
||||||
previous_cap_end_time[package.target] = departure_time
|
previous_cap_end_time[package.target] = departure_time
|
||||||
|
elif package.auto_asap:
|
||||||
|
package.set_tot_asap()
|
||||||
else:
|
else:
|
||||||
# But other packages should be spread out a bit. Note that take
|
# But other packages should be spread out a bit. Note that take
|
||||||
# times are delayed, but all aircraft will become active at
|
# 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 = [
|
SEAD_CAPABLE = [
|
||||||
JF_17,
|
JF_17,
|
||||||
F_16C_50,
|
F_16C_50,
|
||||||
@ -240,6 +240,8 @@ SEAD_CAPABLE = [
|
|||||||
Su_25TM,
|
Su_25TM,
|
||||||
F_4E,
|
F_4E,
|
||||||
A_4E_C,
|
A_4E_C,
|
||||||
|
F_14B,
|
||||||
|
F_14A_135_GR,
|
||||||
AV8BNA,
|
AV8BNA,
|
||||||
Su_24M,
|
Su_24M,
|
||||||
Su_17M4,
|
Su_17M4,
|
||||||
@ -394,7 +396,7 @@ AEWC_CAPABLE = [
|
|||||||
|
|
||||||
|
|
||||||
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
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:
|
if task in cap_missions:
|
||||||
return CAP_CAPABLE
|
return CAP_CAPABLE
|
||||||
elif task == FlightType.ANTISHIP:
|
elif task == FlightType.ANTISHIP:
|
||||||
@ -405,6 +407,8 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
|||||||
return CAS_CAPABLE
|
return CAS_CAPABLE
|
||||||
elif task == FlightType.SEAD:
|
elif task == FlightType.SEAD:
|
||||||
return SEAD_CAPABLE
|
return SEAD_CAPABLE
|
||||||
|
elif task == FlightType.SEAD_ESCORT:
|
||||||
|
return SEAD_CAPABLE
|
||||||
elif task == FlightType.DEAD:
|
elif task == FlightType.DEAD:
|
||||||
return DEAD_CAPABLE
|
return DEAD_CAPABLE
|
||||||
elif task == FlightType.OCA_AIRCRAFT:
|
elif task == FlightType.OCA_AIRCRAFT:
|
||||||
@ -422,3 +426,11 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
|
|||||||
else:
|
else:
|
||||||
logging.error(f"Unplannable flight type: {task}")
|
logging.error(f"Unplannable flight type: {task}")
|
||||||
return []
|
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 dcs.unittype import FlyingType
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
|
from game.squadrons import Pilot, Squadron
|
||||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||||
from game.utils import Distance, meters
|
from game.utils import Distance, meters
|
||||||
from gen.flights.loadouts import Loadout
|
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
|
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
|
each flight and thus a part of the ATO, so changing these values will break
|
||||||
save compat.
|
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"
|
TARCAP = "TARCAP"
|
||||||
@ -45,12 +67,34 @@ class FlightType(Enum):
|
|||||||
OCA_AIRCRAFT = "OCA/Aircraft"
|
OCA_AIRCRAFT = "OCA/Aircraft"
|
||||||
AEWC = "AEW&C"
|
AEWC = "AEW&C"
|
||||||
TRANSPORT = "Transport"
|
TRANSPORT = "Transport"
|
||||||
|
SEAD_ESCORT = "SEAD Escort"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.value
|
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):
|
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
|
TAKEOFF = 0 # Take off point
|
||||||
ASCEND_POINT = 1 # Ascension point after take off
|
ASCEND_POINT = 1 # Ascension point after take off
|
||||||
PATROL = 2 # Patrol point
|
PATROL = 2 # Patrol point
|
||||||
@ -65,7 +109,7 @@ class FlightWaypointType(Enum):
|
|||||||
LANDING_POINT = 11 # Should land there
|
LANDING_POINT = 11 # Should land there
|
||||||
TARGET_POINT = 12 # A target building or static object, position
|
TARGET_POINT = 12 # A target building or static object, position
|
||||||
TARGET_GROUP_LOC = 13 # A target group approximate location
|
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)
|
CUSTOM = 15 # User waypoint (no specific behaviour)
|
||||||
JOIN = 16
|
JOIN = 16
|
||||||
SPLIT = 17
|
SPLIT = 17
|
||||||
@ -163,7 +207,7 @@ class Flight:
|
|||||||
self,
|
self,
|
||||||
package: Package,
|
package: Package,
|
||||||
country: str,
|
country: str,
|
||||||
unit_type: Type[FlyingType],
|
squadron: Squadron,
|
||||||
count: int,
|
count: int,
|
||||||
flight_type: FlightType,
|
flight_type: FlightType,
|
||||||
start_type: str,
|
start_type: str,
|
||||||
@ -175,8 +219,8 @@ class Flight:
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.package = package
|
self.package = package
|
||||||
self.country = country
|
self.country = country
|
||||||
self.unit_type = unit_type
|
self.squadron = squadron
|
||||||
self.count = count
|
self.pilots = [squadron.claim_available_pilot() for _ in range(count)]
|
||||||
self.departure = departure
|
self.departure = departure
|
||||||
self.arrival = arrival
|
self.arrival = arrival
|
||||||
self.divert = divert
|
self.divert = divert
|
||||||
@ -186,7 +230,6 @@ class Flight:
|
|||||||
self.loadout = Loadout.default_for(self)
|
self.loadout = Loadout.default_for(self)
|
||||||
self.start_type = start_type
|
self.start_type = start_type
|
||||||
self.use_custom_loadout = False
|
self.use_custom_loadout = False
|
||||||
self.client_count = 0
|
|
||||||
self.custom_name = custom_name
|
self.custom_name = custom_name
|
||||||
|
|
||||||
# Only used by transport missions.
|
# Only used by transport missions.
|
||||||
@ -201,6 +244,18 @@ class Flight:
|
|||||||
package=package, flight=self, custom_waypoints=[]
|
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
|
@property
|
||||||
def from_cp(self) -> ControlPoint:
|
def from_cp(self) -> ControlPoint:
|
||||||
return self.departure
|
return self.departure
|
||||||
@ -209,6 +264,34 @@ class Flight:
|
|||||||
def points(self) -> List[FlightWaypoint]:
|
def points(self) -> List[FlightWaypoint]:
|
||||||
return self.flight_plan.waypoints[1:]
|
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):
|
def __repr__(self):
|
||||||
name = db.unit_type_name(self.unit_type)
|
name = db.unit_type_name(self.unit_type)
|
||||||
if self.custom_name:
|
if self.custom_name:
|
||||||
|
|||||||
@ -10,18 +10,18 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
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.mapping import Point
|
||||||
|
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from shapely.geometry import Point as ShapelyPoint
|
from shapely.geometry import Point as ShapelyPoint
|
||||||
|
|
||||||
from game.data.doctrine import Doctrine
|
from game.data.doctrine import Doctrine
|
||||||
|
from game.squadrons import Pilot
|
||||||
from game.theater import (
|
from game.theater import (
|
||||||
Airfield,
|
Airfield,
|
||||||
ControlPoint,
|
ControlPoint,
|
||||||
@ -125,6 +125,10 @@ class FlightPlan:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot(self) -> timedelta:
|
||||||
|
return self.package.time_over_target + self.tot_offset
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def bingo_fuel(self) -> int:
|
def bingo_fuel(self) -> int:
|
||||||
"""Bingo fuel value for the FlightPlan"""
|
"""Bingo fuel value for the FlightPlan"""
|
||||||
@ -198,15 +202,28 @@ class FlightPlan:
|
|||||||
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
|
def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
|
||||||
return None
|
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]:
|
def takeoff_time(self) -> Optional[timedelta]:
|
||||||
tot_waypoint = self.tot_waypoint
|
tot_waypoint = self.tot_waypoint
|
||||||
if tot_waypoint is None:
|
if tot_waypoint is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
time = self.tot_for_waypoint(tot_waypoint)
|
time = self.tot
|
||||||
if time is None:
|
if time is None:
|
||||||
return None
|
return None
|
||||||
time += self.tot_offset
|
|
||||||
return time - self._travel_time_to_waypoint(tot_waypoint)
|
return time - self._travel_time_to_waypoint(tot_waypoint)
|
||||||
|
|
||||||
def startup_time(self) -> Optional[timedelta]:
|
def startup_time(self) -> Optional[timedelta]:
|
||||||
@ -243,7 +260,7 @@ class FlightPlan:
|
|||||||
if self.flight.from_cp.is_fleet:
|
if self.flight.from_cp.is_fleet:
|
||||||
return timedelta(minutes=2)
|
return timedelta(minutes=2)
|
||||||
else:
|
else:
|
||||||
return timedelta(minutes=5)
|
return timedelta(minutes=8)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mission_departure_time(self) -> timedelta:
|
def mission_departure_time(self) -> timedelta:
|
||||||
@ -506,7 +523,7 @@ class TarCapFlightPlan(PatrollingFlightPlan):
|
|||||||
start = self.package.escort_start_time
|
start = self.package.escort_start_time
|
||||||
if start is not None:
|
if start is not None:
|
||||||
return start + self.tot_offset
|
return start + self.tot_offset
|
||||||
return super().patrol_start_time + self.tot_offset
|
return self.tot
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def patrol_end_time(self) -> timedelta:
|
def patrol_end_time(self) -> timedelta:
|
||||||
@ -530,6 +547,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
land: FlightWaypoint
|
land: FlightWaypoint
|
||||||
divert: Optional[FlightWaypoint]
|
divert: Optional[FlightWaypoint]
|
||||||
bullseye: FlightWaypoint
|
bullseye: FlightWaypoint
|
||||||
|
lead_time: timedelta = field(default_factory=timedelta)
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
yield self.takeoff
|
yield self.takeoff
|
||||||
@ -568,6 +586,13 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
def tot_waypoint(self) -> FlightWaypoint:
|
def tot_waypoint(self) -> FlightWaypoint:
|
||||||
return self.targets[0]
|
return self.targets[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tot_offset(self) -> timedelta:
|
||||||
|
try:
|
||||||
|
return -self.lead_time
|
||||||
|
except AttributeError:
|
||||||
|
return timedelta()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_area_waypoint(self) -> FlightWaypoint:
|
def target_area_waypoint(self) -> FlightWaypoint:
|
||||||
return FlightWaypoint(
|
return FlightWaypoint(
|
||||||
@ -600,10 +625,6 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
)
|
)
|
||||||
return total
|
return total
|
||||||
|
|
||||||
@property
|
|
||||||
def mission_speed(self) -> Speed:
|
|
||||||
return GroundSpeed.for_flight(self.flight, self.ingress.alt)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def join_time(self) -> timedelta:
|
def join_time(self) -> timedelta:
|
||||||
travel_time = self.travel_time_between_waypoints(self.join, self.ingress)
|
travel_time = self.travel_time_between_waypoints(self.join, self.ingress)
|
||||||
@ -616,7 +637,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ingress_time(self) -> timedelta:
|
def ingress_time(self) -> timedelta:
|
||||||
tot = self.package.time_over_target
|
tot = self.tot
|
||||||
travel_time = self.travel_time_between_waypoints(
|
travel_time = self.travel_time_between_waypoints(
|
||||||
self.ingress, self.target_area_waypoint
|
self.ingress, self.target_area_waypoint
|
||||||
)
|
)
|
||||||
@ -624,7 +645,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def egress_time(self) -> timedelta:
|
def egress_time(self) -> timedelta:
|
||||||
tot = self.package.time_over_target
|
tot = self.tot
|
||||||
travel_time = self.travel_time_between_waypoints(
|
travel_time = self.travel_time_between_waypoints(
|
||||||
self.target_area_waypoint, self.egress
|
self.target_area_waypoint, self.egress
|
||||||
)
|
)
|
||||||
@ -636,7 +657,7 @@ class StrikeFlightPlan(FormationFlightPlan):
|
|||||||
elif waypoint == self.egress:
|
elif waypoint == self.egress:
|
||||||
return self.egress_time
|
return self.egress_time
|
||||||
elif waypoint in self.targets:
|
elif waypoint in self.targets:
|
||||||
return self.package.time_over_target
|
return self.tot
|
||||||
return super().tot_for_waypoint(waypoint)
|
return super().tot_for_waypoint(waypoint)
|
||||||
|
|
||||||
|
|
||||||
@ -681,7 +702,7 @@ class SweepFlightPlan(LoiterFlightPlan):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sweep_end_time(self) -> timedelta:
|
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]:
|
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
|
||||||
if waypoint == self.sweep_start:
|
if waypoint == self.sweep_start:
|
||||||
@ -837,11 +858,7 @@ class FlightPlanBuilder:
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.package = package
|
self.package = package
|
||||||
self.is_player = is_player
|
self.is_player = is_player
|
||||||
if is_player:
|
self.doctrine: Doctrine = self.game.faction_for(self.is_player).doctrine
|
||||||
faction = self.game.player_faction
|
|
||||||
else:
|
|
||||||
faction = self.game.enemy_faction
|
|
||||||
self.doctrine: Doctrine = faction.doctrine
|
|
||||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
|
|
||||||
def populate_flight_plan(
|
def populate_flight_plan(
|
||||||
@ -853,12 +870,12 @@ class FlightPlanBuilder:
|
|||||||
"""Creates a default flight plan for the given mission."""
|
"""Creates a default flight plan for the given mission."""
|
||||||
if flight not in self.package.flights:
|
if flight not in self.package.flights:
|
||||||
raise RuntimeError("Flight must be a part of the package")
|
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
|
from game.navmesh import NavMeshError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if self.package.waypoints is None:
|
||||||
|
self.regenerate_package_waypoints()
|
||||||
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
|
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
|
||||||
except NavMeshError as ex:
|
except NavMeshError as ex:
|
||||||
color = "blue" if self.is_player else "red"
|
color = "blue" if self.is_player else "red"
|
||||||
@ -890,6 +907,8 @@ class FlightPlanBuilder:
|
|||||||
return self.generate_runway_attack(flight)
|
return self.generate_runway_attack(flight)
|
||||||
elif task == FlightType.SEAD:
|
elif task == FlightType.SEAD:
|
||||||
return self.generate_sead(flight, custom_targets)
|
return self.generate_sead(flight, custom_targets)
|
||||||
|
elif task == FlightType.SEAD_ESCORT:
|
||||||
|
return self.generate_escort(flight)
|
||||||
elif task == FlightType.STRIKE:
|
elif task == FlightType.STRIKE:
|
||||||
return self.generate_strike(flight)
|
return self.generate_strike(flight)
|
||||||
elif task == FlightType.SWEEP:
|
elif task == FlightType.SWEEP:
|
||||||
@ -1501,7 +1520,11 @@ class FlightPlanBuilder:
|
|||||||
targets.append(StrikeTarget(location.name, target))
|
targets.append(StrikeTarget(location.name, target))
|
||||||
|
|
||||||
return self.strike_flightplan(
|
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:
|
def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
@ -1679,6 +1702,7 @@ class FlightPlanBuilder:
|
|||||||
location: MissionTarget,
|
location: MissionTarget,
|
||||||
ingress_type: FlightWaypointType,
|
ingress_type: FlightWaypointType,
|
||||||
targets: Optional[List[StrikeTarget]] = None,
|
targets: Optional[List[StrikeTarget]] = None,
|
||||||
|
lead_time: timedelta = timedelta(),
|
||||||
) -> StrikeFlightPlan:
|
) -> StrikeFlightPlan:
|
||||||
assert self.package.waypoints is not None
|
assert self.package.waypoints is not None
|
||||||
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
|
builder = WaypointBuilder(flight, self.game, self.is_player, targets)
|
||||||
@ -1718,6 +1742,7 @@ class FlightPlanBuilder:
|
|||||||
land=builder.land(flight.arrival),
|
land=builder.land(flight.arrival),
|
||||||
divert=builder.divert(flight.divert),
|
divert=builder.divert(flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
|
lead_time=lead_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
|
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.
|
# "tasks": List (as a dict) of task IDs the payload is used by.
|
||||||
# }
|
# }
|
||||||
payloads = flight.unit_type.load_payloads()
|
payloads = flight.unit_type.load_payloads()
|
||||||
for payload in payloads["payloads"].values():
|
for payload in payloads.values():
|
||||||
name = payload["name"]
|
name = payload["name"]
|
||||||
pylons = payload["pylons"]
|
pylons = payload["pylons"]
|
||||||
yield Loadout(
|
yield Loadout(
|
||||||
@ -90,22 +90,33 @@ class Loadout:
|
|||||||
# etc.
|
# etc.
|
||||||
loadout_names = {t: [f"Liberation {t.value}"] for t in FlightType}
|
loadout_names = {t: [f"Liberation {t.value}"] for t in FlightType}
|
||||||
legacy_names = {
|
legacy_names = {
|
||||||
FlightType.TARCAP: ("CAP HEAVY", "CAP"),
|
FlightType.TARCAP: ("CAP HEAVY", "CAP", "Liberation BARCAP"),
|
||||||
FlightType.BARCAP: ("CAP HEAVY", "CAP"),
|
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Liberation TARCAP"),
|
||||||
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
||||||
FlightType.INTERCEPTION: ("CAP HEAVY", "CAP"),
|
|
||||||
FlightType.STRIKE: ("STRIKE",),
|
FlightType.STRIKE: ("STRIKE",),
|
||||||
FlightType.ANTISHIP: ("ANTISHIP",),
|
FlightType.ANTISHIP: ("ANTISHIP",),
|
||||||
FlightType.SEAD: ("SEAD",),
|
FlightType.SEAD: ("SEAD",),
|
||||||
FlightType.DEAD: ("SEAD",),
|
FlightType.BAI: ("BAI",),
|
||||||
FlightType.ESCORT: ("CAP HEAVY", "CAP"),
|
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
|
||||||
FlightType.BAI: ("BAI", "CAS MAVERICK F", "CAS"),
|
FlightType.OCA_AIRCRAFT: ("OCA",),
|
||||||
FlightType.SWEEP: ("CAP HEAVY", "CAP"),
|
|
||||||
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE", "STRIKE"),
|
|
||||||
FlightType.OCA_AIRCRAFT: ("OCA", "CAS MAVERICK F", "CAS"),
|
|
||||||
}
|
}
|
||||||
for flight_type, names in legacy_names.items():
|
for flight_type, names in legacy_names.items():
|
||||||
loadout_names[flight_type].extend(names)
|
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]
|
yield from loadout_names[flight.flight_type]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@ -36,23 +36,23 @@ class GroundSpeed:
|
|||||||
# DCS's max speed is in kph at 0 MSL.
|
# DCS's max speed is in kph at 0 MSL.
|
||||||
max_speed = kph(flight.unit_type.max_speed)
|
max_speed = kph(flight.unit_type.max_speed)
|
||||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
|
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.
|
# 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
|
# For subsonic aircraft, assume the aircraft can reasonably perform at
|
||||||
# 80% of its maximum, and that it can maintain the same mach at altitude
|
# 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
|
# 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
|
# might. be sufficient given the wiggle room. We can come up with
|
||||||
# another heuristic if needed.
|
# another heuristic if needed.
|
||||||
cruise_mach = max_speed.mach() * 0.8
|
cruise_mach = max_speed.mach() * 0.85
|
||||||
return mach(cruise_mach, altitude)
|
return mach(cruise_mach, altitude)
|
||||||
|
|
||||||
|
|
||||||
class TravelTime:
|
class TravelTime:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
|
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))
|
distance = meters(a.distance_to_point(b))
|
||||||
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
|
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)
|
return self.sweep_start(start, altitude), self.sweep_end(end, altitude)
|
||||||
|
|
||||||
def escort(
|
def escort(
|
||||||
self, ingress: Point, target: MissionTarget, egress: Point
|
self,
|
||||||
|
ingress: Point,
|
||||||
|
target: MissionTarget,
|
||||||
|
egress: Point,
|
||||||
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
|
||||||
"""Creates the waypoints needed to escort the package.
|
"""Creates the waypoints needed to escort the package.
|
||||||
|
|
||||||
|
|||||||
@ -80,6 +80,10 @@ class GroundPlanner:
|
|||||||
|
|
||||||
def plan_groundwar(self):
|
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"):
|
if hasattr(self.cp, "stance"):
|
||||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
||||||
else:
|
else:
|
||||||
@ -118,6 +122,12 @@ class GroundPlanner:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
available = self.cp.base.armor[key]
|
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:
|
while available > 0:
|
||||||
|
|
||||||
if role == CombatGroupRole.SHORAD:
|
if role == CombatGroupRole.SHORAD:
|
||||||
@ -144,6 +154,9 @@ class GroundPlanner:
|
|||||||
group.units.append(key)
|
group.units.append(key)
|
||||||
collection.append(group)
|
collection.append(group)
|
||||||
|
|
||||||
|
if remaining_available_frontline_units == 0:
|
||||||
|
break
|
||||||
|
|
||||||
print("------------------")
|
print("------------------")
|
||||||
print("Ground Planner : ")
|
print("Ground Planner : ")
|
||||||
print(self.cp.name)
|
print(self.cp.name)
|
||||||
|
|||||||
@ -36,7 +36,7 @@ from dcs.unittype import FlyingType
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from game.data.alic import AlicCodes
|
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 import ConflictTheater, TheaterGroundObject, LatLon
|
||||||
from game.theater.bullseye import Bullseye
|
from game.theater.bullseye import Bullseye
|
||||||
from game.utils import meters
|
from game.utils import meters
|
||||||
@ -298,9 +298,7 @@ class BriefingPage(KneeboardPage):
|
|||||||
headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
|
headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
|
||||||
)
|
)
|
||||||
|
|
||||||
writer.text(
|
writer.text(f"Bullseye: {self.bullseye.to_lat_lon(self.theater).format_dms()}")
|
||||||
f"Bullseye: {self.format_ll(self.bullseye.to_lat_lon(self.theater))}"
|
|
||||||
)
|
|
||||||
|
|
||||||
writer.table(
|
writer.table(
|
||||||
[
|
[
|
||||||
@ -507,7 +505,7 @@ class SeadTaskPage(KneeboardPage):
|
|||||||
ll = self.theater.point_to_ll(unit.position)
|
ll = self.theater.point_to_ll(unit.position)
|
||||||
unit_type = unit_type_from_name(unit.type)
|
unit_type = unit_type_from_name(unit.type)
|
||||||
name = unit.name if unit_type is None else unit_type.name
|
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):
|
class StrikeTaskPage(KneeboardPage):
|
||||||
@ -546,7 +544,11 @@ class StrikeTaskPage(KneeboardPage):
|
|||||||
|
|
||||||
def target_info_row(self, target: NumberedWaypoint) -> List[str]:
|
def target_info_row(self, target: NumberedWaypoint) -> List[str]:
|
||||||
ll = self.theater.point_to_ll(target.waypoint.position)
|
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):
|
class KneeboardGenerator(MissionInfoGenerator):
|
||||||
|
|||||||
@ -39,7 +39,7 @@ ALPHA_MILITARY = [
|
|||||||
"Zero",
|
"Zero",
|
||||||
]
|
]
|
||||||
|
|
||||||
ANIMALS = [
|
ANIMALS: tuple[str, ...] = (
|
||||||
"SHARK",
|
"SHARK",
|
||||||
"TORTOISE",
|
"TORTOISE",
|
||||||
"BAT",
|
"BAT",
|
||||||
@ -243,7 +243,7 @@ ANIMALS = [
|
|||||||
"CANARY",
|
"CANARY",
|
||||||
"WOODCHUCK",
|
"WOODCHUCK",
|
||||||
"ANACONDA",
|
"ANACONDA",
|
||||||
]
|
)
|
||||||
|
|
||||||
|
|
||||||
class NameGenerator:
|
class NameGenerator:
|
||||||
@ -253,7 +253,7 @@ class NameGenerator:
|
|||||||
convoy_number = 0
|
convoy_number = 0
|
||||||
cargo_ship_number = 0
|
cargo_ship_number = 0
|
||||||
|
|
||||||
ANIMALS = ANIMALS
|
animals: list[str] = list(ANIMALS)
|
||||||
existing_alphas: List[str] = []
|
existing_alphas: List[str] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -262,7 +262,7 @@ class NameGenerator:
|
|||||||
cls.infantry_number = 0
|
cls.infantry_number = 0
|
||||||
cls.convoy_number = 0
|
cls.convoy_number = 0
|
||||||
cls.cargo_ship_number = 0
|
cls.cargo_ship_number = 0
|
||||||
cls.ANIMALS = ANIMALS
|
cls.animals = list(ANIMALS)
|
||||||
cls.existing_alphas = []
|
cls.existing_alphas = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -345,30 +345,25 @@ class NameGenerator:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def random_objective_name(cls):
|
def random_objective_name(cls):
|
||||||
if len(cls.ANIMALS) == 0:
|
if cls.animals:
|
||||||
for i in range(10):
|
animal = random.choice(cls.animals)
|
||||||
new_name_generated = True
|
cls.animals.remove(animal)
|
||||||
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)
|
|
||||||
return 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
|
namegen = NameGenerator
|
||||||
|
|||||||
@ -96,32 +96,15 @@ class TriggersGenerator:
|
|||||||
"""
|
"""
|
||||||
for coalition_name, coalition in self.mission.coalition.items():
|
for coalition_name, coalition in self.mission.coalition.items():
|
||||||
if coalition_name == player_coalition:
|
if coalition_name == player_coalition:
|
||||||
skill_level = (
|
skill_level = Skill(self.game.settings.player_skill)
|
||||||
self.game.settings.player_skill,
|
|
||||||
self.game.settings.player_skill,
|
|
||||||
)
|
|
||||||
elif coalition_name == enemy_coalition:
|
elif coalition_name == enemy_coalition:
|
||||||
skill_level = (
|
skill_level = Skill(self.game.settings.enemy_vehicle_skill)
|
||||||
self.game.settings.enemy_skill,
|
|
||||||
self.game.settings.enemy_vehicle_skill,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for country in coalition.countries.values():
|
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:
|
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):
|
def _gen_markers(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
3
mypy.ini
3
mypy.ini
@ -5,6 +5,9 @@ namespace_packages = True
|
|||||||
follow_imports=silent
|
follow_imports=silent
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-faker.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
[mypy-PIL.*]
|
[mypy-PIL.*]
|
||||||
ignore_missing_imports = True
|
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.planes import PlaneType
|
||||||
from dcs.weapons_data import Weapons
|
from dcs.weapons_data import Weapons
|
||||||
|
|
||||||
|
from pydcs_extensions.weapon_injector import inject_weapons
|
||||||
|
|
||||||
|
|
||||||
class WeaponsA4EC:
|
class WeaponsA4EC:
|
||||||
AN_M57__2__TER_ = {
|
AN_M57__2__TER_ = {
|
||||||
@ -432,6 +434,9 @@ class WeaponsA4EC:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inject_weapons(WeaponsA4EC)
|
||||||
|
|
||||||
|
|
||||||
class A_4E_C(PlaneType):
|
class A_4E_C(PlaneType):
|
||||||
id = "A-4E-C"
|
id = "A-4E-C"
|
||||||
flyable = True
|
flyable = True
|
||||||
|
|||||||
@ -4,12 +4,17 @@ from dcs import task
|
|||||||
from dcs.planes import PlaneType
|
from dcs.planes import PlaneType
|
||||||
from dcs.weapons_data import Weapons
|
from dcs.weapons_data import Weapons
|
||||||
|
|
||||||
|
from pydcs_extensions.weapon_injector import inject_weapons
|
||||||
|
|
||||||
|
|
||||||
class F22AWeapons:
|
class F22AWeapons:
|
||||||
AIM_9XX = {"clsid": "{AIM-9XX}", "name": "AIM-9XX", "weight": 85}
|
AIM_9XX = {"clsid": "{AIM-9XX}", "name": "AIM-9XX", "weight": 85}
|
||||||
AIM_120D = {"clsid": "{AIM-120D}", "name": "AIM-120D", "weight": 152}
|
AIM_120D = {"clsid": "{AIM-120D}", "name": "AIM-120D", "weight": 152}
|
||||||
|
|
||||||
|
|
||||||
|
inject_weapons(F22AWeapons)
|
||||||
|
|
||||||
|
|
||||||
class F_22A(PlaneType):
|
class F_22A(PlaneType):
|
||||||
id = "F-22A"
|
id = "F-22A"
|
||||||
flyable = True
|
flyable = True
|
||||||
|
|||||||
@ -4,6 +4,8 @@ from dcs import task
|
|||||||
from dcs.planes import PlaneType
|
from dcs.planes import PlaneType
|
||||||
from dcs.weapons_data import Weapons
|
from dcs.weapons_data import Weapons
|
||||||
|
|
||||||
|
from pydcs_extensions.weapon_injector import inject_weapons
|
||||||
|
|
||||||
|
|
||||||
class HerculesWeapons:
|
class HerculesWeapons:
|
||||||
GAU_23A_Chain_Gun__30mm_ = {
|
GAU_23A_Chain_Gun__30mm_ = {
|
||||||
@ -679,6 +681,9 @@ class HerculesWeapons:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inject_weapons(HerculesWeapons)
|
||||||
|
|
||||||
|
|
||||||
class Hercules(PlaneType):
|
class Hercules(PlaneType):
|
||||||
id = "Hercules"
|
id = "Hercules"
|
||||||
flyable = True
|
flyable = True
|
||||||
|
|||||||
@ -4,6 +4,8 @@ from dcs import task
|
|||||||
from dcs.planes import PlaneType
|
from dcs.planes import PlaneType
|
||||||
from dcs.weapons_data import Weapons
|
from dcs.weapons_data import Weapons
|
||||||
|
|
||||||
|
from pydcs_extensions.weapon_injector import inject_weapons
|
||||||
|
|
||||||
|
|
||||||
class MB_339PAN_Weapons:
|
class MB_339PAN_Weapons:
|
||||||
ARF8M3_TP = {"clsid": "{ARF8M3_TP}", "name": "ARF8M3 TP", "weight": None}
|
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):
|
class MB_339PAN(PlaneType):
|
||||||
id = "MB-339PAN"
|
id = "MB-339PAN"
|
||||||
flyable = True
|
flyable = True
|
||||||
|
|||||||
@ -4,6 +4,8 @@ from dcs import task
|
|||||||
from dcs.planes import PlaneType
|
from dcs.planes import PlaneType
|
||||||
from dcs.weapons_data import Weapons
|
from dcs.weapons_data import Weapons
|
||||||
|
|
||||||
|
from pydcs_extensions.weapon_injector import inject_weapons
|
||||||
|
|
||||||
|
|
||||||
class Su57Weapons:
|
class Su57Weapons:
|
||||||
Kh_59MK2 = {"clsid": "{KH_59MK2}", "name": "Kh-59MK2", "weight": None}
|
Kh_59MK2 = {"clsid": "{KH_59MK2}", "name": "Kh-59MK2", "weight": None}
|
||||||
@ -18,6 +20,9 @@ class Su57Weapons:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
inject_weapons(Su57Weapons)
|
||||||
|
|
||||||
|
|
||||||
class Su_57(PlaneType):
|
class Su_57(PlaneType):
|
||||||
id = "Su-57"
|
id = "Su-57"
|
||||||
flyable = True
|
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 pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import dcs
|
|
||||||
from PySide2 import QtWidgets
|
from PySide2 import QtWidgets
|
||||||
from PySide2.QtGui import QPixmap
|
from PySide2.QtGui import QPixmap
|
||||||
from PySide2.QtWidgets import QApplication, QSplashScreen
|
from PySide2.QtWidgets import QApplication, QSplashScreen
|
||||||
|
from dcs.payloads import PayloadDirectories
|
||||||
from dcs.weapons_data import weapon_ids
|
from dcs.weapons_data import weapon_ids
|
||||||
|
|
||||||
from game import Game, VERSION, persistency
|
from game import Game, VERSION, persistency
|
||||||
@ -35,6 +35,27 @@ from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
|
|||||||
QLiberationFirstStartWindow,
|
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:
|
def run_ui(game: Optional[Game], new_map: bool) -> None:
|
||||||
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
|
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())
|
logging.info("Loading stylesheet: %s", liberation_theme.get_theme_css_file())
|
||||||
app.setStyleSheet(stylesheet.read())
|
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()
|
first_start = liberation_install.init()
|
||||||
if first_start:
|
if first_start:
|
||||||
window = QLiberationFirstStartWindow()
|
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
|
# Splash screen setup
|
||||||
pixmap = QPixmap("./resources/ui/splash_screen.png")
|
pixmap = QPixmap("./resources/ui/splash_screen.png")
|
||||||
splash = QSplashScreen(pixmap)
|
splash = QSplashScreen(pixmap)
|
||||||
@ -189,6 +196,15 @@ def create_game(
|
|||||||
"Cannot generate campaign without configuring DCS Liberation. Start the UI "
|
"Cannot generate campaign without configuring DCS Liberation. Start the UI "
|
||||||
"for the first run configuration."
|
"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)
|
campaign = Campaign.from_json(campaign_path)
|
||||||
generator = GameGenerator(
|
generator = GameGenerator(
|
||||||
blue,
|
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 import db
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
|
from game.squadrons import Squadron, Pilot
|
||||||
from game.theater.missiontarget import MissionTarget
|
from game.theater.missiontarget import MissionTarget
|
||||||
from game.transfers import TransferOrder
|
from game.transfers import TransferOrder
|
||||||
from gen.ato import AirTaskingOrder, Package
|
from gen.ato import AirTaskingOrder, Package
|
||||||
@ -166,6 +167,7 @@ class PackageModel(QAbstractListModel):
|
|||||||
if flight.cargo is not None:
|
if flight.cargo is not None:
|
||||||
flight.cargo.transport = None
|
flight.cargo.transport = None
|
||||||
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
self.game_model.game.aircraft_inventory.return_from_flight(flight)
|
||||||
|
flight.clear_roster()
|
||||||
self.package.remove_flight(flight)
|
self.package.remove_flight(flight)
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
self.update_tot()
|
self.update_tot()
|
||||||
@ -258,6 +260,7 @@ class AtoModel(QAbstractListModel):
|
|||||||
self.ato.remove_package(package)
|
self.ato.remove_package(package)
|
||||||
for flight in package.flights:
|
for flight in package.flights:
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
self.game.aircraft_inventory.return_from_flight(flight)
|
||||||
|
flight.clear_roster()
|
||||||
if flight.cargo is not None:
|
if flight.cargo is not None:
|
||||||
flight.cargo.transport = None
|
flight.cargo.transport = None
|
||||||
self.endRemoveRows()
|
self.endRemoveRows()
|
||||||
@ -366,6 +369,105 @@ class TransferModel(QAbstractListModel):
|
|||||||
return self.game_model.game.transfers.transfer_at_index(index.row())
|
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:
|
class GameModel:
|
||||||
"""A model for the Game object.
|
"""A model for the Game object.
|
||||||
|
|
||||||
@ -376,6 +478,7 @@ class GameModel:
|
|||||||
def __init__(self, game: Optional[Game]) -> None:
|
def __init__(self, game: Optional[Game]) -> None:
|
||||||
self.game: Optional[Game] = game
|
self.game: Optional[Game] = game
|
||||||
self.transfer_model = TransferModel(self)
|
self.transfer_model = TransferModel(self)
|
||||||
|
self.blue_air_wing_model = AirWingModel(self, player=True)
|
||||||
if self.game is None:
|
if self.game is None:
|
||||||
self.ato_model = AtoModel(self, AirTaskingOrder())
|
self.ato_model = AtoModel(self, AirTaskingOrder())
|
||||||
self.red_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.QFactionsInfos import QFactionsInfos
|
||||||
from qt_ui.widgets.QIntelBox import QIntelBox
|
from qt_ui.widgets.QIntelBox import QIntelBox
|
||||||
from qt_ui.widgets.clientslots import MaxPlayerCount
|
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.GameUpdateSignal import GameUpdateSignal
|
||||||
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
|
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
|
||||||
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
|
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
|
||||||
@ -63,6 +64,11 @@ class QTopPanel(QFrame):
|
|||||||
|
|
||||||
self.factionsInfos = QFactionsInfos(self.game)
|
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 = QPushButton("Transfers")
|
||||||
self.transfers.setDisabled(True)
|
self.transfers.setDisabled(True)
|
||||||
self.transfers.setProperty("style", "btn-primary")
|
self.transfers.setProperty("style", "btn-primary")
|
||||||
@ -84,6 +90,7 @@ class QTopPanel(QFrame):
|
|||||||
|
|
||||||
self.buttonBox = QGroupBox("Misc")
|
self.buttonBox = QGroupBox("Misc")
|
||||||
self.buttonBoxLayout = QHBoxLayout()
|
self.buttonBoxLayout = QHBoxLayout()
|
||||||
|
self.buttonBoxLayout.addWidget(self.air_wing)
|
||||||
self.buttonBoxLayout.addWidget(self.transfers)
|
self.buttonBoxLayout.addWidget(self.transfers)
|
||||||
self.buttonBoxLayout.addWidget(self.settings)
|
self.buttonBoxLayout.addWidget(self.settings)
|
||||||
self.buttonBoxLayout.addWidget(self.statistics)
|
self.buttonBoxLayout.addWidget(self.statistics)
|
||||||
@ -114,6 +121,7 @@ class QTopPanel(QFrame):
|
|||||||
if game is None:
|
if game is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.air_wing.setEnabled(True)
|
||||||
self.transfers.setEnabled(True)
|
self.transfers.setEnabled(True)
|
||||||
self.settings.setEnabled(True)
|
self.settings.setEnabled(True)
|
||||||
self.statistics.setEnabled(True)
|
self.statistics.setEnabled(True)
|
||||||
@ -130,6 +138,10 @@ class QTopPanel(QFrame):
|
|||||||
else:
|
else:
|
||||||
self.proceedButton.setEnabled(True)
|
self.proceedButton.setEnabled(True)
|
||||||
|
|
||||||
|
def open_air_wing(self):
|
||||||
|
self.dialog = AirWingDialog(self.game_model, self.window())
|
||||||
|
self.dialog.show()
|
||||||
|
|
||||||
def open_transfers(self):
|
def open_transfers(self):
|
||||||
self.dialog = PendingTransfersDialog(self.game_model)
|
self.dialog = PendingTransfersDialog(self.game_model)
|
||||||
self.dialog.show()
|
self.dialog.show()
|
||||||
@ -176,17 +188,18 @@ class QTopPanel(QFrame):
|
|||||||
def confirm_no_client_launch(self) -> bool:
|
def confirm_no_client_launch(self) -> bool:
|
||||||
result = QMessageBox.question(
|
result = QMessageBox.question(
|
||||||
self,
|
self,
|
||||||
"Continue without client slots?",
|
"Continue without player pilots?",
|
||||||
(
|
(
|
||||||
"No client slots have been created for players. Continuing will "
|
"No player pilots have been assigned to flights. Continuing will allow "
|
||||||
"allow the AI to perform the mission, but players will be unable "
|
"the AI to perform the mission, but players will be unable to "
|
||||||
"to participate.<br />"
|
"participate.<br />"
|
||||||
"<br />"
|
"<br />"
|
||||||
"To add client slots for players, select a package from the "
|
"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 "
|
"Packages panel on the left of the main window, and then a flight from "
|
||||||
"from the Flights panel below the Packages panel. The edit button "
|
"the Flights panel below the Packages panel. The edit button below the "
|
||||||
"below the Flights panel will allow you to edit the number of "
|
"Flights panel will allow you to assign specific pilots to the flight. "
|
||||||
"client slots in the flight. Each client slot allows one player.<br />"
|
"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 'Yes' to continue with an AI only mission"
|
||||||
"<br />Click 'No' if you'd like to make more changes."
|
"<br />Click 'No' if you'd like to make more changes."
|
||||||
),
|
),
|
||||||
@ -232,11 +245,44 @@ class QTopPanel(QFrame):
|
|||||||
return True
|
return True
|
||||||
return False
|
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):
|
def launch_mission(self):
|
||||||
"""Finishes planning and waits for mission completion."""
|
"""Finishes planning and waits for mission completion."""
|
||||||
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
if not self.ato_has_clients() and not self.confirm_no_client_launch():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.check_no_missing_pilots():
|
||||||
|
return
|
||||||
|
|
||||||
negative_starts = self.negative_start_packages()
|
negative_starts = self.negative_start_packages()
|
||||||
if negative_starts:
|
if negative_starts:
|
||||||
if not self.confirm_negative_start_time(negative_starts):
|
if not self.confirm_negative_start_time(negative_starts):
|
||||||
|
|||||||
@ -10,10 +10,6 @@ from PySide2.QtCore import (
|
|||||||
)
|
)
|
||||||
from PySide2.QtGui import (
|
from PySide2.QtGui import (
|
||||||
QContextMenuEvent,
|
QContextMenuEvent,
|
||||||
QFont,
|
|
||||||
QFontMetrics,
|
|
||||||
QIcon,
|
|
||||||
QPainter,
|
|
||||||
)
|
)
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
@ -25,9 +21,6 @@ from PySide2.QtWidgets import (
|
|||||||
QMenu,
|
QMenu,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
QStyle,
|
|
||||||
QStyleOptionViewItem,
|
|
||||||
QStyledItemDelegate,
|
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,111 +28,42 @@ from gen.ato import Package
|
|||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from gen.flights.traveltime import TotEstimator
|
from gen.flights.traveltime import TotEstimator
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
from ..delegate_helpers import painter_context
|
from ..delegates import TwoColumnRowDelegate
|
||||||
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
from ..models import AtoModel, GameModel, NullListModel, PackageModel
|
||||||
|
|
||||||
|
|
||||||
class FlightDelegate(QStyledItemDelegate):
|
class FlightDelegate(TwoColumnRowDelegate):
|
||||||
FONT_SIZE = 10
|
|
||||||
HMARGIN = 4
|
|
||||||
VMARGIN = 4
|
|
||||||
|
|
||||||
def __init__(self, package: Package) -> None:
|
def __init__(self, package: Package) -> None:
|
||||||
super().__init__()
|
super().__init__(rows=2, columns=2, font_size=10)
|
||||||
self.package = package
|
self.package = package
|
||||||
|
|
||||||
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
|
||||||
font = QFont(option.font)
|
|
||||||
font.setPointSize(self.FONT_SIZE)
|
|
||||||
return font
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def flight(index: QModelIndex) -> Flight:
|
def flight(index: QModelIndex) -> Flight:
|
||||||
return index.data(PackageModel.FlightRole)
|
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)
|
flight = self.flight(index)
|
||||||
estimator = TotEstimator(self.package)
|
if (row, column) == (0, 0):
|
||||||
delay = estimator.mission_start_time(flight)
|
estimator = TotEstimator(self.package)
|
||||||
return f"{flight} in {delay}"
|
delay = estimator.mission_start_time(flight)
|
||||||
|
return f"{flight} in {delay}"
|
||||||
def second_row_text(self, index: QModelIndex) -> str:
|
elif (row, column) == (0, 1):
|
||||||
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))
|
|
||||||
|
|
||||||
clients = self.num_clients(index)
|
clients = self.num_clients(index)
|
||||||
if clients:
|
return f"Player Slots: {clients}" if clients else ""
|
||||||
painter.drawText(rect, Qt.AlignRight, f"Player Slots: {clients}")
|
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:
|
def num_clients(self, index: QModelIndex) -> int:
|
||||||
flight = self.flight(index)
|
flight = self.flight(index)
|
||||||
return flight.client_count
|
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):
|
class QFlightList(QListView):
|
||||||
"""List view for displaying the flights of a package."""
|
"""List view for displaying the flights of a package."""
|
||||||
@ -310,62 +234,35 @@ class QFlightPanel(QGroupBox):
|
|||||||
self.flight_list.delete_flight(index)
|
self.flight_list.delete_flight(index)
|
||||||
|
|
||||||
|
|
||||||
class PackageDelegate(QStyledItemDelegate):
|
class PackageDelegate(TwoColumnRowDelegate):
|
||||||
FONT_SIZE = 12
|
def __init__(self) -> None:
|
||||||
HMARGIN = 4
|
super().__init__(rows=2, columns=2)
|
||||||
VMARGIN = 4
|
|
||||||
|
|
||||||
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
|
||||||
font = QFont(option.font)
|
|
||||||
font.setPointSize(self.FONT_SIZE)
|
|
||||||
return font
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def package(index: QModelIndex) -> Package:
|
def package(index: QModelIndex) -> Package:
|
||||||
return index.data(AtoModel.PackageRole)
|
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)
|
package = self.package(index)
|
||||||
return f"{package.package_description} {package.target.name}"
|
if (row, column) == (0, 0):
|
||||||
|
return f"{package.package_description} {package.target.name}"
|
||||||
def right_text(self, index: QModelIndex) -> str:
|
elif (row, column) == (0, 1):
|
||||||
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))
|
|
||||||
|
|
||||||
clients = self.num_clients(index)
|
clients = self.num_clients(index)
|
||||||
if clients:
|
return f"Player Slots: {clients}" if clients else ""
|
||||||
painter.drawText(rect, Qt.AlignRight, f"Player Slots: {clients}")
|
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:
|
def num_clients(self, index: QModelIndex) -> int:
|
||||||
package = self.package(index)
|
package = self.package(index)
|
||||||
return sum(f.client_count for f in package.flights)
|
return sum(f.client_count for f in package.flights)
|
||||||
|
|
||||||
def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
|
def missing_pilots(self, index: QModelIndex) -> int:
|
||||||
metrics = QFontMetrics(self.get_font(option))
|
package = self.package(index)
|
||||||
left = metrics.size(0, self.left_text(index))
|
return sum(f.missing_pilots for f in package.flights)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class QPackageList(QListView):
|
class QPackageList(QListView):
|
||||||
@ -376,7 +273,7 @@ class QPackageList(QListView):
|
|||||||
self.ato_model = model
|
self.ato_model = model
|
||||||
self.setModel(model)
|
self.setModel(model)
|
||||||
self.setItemDelegate(PackageDelegate())
|
self.setItemDelegate(PackageDelegate())
|
||||||
self.setIconSize(QSize(91, 24))
|
self.setIconSize(QSize(0, 0))
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||||
self.model().rowsInserted.connect(self.on_new_packages)
|
self.model().rowsInserted.connect(self.on_new_packages)
|
||||||
self.doubleClicked.connect(self.on_double_click)
|
self.doubleClicked.connect(self.on_double_click)
|
||||||
|
|||||||
@ -2,15 +2,12 @@
|
|||||||
from typing import Iterable, Type
|
from typing import Iterable, Type
|
||||||
|
|
||||||
from PySide2.QtWidgets import QComboBox
|
from PySide2.QtWidgets import QComboBox
|
||||||
|
|
||||||
from dcs.unittype import FlyingType
|
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
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
import gen.flights.ai_flight_planner_db
|
|
||||||
|
|
||||||
from game import Game, db
|
|
||||||
|
|
||||||
|
|
||||||
class QAircraftTypeSelector(QComboBox):
|
class QAircraftTypeSelector(QComboBox):
|
||||||
"""Combo box for selecting among the given aircraft types."""
|
"""Combo box for selecting among the given aircraft types."""
|
||||||
@ -19,77 +16,24 @@ class QAircraftTypeSelector(QComboBox):
|
|||||||
self,
|
self,
|
||||||
aircraft_types: Iterable[Type[FlyingType]],
|
aircraft_types: Iterable[Type[FlyingType]],
|
||||||
country: str,
|
country: str,
|
||||||
mission_type: str,
|
mission_type: FlightType,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.model().sort(0)
|
self.model().sort(0)
|
||||||
self.setSizeAdjustPolicy(self.AdjustToContents)
|
self.setSizeAdjustPolicy(self.AdjustToContents)
|
||||||
self.country = country
|
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()
|
current_aircraft = self.currentData()
|
||||||
self.clear()
|
self.clear()
|
||||||
for aircraft in aircraft_types:
|
for aircraft in aircraft_types:
|
||||||
if mission_type in [
|
if aircraft in aircraft_for_task(mission_type):
|
||||||
FlightType.BARCAP,
|
self.addItem(
|
||||||
FlightType.ESCORT,
|
f"{db.unit_get_expanded_info(self.country, aircraft, 'name')}",
|
||||||
FlightType.INTERCEPTION,
|
userData=aircraft,
|
||||||
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,
|
|
||||||
)
|
|
||||||
current_aircraft_index = self.findData(current_aircraft)
|
current_aircraft_index = self.findData(current_aircraft)
|
||||||
if current_aircraft_index != -1:
|
if current_aircraft_index != -1:
|
||||||
self.setCurrentIndex(current_aircraft_index)
|
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 import Point
|
||||||
from dcs.mapping import point_from_heading
|
from dcs.mapping import point_from_heading
|
||||||
from dcs.planes import F_16C_50
|
|
||||||
from dcs.unitgroup import Group
|
from dcs.unitgroup import Group
|
||||||
from shapely.geometry import (
|
from shapely.geometry import (
|
||||||
LineString,
|
LineString,
|
||||||
@ -167,11 +166,6 @@ class LeafletMap(QWebEngineView, LiberationMap):
|
|||||||
)
|
)
|
||||||
self.setPage(self.page)
|
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:
|
def set_game(self, game: Optional[Game]) -> None:
|
||||||
if game is None:
|
if game is None:
|
||||||
self.map_model.clear()
|
self.map_model.clear()
|
||||||
@ -567,10 +561,17 @@ class QLiberationMap(QGraphicsView, LiberationMap):
|
|||||||
origin = self.game.theater.enemy_points()[0]
|
origin = self.game.theater.enemy_points()[0]
|
||||||
|
|
||||||
package = Package(target)
|
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(
|
flight = Flight(
|
||||||
package,
|
package,
|
||||||
self.game.player_country if player else self.game.enemy_country,
|
self.game.country_for(player),
|
||||||
F_16C_50,
|
squadron,
|
||||||
2,
|
2,
|
||||||
task,
|
task,
|
||||||
start_type="Warm",
|
start_type="Warm",
|
||||||
|
|||||||
@ -2,16 +2,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
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 PySide2.QtCore import Property, QObject, Signal, Slot
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from dcs.vehicles import vehicle_map
|
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 import Game, db
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
|
from game.navmesh import NavMesh
|
||||||
from game.profiling import logged_duration
|
from game.profiling import logged_duration
|
||||||
from game.theater import (
|
from game.theater import (
|
||||||
ConflictTheater,
|
ConflictTheater,
|
||||||
@ -20,6 +21,7 @@ from game.theater import (
|
|||||||
FrontLine,
|
FrontLine,
|
||||||
LatLon,
|
LatLon,
|
||||||
)
|
)
|
||||||
|
from game.threatzones import ThreatZones
|
||||||
from game.transfers import MultiGroupTransport, TransportMap
|
from game.transfers import MultiGroupTransport, TransportMap
|
||||||
from game.utils import meters, nautical_miles
|
from game.utils import meters, nautical_miles
|
||||||
from gen.ato import AirTaskingOrder
|
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.basemenu.QBaseMenu2 import QBaseMenu2
|
||||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||||
|
|
||||||
LeafletLatLon = List[float]
|
LeafletLatLon = list[float]
|
||||||
|
LeafletPoly = list[LeafletLatLon]
|
||||||
|
|
||||||
# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL**
|
# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL**
|
||||||
#
|
#
|
||||||
@ -51,9 +54,9 @@ LeafletLatLon = List[float]
|
|||||||
|
|
||||||
def shapely_poly_to_leaflet_points(
|
def shapely_poly_to_leaflet_points(
|
||||||
poly: Polygon, theater: ConflictTheater
|
poly: Polygon, theater: ConflictTheater
|
||||||
) -> Optional[List[LeafletLatLon]]:
|
) -> LeafletPoly:
|
||||||
if poly.is_empty:
|
if poly.is_empty:
|
||||||
return None
|
return []
|
||||||
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in poly.exterior.coords]
|
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()
|
positionChanged = Signal()
|
||||||
mobileChanged = Signal()
|
mobileChanged = Signal()
|
||||||
destinationChanged = Signal(list)
|
destinationChanged = Signal(list)
|
||||||
|
categoryChanged = Signal()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -84,6 +88,10 @@ class ControlPointJs(QObject):
|
|||||||
def blue(self) -> bool:
|
def blue(self) -> bool:
|
||||||
return self.control_point.captured
|
return self.control_point.captured
|
||||||
|
|
||||||
|
@Property(str, notify=categoryChanged)
|
||||||
|
def category(self) -> str:
|
||||||
|
return self.control_point.category
|
||||||
|
|
||||||
@Property(list, notify=positionChanged)
|
@Property(list, notify=positionChanged)
|
||||||
def position(self) -> LeafletLatLon:
|
def position(self) -> LeafletLatLon:
|
||||||
ll = self.theater.point_to_ll(self.control_point.position)
|
ll = self.theater.point_to_ll(self.control_point.position)
|
||||||
@ -373,7 +381,9 @@ class WaypointJs(QObject):
|
|||||||
altitudeReferenceChanged = Signal()
|
altitudeReferenceChanged = Signal()
|
||||||
nameChanged = Signal()
|
nameChanged = Signal()
|
||||||
timingChanged = Signal()
|
timingChanged = Signal()
|
||||||
|
isTargetPointChanged = Signal()
|
||||||
isTakeoffChanged = Signal()
|
isTakeoffChanged = Signal()
|
||||||
|
isLandingChanged = Signal()
|
||||||
isDivertChanged = Signal()
|
isDivertChanged = Signal()
|
||||||
isBullseyeChanged = Signal()
|
isBullseyeChanged = Signal()
|
||||||
|
|
||||||
@ -432,10 +442,18 @@ class WaypointJs(QObject):
|
|||||||
return ""
|
return ""
|
||||||
return f"{prefix} T+{timedelta(seconds=int(time.total_seconds()))}"
|
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)
|
@Property(bool, notify=isTakeoffChanged)
|
||||||
def isTakeoff(self) -> bool:
|
def isTakeoff(self) -> bool:
|
||||||
return self.waypoint.waypoint_type is FlightWaypointType.TAKEOFF
|
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)
|
@Property(bool, notify=isDivertChanged)
|
||||||
def isDivert(self) -> bool:
|
def isDivert(self) -> bool:
|
||||||
return self.waypoint.waypoint_type is FlightWaypointType.DIVERT
|
return self.waypoint.waypoint_type is FlightWaypointType.DIVERT
|
||||||
@ -512,7 +530,7 @@ class FlightJs(QObject):
|
|||||||
return self._selected
|
return self._selected
|
||||||
|
|
||||||
@Property(list, notify=commitBoundaryChanged)
|
@Property(list, notify=commitBoundaryChanged)
|
||||||
def commitBoundary(self) -> Optional[List[LeafletLatLon]]:
|
def commitBoundary(self) -> LeafletPoly:
|
||||||
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
|
||||||
return []
|
return []
|
||||||
start = self.flight.flight_plan.patrol_start
|
start = self.flight.flight_plan.patrol_start
|
||||||
@ -528,6 +546,118 @@ class FlightJs(QObject):
|
|||||||
return shapely_poly_to_leaflet_points(bubble, self.theater)
|
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):
|
class MapModel(QObject):
|
||||||
cleared = Signal()
|
cleared = Signal()
|
||||||
|
|
||||||
@ -537,6 +667,8 @@ class MapModel(QObject):
|
|||||||
supplyRoutesChanged = Signal()
|
supplyRoutesChanged = Signal()
|
||||||
flightsChanged = Signal()
|
flightsChanged = Signal()
|
||||||
frontLinesChanged = Signal()
|
frontLinesChanged = Signal()
|
||||||
|
threatZonesChanged = Signal()
|
||||||
|
navmeshesChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, game_model: GameModel) -> None:
|
def __init__(self, game_model: GameModel) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -547,6 +679,10 @@ class MapModel(QObject):
|
|||||||
self._supply_routes = []
|
self._supply_routes = []
|
||||||
self._flights = []
|
self._flights = []
|
||||||
self._front_lines = []
|
self._front_lines = []
|
||||||
|
self._threat_zones = ThreatZoneContainerJs(
|
||||||
|
ThreatZonesJs.empty(), ThreatZonesJs.empty()
|
||||||
|
)
|
||||||
|
self._navmeshes = NavMeshJs([], [])
|
||||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||||
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
||||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
||||||
@ -564,6 +700,10 @@ class MapModel(QObject):
|
|||||||
self._ground_objects = []
|
self._ground_objects = []
|
||||||
self._flights = []
|
self._flights = []
|
||||||
self._front_lines = []
|
self._front_lines = []
|
||||||
|
self._threat_zones = ThreatZoneContainerJs(
|
||||||
|
ThreatZonesJs.empty(), ThreatZonesJs.empty()
|
||||||
|
)
|
||||||
|
self._navmeshes = NavMeshJs([], [])
|
||||||
self.cleared.emit()
|
self.cleared.emit()
|
||||||
|
|
||||||
def set_package_selection(self, index: int) -> None:
|
def set_package_selection(self, index: int) -> None:
|
||||||
@ -607,6 +747,8 @@ class MapModel(QObject):
|
|||||||
self.reset_routes()
|
self.reset_routes()
|
||||||
self.reset_atos()
|
self.reset_atos()
|
||||||
self.reset_front_lines()
|
self.reset_front_lines()
|
||||||
|
self.reset_threat_zones()
|
||||||
|
self.reset_navmeshes()
|
||||||
|
|
||||||
def on_game_load(self, game: Optional[Game]) -> None:
|
def on_game_load(self, game: Optional[Game]) -> None:
|
||||||
if game is not None:
|
if game is not None:
|
||||||
@ -730,6 +872,29 @@ class MapModel(QObject):
|
|||||||
def frontLines(self) -> List[FrontLineJs]:
|
def frontLines(self) -> List[FrontLineJs]:
|
||||||
return self._front_lines
|
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
|
@property
|
||||||
def game(self) -> Game:
|
def game(self) -> Game:
|
||||||
if self.game_model.game is None:
|
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 (
|
from PySide2.QtCore import (
|
||||||
QItemSelection,
|
QItemSelection,
|
||||||
QItemSelectionModel,
|
QItemSelectionModel,
|
||||||
QModelIndex,
|
QModelIndex,
|
||||||
QSize,
|
|
||||||
Qt,
|
Qt,
|
||||||
)
|
)
|
||||||
from PySide2.QtGui import QContextMenuEvent, QFont, QFontMetrics, QIcon, QPainter
|
from PySide2.QtGui import QContextMenuEvent
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QAction,
|
QAction,
|
||||||
@ -16,102 +13,29 @@ from PySide2.QtWidgets import (
|
|||||||
QListView,
|
QListView,
|
||||||
QMenu,
|
QMenu,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QStyle,
|
|
||||||
QStyleOptionViewItem,
|
|
||||||
QStyledItemDelegate,
|
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from game.transfers import TransferOrder
|
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
|
from qt_ui.models import GameModel, TransferModel
|
||||||
|
|
||||||
|
|
||||||
class TransferDelegate(QStyledItemDelegate):
|
class TransferDelegate(TwoColumnRowDelegate):
|
||||||
FONT_SIZE = 10
|
|
||||||
HMARGIN = 4
|
|
||||||
VMARGIN = 4
|
|
||||||
|
|
||||||
def __init__(self, transfer_model: TransferModel) -> None:
|
def __init__(self, transfer_model: TransferModel) -> None:
|
||||||
super().__init__()
|
super().__init__(rows=2, columns=1, font_size=12)
|
||||||
self.transfer_model = transfer_model
|
self.transfer_model = transfer_model
|
||||||
|
|
||||||
def get_font(self, option: QStyleOptionViewItem) -> QFont:
|
|
||||||
font = QFont(option.font)
|
|
||||||
font.setPointSize(self.FONT_SIZE)
|
|
||||||
return font
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def transfer(index: QModelIndex) -> TransferOrder:
|
def transfer(index: QModelIndex) -> TransferOrder:
|
||||||
return index.data(TransferModel.TransferRole)
|
return index.data(TransferModel.TransferRole)
|
||||||
|
|
||||||
def first_row_text(self, index: QModelIndex) -> str:
|
def text_for(self, index: QModelIndex, row: int, column: int) -> str:
|
||||||
return self.transfer_model.data(index, Qt.DisplayRole)
|
if row == 0:
|
||||||
|
return self.transfer_model.data(index, Qt.DisplayRole)
|
||||||
def second_row_text(self, index: QModelIndex) -> str:
|
elif row == 1:
|
||||||
return self.transfer(index).description
|
return self.transfer(index).description
|
||||||
|
return ""
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PendingTransfersList(QListView):
|
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 collections import defaultdict
|
||||||
from typing import Callable, Dict, Type
|
from typing import Callable, Dict, Type
|
||||||
|
|
||||||
from PySide2.QtCore import Qt
|
from PySide2.QtCore import Qt, Signal
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
@ -153,6 +153,8 @@ class TransferControls(QGroupBox):
|
|||||||
|
|
||||||
|
|
||||||
class ScrollingUnitTransferGrid(QFrame):
|
class ScrollingUnitTransferGrid(QFrame):
|
||||||
|
transfer_quantity_changed = Signal()
|
||||||
|
|
||||||
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
def __init__(self, cp: ControlPoint, game_model: GameModel) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cp = cp
|
self.cp = cp
|
||||||
@ -229,6 +231,7 @@ class ScrollingUnitTransferGrid(QFrame):
|
|||||||
origin_inventory -= 1
|
origin_inventory -= 1
|
||||||
controls.set_quantity(self.transfers[unit_type])
|
controls.set_quantity(self.transfers[unit_type])
|
||||||
origin_inventory_label.setText(str(origin_inventory))
|
origin_inventory_label.setText(str(origin_inventory))
|
||||||
|
self.transfer_quantity_changed.emit()
|
||||||
|
|
||||||
def decrease(controls: TransferControls):
|
def decrease(controls: TransferControls):
|
||||||
nonlocal origin_inventory
|
nonlocal origin_inventory
|
||||||
@ -240,6 +243,7 @@ class ScrollingUnitTransferGrid(QFrame):
|
|||||||
origin_inventory += 1
|
origin_inventory += 1
|
||||||
controls.set_quantity(self.transfers[unit_type])
|
controls.set_quantity(self.transfers[unit_type])
|
||||||
origin_inventory_label.setText(str(origin_inventory))
|
origin_inventory_label.setText(str(origin_inventory))
|
||||||
|
self.transfer_quantity_changed.emit()
|
||||||
|
|
||||||
transfer_controls = TransferControls("->", increase, "<-", decrease)
|
transfer_controls = TransferControls("->", increase, "<-", decrease)
|
||||||
|
|
||||||
@ -276,11 +280,15 @@ class NewUnitTransferDialog(QDialog):
|
|||||||
layout.addLayout(self.dest_panel)
|
layout.addLayout(self.dest_panel)
|
||||||
|
|
||||||
self.transfer_panel = ScrollingUnitTransferGrid(origin, game_model)
|
self.transfer_panel = ScrollingUnitTransferGrid(origin, game_model)
|
||||||
|
self.transfer_panel.transfer_quantity_changed.connect(
|
||||||
|
self.on_transfer_quantity_changed
|
||||||
|
)
|
||||||
layout.addWidget(self.transfer_panel)
|
layout.addWidget(self.transfer_panel)
|
||||||
|
|
||||||
self.submit_button = QPushButton("Create Transfer Order", parent=self)
|
self.submit_button = QPushButton("Create Transfer Order", parent=self)
|
||||||
self.submit_button.clicked.connect(self.on_submit)
|
self.submit_button.clicked.connect(self.on_submit)
|
||||||
self.submit_button.setProperty("style", "start-button")
|
self.submit_button.setProperty("style", "start-button")
|
||||||
|
self.submit_button.setDisabled(True)
|
||||||
layout.addWidget(self.submit_button)
|
layout.addWidget(self.submit_button)
|
||||||
|
|
||||||
def on_submit(self) -> None:
|
def on_submit(self) -> None:
|
||||||
@ -303,3 +311,7 @@ class NewUnitTransferDialog(QDialog):
|
|||||||
)
|
)
|
||||||
self.game_model.transfer_model.new_transfer(transfer)
|
self.game_model.transfer_model.new_transfer(transfer)
|
||||||
self.close()
|
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)
|
bottom_row.addWidget(transfer_button)
|
||||||
transfer_button.clicked.connect(self.open_transfer_dialog)
|
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(
|
self.budget_display = QLabel(
|
||||||
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget)
|
QRecruitBehaviour.BUDGET_FORMAT.format(self.game_model.game.budget)
|
||||||
)
|
)
|
||||||
@ -104,6 +110,26 @@ class QBaseMenu2(QDialog):
|
|||||||
GameUpdateSignal.get_instance().budgetupdated.connect(self.update_budget)
|
GameUpdateSignal.get_instance().budgetupdated.connect(self.update_budget)
|
||||||
self.setLayout(main_layout)
|
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
|
@property
|
||||||
def has_transfer_destinations(self) -> bool:
|
def has_transfer_destinations(self) -> bool:
|
||||||
return self.game_model.game.transit_network_for(
|
return self.game_model.game.transit_network_for(
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
|
QCheckBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
QFrame,
|
QFrame,
|
||||||
QGridLayout,
|
QGridLayout,
|
||||||
@ -42,9 +43,9 @@ class ScrollingFrame(QFrame):
|
|||||||
|
|
||||||
|
|
||||||
class EconomyIntelTab(ScrollingFrame):
|
class EconomyIntelTab(ScrollingFrame):
|
||||||
def __init__(self, game: Game) -> None:
|
def __init__(self, game: Game, player: bool) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.addLayout(FinancesLayout(game, player=False))
|
self.addLayout(FinancesLayout(game, player=player))
|
||||||
|
|
||||||
|
|
||||||
class IntelTableLayout(QGridLayout):
|
class IntelTableLayout(QGridLayout):
|
||||||
@ -93,9 +94,9 @@ class AircraftIntelLayout(IntelTableLayout):
|
|||||||
|
|
||||||
|
|
||||||
class AircraftIntelTab(ScrollingFrame):
|
class AircraftIntelTab(ScrollingFrame):
|
||||||
def __init__(self, game: Game) -> None:
|
def __init__(self, game: Game, player: bool) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.addLayout(AircraftIntelLayout(game, player=False))
|
self.addLayout(AircraftIntelLayout(game, player=player))
|
||||||
|
|
||||||
|
|
||||||
class ArmyIntelLayout(IntelTableLayout):
|
class ArmyIntelLayout(IntelTableLayout):
|
||||||
@ -120,18 +121,18 @@ class ArmyIntelLayout(IntelTableLayout):
|
|||||||
|
|
||||||
|
|
||||||
class ArmyIntelTab(ScrollingFrame):
|
class ArmyIntelTab(ScrollingFrame):
|
||||||
def __init__(self, game: Game) -> None:
|
def __init__(self, game: Game, player: bool) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.addLayout(ArmyIntelLayout(game, player=False))
|
self.addLayout(ArmyIntelLayout(game, player=player))
|
||||||
|
|
||||||
|
|
||||||
class IntelTabs(QTabWidget):
|
class IntelTabs(QTabWidget):
|
||||||
def __init__(self, game: Game):
|
def __init__(self, game: Game, player: bool):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.addTab(EconomyIntelTab(game), "Economy")
|
self.addTab(EconomyIntelTab(game, player), "Economy")
|
||||||
self.addTab(AircraftIntelTab(game), "Air forces")
|
self.addTab(AircraftIntelTab(game, player), "Air forces")
|
||||||
self.addTab(ArmyIntelTab(game), "Ground forces")
|
self.addTab(ArmyIntelTab(game, player), "Ground forces")
|
||||||
|
|
||||||
|
|
||||||
class IntelWindow(QDialog):
|
class IntelWindow(QDialog):
|
||||||
@ -139,12 +140,42 @@ class IntelWindow(QDialog):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
|
self.player = True
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
self.setWindowTitle("Intelligence")
|
self.setWindowTitle("Intelligence")
|
||||||
self.setWindowIcon(ICONS["Statistics"])
|
self.setWindowIcon(ICONS["Statistics"])
|
||||||
self.setMinimumSize(600, 500)
|
self.setMinimumSize(600, 500)
|
||||||
|
self.selected_intel_tab = 0
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
self.setLayout(layout)
|
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
|
self, game_model: GameModel, model: AtoModel, target: MissionTarget, parent=None
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
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
|
self.ato_model = model
|
||||||
|
|
||||||
@ -237,6 +239,7 @@ class QNewPackageDialog(QPackageDialog):
|
|||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
for flight in self.package_model.package.flights:
|
for flight in self.package_model.package.flights:
|
||||||
self.game.aircraft_inventory.return_from_flight(flight)
|
self.game.aircraft_inventory.return_from_flight(flight)
|
||||||
|
flight.clear_roster()
|
||||||
|
|
||||||
|
|
||||||
class QEditPackageDialog(QPackageDialog):
|
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.QtCore import Qt, Signal
|
||||||
from PySide2.QtWidgets import (
|
from PySide2.QtWidgets import (
|
||||||
@ -10,9 +11,10 @@ from PySide2.QtWidgets import (
|
|||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
)
|
)
|
||||||
from dcs.planes import PlaneType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from game.squadrons import Squadron
|
||||||
from game.theater import ControlPoint, OffMapSpawn
|
from game.theater import ControlPoint, OffMapSpawn
|
||||||
from gen.ato import Package
|
from gen.ato import Package
|
||||||
from gen.flights.flight import Flight
|
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.QArrivalAirfieldSelector import QArrivalAirfieldSelector
|
||||||
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
||||||
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
|
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
|
||||||
|
from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector
|
||||||
|
|
||||||
|
|
||||||
class QFlightCreator(QDialog):
|
class QFlightCreator(QDialog):
|
||||||
@ -55,6 +58,14 @@ class QFlightCreator(QDialog):
|
|||||||
self.aircraft_selector.currentIndexChanged.connect(self.on_aircraft_changed)
|
self.aircraft_selector.currentIndexChanged.connect(self.on_aircraft_changed)
|
||||||
layout.addLayout(QLabeledWidget("Aircraft:", self.aircraft_selector))
|
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.departure = QOriginAirfieldSelector(
|
||||||
self.game.aircraft_inventory,
|
self.game.aircraft_inventory,
|
||||||
[cp for cp in game.theater.controlpoints if cp.captured],
|
[cp for cp in game.theater.controlpoints if cp.captured],
|
||||||
@ -132,13 +143,16 @@ class QFlightCreator(QDialog):
|
|||||||
self.custom_name_text = text
|
self.custom_name_text = text
|
||||||
|
|
||||||
def verify_form(self) -> Optional[str]:
|
def verify_form(self) -> Optional[str]:
|
||||||
aircraft: PlaneType = self.aircraft_selector.currentData()
|
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
|
||||||
origin: ControlPoint = self.departure.currentData()
|
squadron: Optional[Squadron] = self.squadron_selector.currentData()
|
||||||
arrival: ControlPoint = self.arrival.currentData()
|
origin: Optional[ControlPoint] = self.departure.currentData()
|
||||||
divert: ControlPoint = self.divert.currentData()
|
arrival: Optional[ControlPoint] = self.arrival.currentData()
|
||||||
|
divert: Optional[ControlPoint] = self.divert.currentData()
|
||||||
size: int = self.flight_size_spinner.value()
|
size: int = self.flight_size_spinner.value()
|
||||||
if aircraft is None:
|
if aircraft is None:
|
||||||
return "You must select an aircraft type."
|
return "You must select an aircraft type."
|
||||||
|
if squadron is None:
|
||||||
|
return "You must select a squadron."
|
||||||
if not origin.captured:
|
if not origin.captured:
|
||||||
return f"{origin.name} is not owned by your coalition."
|
return f"{origin.name} is not owned by your coalition."
|
||||||
if arrival is not None and not arrival.captured:
|
if arrival is not None and not arrival.captured:
|
||||||
@ -163,7 +177,7 @@ class QFlightCreator(QDialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
task = self.task_selector.currentData()
|
task = self.task_selector.currentData()
|
||||||
aircraft = self.aircraft_selector.currentData()
|
squadron = self.squadron_selector.currentData()
|
||||||
origin = self.departure.currentData()
|
origin = self.departure.currentData()
|
||||||
arrival = self.arrival.currentData()
|
arrival = self.arrival.currentData()
|
||||||
divert = self.divert.currentData()
|
divert = self.divert.currentData()
|
||||||
@ -175,7 +189,7 @@ class QFlightCreator(QDialog):
|
|||||||
flight = Flight(
|
flight = Flight(
|
||||||
self.package,
|
self.package,
|
||||||
self.country,
|
self.country,
|
||||||
aircraft,
|
squadron,
|
||||||
size,
|
size,
|
||||||
task,
|
task,
|
||||||
self.start_type.currentText(),
|
self.start_type.currentText(),
|
||||||
@ -184,7 +198,14 @@ class QFlightCreator(QDialog):
|
|||||||
divert,
|
divert,
|
||||||
custom_name=self.custom_name_text,
|
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
|
# noinspection PyUnresolvedReferences
|
||||||
self.created.emit(flight)
|
self.created.emit(flight)
|
||||||
@ -192,6 +213,9 @@ class QFlightCreator(QDialog):
|
|||||||
|
|
||||||
def on_aircraft_changed(self, index: int) -> None:
|
def on_aircraft_changed(self, index: int) -> None:
|
||||||
new_aircraft = self.aircraft_selector.itemData(index)
|
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.departure.change_aircraft(new_aircraft)
|
||||||
self.arrival.change_aircraft(new_aircraft)
|
self.arrival.change_aircraft(new_aircraft)
|
||||||
self.divert.change_aircraft(new_aircraft)
|
self.divert.change_aircraft(new_aircraft)
|
||||||
@ -211,10 +235,13 @@ class QFlightCreator(QDialog):
|
|||||||
self.restore_start_type = None
|
self.restore_start_type = None
|
||||||
|
|
||||||
def on_task_changed(self) -> None:
|
def on_task_changed(self) -> None:
|
||||||
self.aircraft_selector.updateItems(
|
self.aircraft_selector.update_items(
|
||||||
self.task_selector.currentData(),
|
self.task_selector.currentData(),
|
||||||
self.game.aircraft_inventory.available_types_for_player,
|
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:
|
def update_max_size(self, available: int) -> None:
|
||||||
self.flight_size_spinner.setMaximum(min(available, 4))
|
self.flight_size_spinner.setMaximum(min(available, 4))
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class QFlightPlanner(QTabWidget):
|
|||||||
)
|
)
|
||||||
self.payload_tab = QFlightPayloadTab(flight, game)
|
self.payload_tab = QFlightPayloadTab(flight, game)
|
||||||
self.waypoint_tab = QFlightWaypointTab(game, package_model.package, flight)
|
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.general_settings_tab, "General Flight settings")
|
||||||
self.addTab(self.payload_tab, "Payload")
|
self.addTab(self.payload_tab, "Payload")
|
||||||
self.addTab(self.waypoint_tab, "Waypoints")
|
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)
|
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:
|
def on_new_loadout(self, index: int) -> None:
|
||||||
self.flight.loadout = self.loadout_selector.itemData(index)
|
self.flight.loadout = self.loadout_selector.itemData(index)
|
||||||
self.payload_editor.reset_pylons()
|
self.payload_editor.reset_pylons()
|
||||||
@ -49,5 +52,5 @@ class QFlightPayloadTab(QFrame):
|
|||||||
if use_custom:
|
if use_custom:
|
||||||
self.flight.loadout = self.flight.loadout.derive_custom("Custom")
|
self.flight.loadout = self.flight.loadout.derive_custom("Custom")
|
||||||
else:
|
else:
|
||||||
self.flight.loadout = Loadout.default_for(self.flight)
|
self.flight.loadout = self.loadout_selector.currentData()
|
||||||
self.payload_editor.reset_pylons()
|
self.payload_editor.reset_pylons()
|
||||||
|
|||||||
@ -1,17 +1,174 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
from PySide2.QtCore import Signal
|
from PySide2.QtCore import Signal, QModelIndex
|
||||||
from PySide2.QtWidgets import QLabel, QGroupBox, QSpinBox, QGridLayout
|
from PySide2.QtWidgets import (
|
||||||
|
QLabel,
|
||||||
|
QGroupBox,
|
||||||
|
QSpinBox,
|
||||||
|
QGridLayout,
|
||||||
|
QComboBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QCheckBox,
|
||||||
|
QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
|
from game.squadrons import Pilot
|
||||||
from gen.flights.flight import Flight
|
from gen.flights.flight import Flight
|
||||||
from qt_ui.models import PackageModel
|
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):
|
class QFlightSlotEditor(QGroupBox):
|
||||||
|
|
||||||
changed = Signal()
|
|
||||||
|
|
||||||
def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
|
def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
|
||||||
super().__init__("Slots")
|
super().__init__("Slots")
|
||||||
self.package_model = package_model
|
self.package_model = package_model
|
||||||
@ -32,52 +189,35 @@ class QFlightSlotEditor(QGroupBox):
|
|||||||
self.aircraft_count_spinner.setValue(flight.count)
|
self.aircraft_count_spinner.setValue(flight.count)
|
||||||
self.aircraft_count_spinner.valueChanged.connect(self._changed_aircraft_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, 0, 0)
|
||||||
layout.addWidget(self.aircraft_count_spinner, 0, 1)
|
layout.addWidget(self.aircraft_count_spinner, 0, 1)
|
||||||
|
|
||||||
layout.addWidget(self.client_count, 1, 0)
|
layout.addWidget(QLabel("Squadron:"), 1, 0)
|
||||||
layout.addWidget(self.client_count_spinner, 1, 1)
|
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)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def _changed_aircraft_count(self):
|
def _changed_aircraft_count(self):
|
||||||
self.game.aircraft_inventory.return_from_flight(self.flight)
|
self.game.aircraft_inventory.return_from_flight(self.flight)
|
||||||
old_count = self.flight.count
|
new_count = int(self.aircraft_count_spinner.value())
|
||||||
self.flight.count = int(self.aircraft_count_spinner.value())
|
|
||||||
try:
|
try:
|
||||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# The UI should have prevented this, but if we ran out of aircraft
|
# The UI should have prevented this, but if we ran out of aircraft
|
||||||
# then roll back the inventory change.
|
# 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)
|
available = self.inventory.available(self.flight.unit_type)
|
||||||
logging.error(
|
logging.error(
|
||||||
f"Could not add {difference} additional aircraft to "
|
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"
|
f"{available} {self.flight.unit_type} remaining"
|
||||||
)
|
)
|
||||||
self.flight.count = old_count
|
|
||||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
||||||
self.changed.emit()
|
return
|
||||||
|
|
||||||
def _changed_client_count(self):
|
self.flight.resize(new_count)
|
||||||
self.flight.client_count = int(self.client_count_spinner.value())
|
self.roster_editor.resize(new_count)
|
||||||
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)
|
|
||||||
|
|||||||
@ -21,9 +21,6 @@ class QFlightWaypointList(QTableView):
|
|||||||
|
|
||||||
header = self.horizontalHeader()
|
header = self.horizontalHeader()
|
||||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
||||||
|
|
||||||
if len(self.flight.points) > 0:
|
|
||||||
self.selectedPoint = self.flight.points[0]
|
|
||||||
self.update_list()
|
self.update_list()
|
||||||
|
|
||||||
self.selectionModel().setCurrentIndex(
|
self.selectionModel().setCurrentIndex(
|
||||||
@ -31,6 +28,9 @@ class QFlightWaypointList(QTableView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_list(self):
|
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.clear()
|
||||||
|
|
||||||
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
|
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
|
||||||
@ -39,7 +39,7 @@ class QFlightWaypointList(QTableView):
|
|||||||
for row, waypoint in enumerate(waypoints):
|
for row, waypoint in enumerate(waypoints):
|
||||||
self.add_waypoint_row(row, self.flight, waypoint)
|
self.add_waypoint_row(row, self.flight, waypoint)
|
||||||
self.selectionModel().setCurrentIndex(
|
self.selectionModel().setCurrentIndex(
|
||||||
self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select
|
self.model.index(current_index, 0), QItemSelectionModel.Select
|
||||||
)
|
)
|
||||||
self.resizeColumnsToContents()
|
self.resizeColumnsToContents()
|
||||||
total_column_width = self.verticalHeader().width() + self.lineWidth()
|
total_column_width = self.verticalHeader().width() + self.lineWidth()
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from gen.flights.flightplan import (
|
|||||||
PlanningError,
|
PlanningError,
|
||||||
StrikeFlightPlan,
|
StrikeFlightPlan,
|
||||||
)
|
)
|
||||||
|
from gen.flights.loadouts import Loadout
|
||||||
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import (
|
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import (
|
||||||
QFlightWaypointList,
|
QFlightWaypointList,
|
||||||
)
|
)
|
||||||
@ -29,6 +30,8 @@ from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow i
|
|||||||
|
|
||||||
|
|
||||||
class QFlightWaypointTab(QFrame):
|
class QFlightWaypointTab(QFrame):
|
||||||
|
loadout_changed = Signal()
|
||||||
|
|
||||||
def __init__(self, game: Game, package: Package, flight: Flight):
|
def __init__(self, game: Game, package: Package, flight: Flight):
|
||||||
super(QFlightWaypointTab, self).__init__()
|
super(QFlightWaypointTab, self).__init__()
|
||||||
self.game = game
|
self.game = game
|
||||||
@ -161,6 +164,9 @@ class QFlightWaypointTab(QFrame):
|
|||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
self, "Could not recreate flight", str(ex), QMessageBox.Ok
|
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.flight_waypoint_list.update_list()
|
||||||
self.on_change()
|
self.on_change()
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ jinja_env = Environment(
|
|||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_BUDGET = 2000
|
DEFAULT_BUDGET = 2000
|
||||||
DEFAULT_MISSION_LENGTH: timedelta = timedelta(minutes=90)
|
DEFAULT_MISSION_LENGTH: timedelta = timedelta(minutes=60)
|
||||||
|
|
||||||
|
|
||||||
class NewGameWizard(QtWidgets.QWizard):
|
class NewGameWizard(QtWidgets.QWizard):
|
||||||
|
|||||||
@ -23,7 +23,7 @@ from dcs.forcedoptions import ForcedOptions
|
|||||||
import qt_ui.uiconstants as CONST
|
import qt_ui.uiconstants as CONST
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
from game.infos.information import Information
|
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.QLabeledWidget import QLabeledWidget
|
||||||
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs
|
from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
@ -75,6 +75,110 @@ class CheatSettingsBox(QGroupBox):
|
|||||||
return self.base_capture_cheat_checkbox.isChecked()
|
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."
|
START_TYPE_TOOLTIP = "Selects the start type used for AI aircraft."
|
||||||
|
|
||||||
|
|
||||||
@ -92,7 +196,7 @@ class StartTypeComboBox(QComboBox):
|
|||||||
|
|
||||||
class QSettingsWindow(QDialog):
|
class QSettingsWindow(QDialog):
|
||||||
def __init__(self, game: Game):
|
def __init__(self, game: Game):
|
||||||
super(QSettingsWindow, self).__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.game = game
|
self.game = game
|
||||||
self.pluginsPage = None
|
self.pluginsPage = None
|
||||||
@ -285,6 +389,23 @@ class QSettingsWindow(QDialog):
|
|||||||
self.ext_views.setChecked(self.game.settings.external_views_allowed)
|
self.ext_views.setChecked(self.game.settings.external_views_allowed)
|
||||||
self.ext_views.toggled.connect(self.applySettings)
|
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(QLabel("Player coalition skill"), 0, 0)
|
||||||
self.aiDifficultyLayout.addWidget(
|
self.aiDifficultyLayout.addWidget(
|
||||||
self.playerCoalitionSkill, 0, 1, Qt.AlignRight
|
self.playerCoalitionSkill, 0, 1, Qt.AlignRight
|
||||||
@ -295,6 +416,10 @@ class QSettingsWindow(QDialog):
|
|||||||
self.aiDifficultyLayout.addWidget(self.enemyAASkill, 2, 1, Qt.AlignRight)
|
self.aiDifficultyLayout.addWidget(self.enemyAASkill, 2, 1, Qt.AlignRight)
|
||||||
self.aiDifficultyLayout.addLayout(self.player_income, 3, 0)
|
self.aiDifficultyLayout.addLayout(self.player_income, 3, 0)
|
||||||
self.aiDifficultyLayout.addLayout(self.enemy_income, 4, 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.aiDifficultySettings.setLayout(self.aiDifficultyLayout)
|
||||||
self.difficultyLayout.addWidget(self.aiDifficultySettings)
|
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_label, 1, 0)
|
||||||
general_layout.addWidget(old_awac, 1, 1, Qt.AlignRight)
|
general_layout.addWidget(old_awac, 1, 1, Qt.AlignRight)
|
||||||
|
|
||||||
automation = QGroupBox("HQ Automation")
|
campaign_layout.addWidget(HqAutomationSettingsBox(self.game))
|
||||||
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)
|
|
||||||
|
|
||||||
def initGeneratorLayout(self):
|
def initGeneratorLayout(self):
|
||||||
self.generatorPage = QWidget()
|
self.generatorPage = QWidget()
|
||||||
|
|||||||
@ -5,6 +5,7 @@ certifi==2020.12.5
|
|||||||
cfgv==3.2.0
|
cfgv==3.2.0
|
||||||
click==7.1.2
|
click==7.1.2
|
||||||
distlib==0.3.1
|
distlib==0.3.1
|
||||||
|
Faker==8.2.1
|
||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
future==0.18.2
|
future==0.18.2
|
||||||
identify==1.5.13
|
identify==1.5.13
|
||||||
@ -23,6 +24,7 @@ pyinstaller-hooks-contrib==2021.1
|
|||||||
pyparsing==2.4.7
|
pyparsing==2.4.7
|
||||||
pyproj==3.0.1
|
pyproj==3.0.1
|
||||||
PySide2==5.15.2
|
PySide2==5.15.2
|
||||||
|
python-dateutil==2.8.1
|
||||||
pywin32-ctypes==0.2.0
|
pywin32-ctypes==0.2.0
|
||||||
PyYAML==5.4.1
|
PyYAML==5.4.1
|
||||||
regex==2020.11.13
|
regex==2020.11.13
|
||||||
@ -30,6 +32,7 @@ Shapely==1.7.1
|
|||||||
shiboken2==5.15.2
|
shiboken2==5.15.2
|
||||||
six==1.15.0
|
six==1.15.0
|
||||||
tabulate==0.8.7
|
tabulate==0.8.7
|
||||||
|
text-unidecode==1.3
|
||||||
toml==0.10.2
|
toml==0.10.2
|
||||||
typed-ast==1.4.2
|
typed-ast==1.4.2
|
||||||
typing-extensions==3.7.4.3
|
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>",
|
"description": "<p>You have managed to establish a foothold at Khasab. Continue pushing south.</p>",
|
||||||
"miz": "battle_of_abu_dhabi.miz",
|
"miz": "battle_of_abu_dhabi.miz",
|
||||||
"performance": 2,
|
"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>",
|
"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",
|
"miz": "golan_heights_lite.miz",
|
||||||
"performance": 1,
|
"performance": 1,
|
||||||
"version": 3
|
"version": "5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@ -5,7 +5,7 @@
|
|||||||
"recommended_player_faction": "USA 2005",
|
"recommended_player_faction": "USA 2005",
|
||||||
"recommended_enemy_faction": "Insurgents (Hard)",
|
"recommended_enemy_faction": "Insurgents (Hard)",
|
||||||
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
|
"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",
|
"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] = {
|
[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",
|
["name"] = "STRIKE",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
@ -241,7 +145,7 @@ local unitPayloads = {
|
|||||||
[1] = 10,
|
[1] = 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[6] = {
|
[4] = {
|
||||||
["name"] = "BAI",
|
["name"] = "BAI",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
@ -289,7 +193,7 @@ local unitPayloads = {
|
|||||||
[1] = 10,
|
[1] = 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[7] = {
|
[5] = {
|
||||||
["name"] = "ANTISHIP",
|
["name"] = "ANTISHIP",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
@ -337,6 +241,103 @@ local unitPayloads = {
|
|||||||
[1] = 10,
|
[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",
|
["unitType"] = "F-14A-135-GR",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -155,57 +155,6 @@ local unitPayloads = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
[4] = {
|
[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",
|
["name"] = "ANTISHIP",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
@ -256,6 +205,103 @@ local unitPayloads = {
|
|||||||
[4] = 19,
|
[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",
|
["unitType"] = "F-14B",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,71 +2,88 @@ local unitPayloads = {
|
|||||||
["name"] = "F-15E",
|
["name"] = "F-15E",
|
||||||
["payloads"] = {
|
["payloads"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
["name"] = "CAS",
|
["displayName"] = "Liberation CAS",
|
||||||
|
["name"] = "Liberation CAS",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
|
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
|
||||||
["num"] = 1,
|
["num"] = 18,
|
||||||
},
|
},
|
||||||
[2] = {
|
[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}",
|
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
|
||||||
["num"] = 3,
|
["num"] = 3,
|
||||||
},
|
},
|
||||||
[3] = {
|
|
||||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
|
||||||
["num"] = 4,
|
|
||||||
},
|
|
||||||
[4] = {
|
|
||||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
|
||||||
["num"] = 6,
|
|
||||||
},
|
|
||||||
[5] = {
|
[5] = {
|
||||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
|
||||||
["num"] = 7,
|
["num"] = 17,
|
||||||
},
|
},
|
||||||
[6] = {
|
[6] = {
|
||||||
["CLSID"] = "{GBU-38}",
|
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||||
["num"] = 9,
|
["num"] = 19,
|
||||||
},
|
},
|
||||||
[7] = {
|
[7] = {
|
||||||
["CLSID"] = "{E1F29B21-F291-4589-9FD8-3272EEC69506}",
|
["CLSID"] = "{E1F29B21-F291-4589-9FD8-3272EEC69506}",
|
||||||
["num"] = 10,
|
["num"] = 10,
|
||||||
},
|
},
|
||||||
[8] = {
|
[8] = {
|
||||||
["CLSID"] = "{GBU-38}",
|
["CLSID"] = "{CBU_105}",
|
||||||
["num"] = 11,
|
["num"] = 11,
|
||||||
},
|
},
|
||||||
[9] = {
|
[9] = {
|
||||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
["CLSID"] = "{CBU_105}",
|
||||||
["num"] = 13,
|
["num"] = 9,
|
||||||
},
|
},
|
||||||
[10] = {
|
[10] = {
|
||||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
["CLSID"] = "{CBU_105}",
|
||||||
["num"] = 14,
|
["num"] = 8,
|
||||||
},
|
},
|
||||||
[11] = {
|
[11] = {
|
||||||
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
|
["CLSID"] = "{CBU_105}",
|
||||||
["num"] = 16,
|
["num"] = 7,
|
||||||
},
|
},
|
||||||
[12] = {
|
[12] = {
|
||||||
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
|
["CLSID"] = "{CBU_105}",
|
||||||
["num"] = 19,
|
["num"] = 12,
|
||||||
},
|
},
|
||||||
[13] = {
|
[13] = {
|
||||||
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
|
["CLSID"] = "{CBU_105}",
|
||||||
["num"] = 17,
|
["num"] = 13,
|
||||||
},
|
},
|
||||||
[14] = {
|
[14] = {
|
||||||
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
|
["CLSID"] = "{Mk82AIR}",
|
||||||
["num"] = 18,
|
["num"] = 6,
|
||||||
},
|
},
|
||||||
[15] = {
|
[15] = {
|
||||||
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
|
["CLSID"] = "{Mk82AIR}",
|
||||||
["num"] = 2,
|
["num"] = 5,
|
||||||
|
},
|
||||||
|
[16] = {
|
||||||
|
["CLSID"] = "{Mk82AIR}",
|
||||||
|
["num"] = 4,
|
||||||
|
},
|
||||||
|
[17] = {
|
||||||
|
["CLSID"] = "{Mk82AIR}",
|
||||||
|
["num"] = 14,
|
||||||
|
},
|
||||||
|
[18] = {
|
||||||
|
["CLSID"] = "{Mk82AIR}",
|
||||||
|
["num"] = 15,
|
||||||
|
},
|
||||||
|
[19] = {
|
||||||
|
["CLSID"] = "{Mk82AIR}",
|
||||||
|
["num"] = 16,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
["tasks"] = {
|
["tasks"] = {
|
||||||
[1] = 32,
|
[1] = 31,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[2] = {
|
[2] = {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ local unitPayloads = {
|
|||||||
["name"] = "CAS",
|
["name"] = "CAS",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
["CLSID"] = "<CLEAN>",
|
||||||
["num"] = 5,
|
["num"] = 5,
|
||||||
},
|
},
|
||||||
[2] = {
|
[2] = {
|
||||||
@ -80,7 +80,7 @@ local unitPayloads = {
|
|||||||
["num"] = 11,
|
["num"] = 11,
|
||||||
},
|
},
|
||||||
[8] = {
|
[8] = {
|
||||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
["CLSID"] = "<CLEAN>",
|
||||||
["num"] = 5,
|
["num"] = 5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -91,7 +91,7 @@ local unitPayloads = {
|
|||||||
["name"] = "CAP",
|
["name"] = "CAP",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
["CLSID"] = "<CLEAN>",
|
||||||
["num"] = 5,
|
["num"] = 5,
|
||||||
},
|
},
|
||||||
[2] = {
|
[2] = {
|
||||||
@ -166,7 +166,7 @@ local unitPayloads = {
|
|||||||
["num"] = 1,
|
["num"] = 1,
|
||||||
},
|
},
|
||||||
[9] = {
|
[9] = {
|
||||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
["CLSID"] = "<CLEAN>",
|
||||||
["num"] = 5,
|
["num"] = 5,
|
||||||
},
|
},
|
||||||
[10] = {
|
[10] = {
|
||||||
@ -197,7 +197,7 @@ local unitPayloads = {
|
|||||||
["num"] = 3,
|
["num"] = 3,
|
||||||
},
|
},
|
||||||
[5] = {
|
[5] = {
|
||||||
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
|
["CLSID"] = "<CLEAN>",
|
||||||
["num"] = 5,
|
["num"] = 5,
|
||||||
},
|
},
|
||||||
[6] = {
|
[6] = {
|
||||||
@ -220,6 +220,55 @@ local unitPayloads = {
|
|||||||
["tasks"] = {
|
["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",
|
["unitType"] = "F-16C_50",
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,39 +2,39 @@ local unitPayloads = {
|
|||||||
["name"] = "FA-18C_hornet",
|
["name"] = "FA-18C_hornet",
|
||||||
["payloads"] = {
|
["payloads"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
["name"] = "CAS MAVERICK F",
|
["name"] = "Liberation BARCAP",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
["CLSID"] = "LAU_117_AGM_65F",
|
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||||
["num"] = 7,
|
["num"] = 9,
|
||||||
},
|
},
|
||||||
[2] = {
|
[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}",
|
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||||
["num"] = 1,
|
["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] = {
|
[8] = {
|
||||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||||
["num"] = 9,
|
["num"] = 4,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
["tasks"] = {
|
["tasks"] = {
|
||||||
@ -42,90 +42,50 @@ local unitPayloads = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
[2] = {
|
[2] = {
|
||||||
["name"] = "CAS MAVERICK E",
|
["name"] = "Liberation CAS",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
["CLSID"] = "{F16A4DE0-116C-4A71-97F0-2CF85B0313EC}",
|
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||||
["num"] = 7,
|
["num"] = 9,
|
||||||
},
|
},
|
||||||
[2] = {
|
[2] = {
|
||||||
["CLSID"] = "{F16A4DE0-116C-4A71-97F0-2CF85B0313EC}",
|
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||||
["num"] = 8,
|
["num"] = 1,
|
||||||
},
|
},
|
||||||
[3] = {
|
[3] = {
|
||||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
["CLSID"] = "LAU_117_AGM_65F",
|
||||||
["num"] = 6,
|
["num"] = 2,
|
||||||
},
|
},
|
||||||
[4] = {
|
[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}",
|
["CLSID"] = "{AN_ASQ_228}",
|
||||||
["num"] = 4,
|
["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] = {
|
[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}",
|
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||||
["num"] = 6,
|
["num"] = 6,
|
||||||
},
|
},
|
||||||
[4] = {
|
[9] = {
|
||||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
|
||||||
["num"] = 4,
|
|
||||||
},
|
|
||||||
[5] = {
|
|
||||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||||
["num"] = 5,
|
["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"] = {
|
["tasks"] = {
|
||||||
[1] = 11,
|
[1] = 31,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[4] = {
|
[3] = {
|
||||||
["name"] = "STRIKE",
|
["name"] = "STRIKE",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
@ -165,7 +125,7 @@ local unitPayloads = {
|
|||||||
[1] = 11,
|
[1] = 11,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[5] = {
|
[4] = {
|
||||||
["name"] = "ANTISHIP",
|
["name"] = "ANTISHIP",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
@ -205,16 +165,17 @@ local unitPayloads = {
|
|||||||
[1] = 11,
|
[1] = 11,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[6] = {
|
[5] = {
|
||||||
["name"] = "SEAD",
|
["displayName"] = "Liberation SEAD",
|
||||||
|
["name"] = "Liberation SEAD",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||||
["num"] = 7,
|
["num"] = 9,
|
||||||
},
|
},
|
||||||
[2] = {
|
[2] = {
|
||||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
||||||
["num"] = 8,
|
["num"] = 1,
|
||||||
},
|
},
|
||||||
[3] = {
|
[3] = {
|
||||||
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
|
||||||
@ -226,30 +187,26 @@ local unitPayloads = {
|
|||||||
},
|
},
|
||||||
[5] = {
|
[5] = {
|
||||||
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||||
["num"] = 5,
|
["num"] = 3,
|
||||||
},
|
},
|
||||||
[6] = {
|
[6] = {
|
||||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
["CLSID"] = "{FPU_8A_FUEL_TANK}",
|
||||||
["num"] = 3,
|
["num"] = 7,
|
||||||
},
|
},
|
||||||
[7] = {
|
[7] = {
|
||||||
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
||||||
["num"] = 2,
|
["num"] = 2,
|
||||||
},
|
},
|
||||||
[8] = {
|
[8] = {
|
||||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
|
||||||
["num"] = 1,
|
["num"] = 8,
|
||||||
},
|
|
||||||
[9] = {
|
|
||||||
["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}",
|
|
||||||
["num"] = 9,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
["tasks"] = {
|
["tasks"] = {
|
||||||
[1] = 11,
|
[1] = 29,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[7] = {
|
[6] = {
|
||||||
["name"] = "RUNWAY_ATTACK",
|
["name"] = "RUNWAY_ATTACK",
|
||||||
["pylons"] = {
|
["pylons"] = {
|
||||||
[1] = {
|
[1] = {
|
||||||
@ -293,6 +250,46 @@ local unitPayloads = {
|
|||||||
[1] = 34,
|
[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"] = {
|
["tasks"] = {
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "Canada 2005",
|
"name": "Canada 2005",
|
||||||
"authors": "Khopa",
|
"authors": "Khopa",
|
||||||
"description": "<p>Canada in the 2000s, an F/A-18C Hornet focused faction.</p>",
|
"description": "<p>Canada in the 2000s, an F/A-18C Hornet focused faction.</p>",
|
||||||
|
"locales": ["en_US", "fr_CA"],
|
||||||
"aircrafts": [
|
"aircrafts": [
|
||||||
"FA_18C_hornet",
|
"FA_18C_hornet",
|
||||||
"UH_1H",
|
"UH_1H",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "Canada 2005 (With C-130)",
|
"name": "Canada 2005 (With C-130)",
|
||||||
"authors": "Khopa, SpaceEnthusiast",
|
"authors": "Khopa, SpaceEnthusiast",
|
||||||
"description": "<p>Canada in the 2000s, an F/A-18C Hornet focused faction.</p>",
|
"description": "<p>Canada in the 2000s, an F/A-18C Hornet focused faction.</p>",
|
||||||
|
"locales": ["en_US", "fr_CA"],
|
||||||
"aircrafts": [
|
"aircrafts": [
|
||||||
"FA_18C_hornet",
|
"FA_18C_hornet",
|
||||||
"UH_1H",
|
"UH_1H",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "China 2010",
|
"name": "China 2010",
|
||||||
"authors": "Khopa",
|
"authors": "Khopa",
|
||||||
"description": "<p>China in the late 2000s, early 2010s.</p>",
|
"description": "<p>China in the late 2000s, early 2010s.</p>",
|
||||||
|
"locales": ["zh_CN"],
|
||||||
"aircrafts": [
|
"aircrafts": [
|
||||||
"MiG_21Bis",
|
"MiG_21Bis",
|
||||||
"Su_30",
|
"Su_30",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "France 1985 (Frenchpack)",
|
"name": "France 1985 (Frenchpack)",
|
||||||
"authors": "Colonel Panic",
|
"authors": "Colonel Panic",
|
||||||
"description": "<p>1980s French equipment using FrenchPack.</p>",
|
"description": "<p>1980s French equipment using FrenchPack.</p>",
|
||||||
|
"locales": ["fr_FR"],
|
||||||
"doctrine": "coldwar",
|
"doctrine": "coldwar",
|
||||||
"aircrafts": [
|
"aircrafts": [
|
||||||
"M_2000C",
|
"M_2000C",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "France 1995",
|
"name": "France 1995",
|
||||||
"authors": "Khopa",
|
"authors": "Khopa",
|
||||||
"description": "<p>France in the late 90s before Rafale introduction. A Mirage-2000 centric faction choice.</p>",
|
"description": "<p>France in the late 90s before Rafale introduction. A Mirage-2000 centric faction choice.</p>",
|
||||||
|
"locales": ["fr_FR"],
|
||||||
"aircrafts": [
|
"aircrafts": [
|
||||||
"M_2000C",
|
"M_2000C",
|
||||||
"Mirage_2000_5",
|
"Mirage_2000_5",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "France 2005 (Frenchpack)",
|
"name": "France 2005 (Frenchpack)",
|
||||||
"authors": "HerrTom",
|
"authors": "HerrTom",
|
||||||
"description": "<p>French equipment using the Frenchpack, but without the Rafale mod.</p>",
|
"description": "<p>French equipment using the Frenchpack, but without the Rafale mod.</p>",
|
||||||
|
"locales": ["fr_FR"],
|
||||||
"aircrafts": [
|
"aircrafts": [
|
||||||
"M_2000C",
|
"M_2000C",
|
||||||
"Mirage_2000_5",
|
"Mirage_2000_5",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "German Democratic Republic 1985",
|
"name": "German Democratic Republic 1985",
|
||||||
"authors": "Colonel Panic",
|
"authors": "Colonel Panic",
|
||||||
"description": "<p>The German Democratic Republic in 1985.</p>",
|
"description": "<p>The German Democratic Republic in 1985.</p>",
|
||||||
|
"locales": ["de_DE"],
|
||||||
"doctrine": "coldwar",
|
"doctrine": "coldwar",
|
||||||
"aircrafts": [
|
"aircrafts": [
|
||||||
"MiG_21Bis",
|
"MiG_21Bis",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "Germany 1940",
|
"name": "Germany 1940",
|
||||||
"authors": "Khopa",
|
"authors": "Khopa",
|
||||||
"description": "<p>Germany 1940, Early german faction for Battle of France, or Battle of England.</p>",
|
"description": "<p>Germany 1940, Early german faction for Battle of France, or Battle of England.</p>",
|
||||||
|
"locales": ["de_DE"],
|
||||||
"aircrafts": [
|
"aircrafts": [
|
||||||
"FW_190A8",
|
"FW_190A8",
|
||||||
"FW_190D9",
|
"FW_190D9",
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
"name": "Germany 1942",
|
"name": "Germany 1942",
|
||||||
"authors": "Khopa",
|
"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>",
|
"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": [
|
"aircrafts": [
|
||||||
"FW_190A8",
|
"FW_190A8",
|
||||||
"FW_190D9",
|
"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