Merge branch 'develop' into helipads
# Conflicts: # game/theater/conflicttheater.py # gen/flights/flightplan.py
28
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Campaign update submission
|
||||
about: Submit an update to a campaign you maintain.
|
||||
title: 'Update for <campaign name>'
|
||||
labels: campaign-update-submission
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
This form should only be used for submitted updated miz/json files for campaigns
|
||||
distributed with Liberation. If you are _requesting_ an update to a campaign, see
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance. If the
|
||||
campaign has an owner, it will be updated before release. If it does not, you can
|
||||
volunteer to own it.
|
||||
|
||||
If you are not the owner of the campaign listed on
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance, please start
|
||||
there.
|
||||
|
||||
Otherwise, delete everything above the line below and fill out the following form. Note:
|
||||
GitHub does not accept .miz files. You can either rename the file to .miz.txt or add the
|
||||
file to a .zip file.
|
||||
|
||||
---
|
||||
|
||||
* Campaign name:
|
||||
* Files:
|
||||
* Update summary (optional):
|
||||
22
changelog.md
@ -1,3 +1,11 @@
|
||||
# 4.0.0
|
||||
|
||||
Saves from 3.x are not compatible with 4.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
## Fixes
|
||||
|
||||
# 3.0.0
|
||||
|
||||
Saves from 2.5 are not compatible with 3.0.
|
||||
@ -9,9 +17,14 @@ Saves from 2.5 are not compatible with 3.0.
|
||||
* **[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]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information.
|
||||
* **[Campaign]** Capturing a base now depopulates all of its attached objectives with units: air defenses, EWRs, ships, armor groups, etc. Buildings are captured.
|
||||
* **[Campaign]** Ammunition Depots determine how many ground units can be deployed on the frontline by a control point.
|
||||
* **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions.
|
||||
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
|
||||
* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities.
|
||||
* **[Campaign AI]** Auto purchase of ground units will now maintain unit composition instead of buying randomly. The unit composition is predefined.
|
||||
* **[Campaign AI]** Auto purchase will aim to purchase enough ground units to support the frontline, plus 30% reserve units.
|
||||
* **[Campaign AI]** Auto purchase will now adjust its air/ground balance to favor whichever is under-funded.
|
||||
* **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
|
||||
* **[Flight Planner]** Flight plans now include bullseye waypoints.
|
||||
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route.
|
||||
@ -22,12 +35,15 @@ Saves from 2.5 are not compatible with 3.0.
|
||||
* **[Flight Planner]** Automatic ATO generation for the player's coalition can now be disabled in the settings.
|
||||
* **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets.
|
||||
* **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows.
|
||||
* **[UI]** Added new web based map UI. This is mostly functional but many of the old display options are a WIP. Revert to the old map with --old-map.
|
||||
* **[UI]** Overhauled the map implementation. Now uses satellite imagery instead of low res map images. Display options have moved from the toolbar to panels in the map.
|
||||
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
|
||||
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.
|
||||
* **[UI]** Added global aircraft inventory view under Air Wing dialog.
|
||||
* **[UI]** Base menu now shows information about ground unit deployment limits.
|
||||
* **[Modding]** Campaigns now choose locations for factories to spawn.
|
||||
* **[Modding]** Campaigns now choose locations for ammunition depots to spawn.
|
||||
* **[Modding]** Campaigns now use map structures as strike targets.
|
||||
* **[Modding]** Campaigns may now set *any* objective type to be a required spawn rather than random chance.
|
||||
* **[Modding]** Campaigns may now set *any* objective type to be a required spawn rather than random chance. Support for random objective generation was removed.
|
||||
* **[Modding]** Campaigns may now place AAA objectives.
|
||||
* **[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.
|
||||
@ -41,8 +57,10 @@ Saves from 2.5 are not compatible with 3.0.
|
||||
* **[Campaign AI]** Auto planner will no longer attempt to plan missions for which the faction has no compatible aircraft.
|
||||
* **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used.
|
||||
* **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives.
|
||||
* **[Campaign]** EWR sites are now purchasable.
|
||||
* **[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).
|
||||
* **[Flight Planner]** Fixed some contexts where damaged runways would be used. Destroying a carrier will no longer break the game.
|
||||
|
||||
# 2.5.1
|
||||
|
||||
|
||||
0
game/data/__init__.py
Normal file
@ -11,7 +11,7 @@ DEFAULT_AVAILABLE_BUILDINGS = [
|
||||
"derrick",
|
||||
]
|
||||
|
||||
WW2_FREE = ["fuel", "ware", "fob"]
|
||||
WW2_FREE = ["fuel", "ware"]
|
||||
WW2_GERMANY_BUILDINGS = [
|
||||
"fuel",
|
||||
"ww2bunker",
|
||||
|
||||
@ -1,7 +1,20 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from dcs.task import Reconnaissance
|
||||
|
||||
from game.utils import Distance, feet, nautical_miles
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundUnitProcurementRatios:
|
||||
ratios: dict[GroundUnitClass, float]
|
||||
|
||||
def for_unit_class(self, unit_class: GroundUnitClass) -> float:
|
||||
try:
|
||||
return self.ratios[unit_class] / sum(self.ratios.values())
|
||||
except KeyError:
|
||||
return 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -50,6 +63,8 @@ class Doctrine:
|
||||
|
||||
sweep_distance: Distance
|
||||
|
||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
@ -76,6 +91,17 @@ MODERN_DOCTRINE = Doctrine(
|
||||
cap_engagement_range=nautical_miles(50),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(60),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 3,
|
||||
GroundUnitClass.Atgm: 2,
|
||||
GroundUnitClass.Apc: 2,
|
||||
GroundUnitClass.Ifv: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
@ -103,6 +129,17 @@ COLDWAR_DOCTRINE = Doctrine(
|
||||
cap_engagement_range=nautical_miles(35),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(40),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 4,
|
||||
GroundUnitClass.Atgm: 2,
|
||||
GroundUnitClass.Apc: 3,
|
||||
GroundUnitClass.Ifv: 2,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
@ -130,4 +167,14 @@ WWII_DOCTRINE = Doctrine(
|
||||
cap_engagement_range=nautical_miles(20),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(10),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 3,
|
||||
GroundUnitClass.Atgm: 3,
|
||||
GroundUnitClass.Apc: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 3,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
239
game/data/groundunitclass.py
Normal file
@ -0,0 +1,239 @@
|
||||
from enum import unique, Enum
|
||||
from typing import Type
|
||||
|
||||
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from pydcs_extensions.frenchpack import frenchpack
|
||||
|
||||
|
||||
@unique
|
||||
class GroundUnitClass(Enum):
|
||||
Tank = (
|
||||
"Tank",
|
||||
(
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2A4,
|
||||
Armor.MBT_Leopard_2A4_Trs,
|
||||
Armor.MBT_Leopard_2A5,
|
||||
Armor.MBT_Leopard_2A6M,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_Chieftain_Mk_3,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_IV,
|
||||
Armor.ZTZ_96B,
|
||||
# WW2
|
||||
# Axis
|
||||
Armor.Tk_PzIV_H,
|
||||
Armor.SPG_Sturmpanzer_IV_Brummbar,
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
# Allies
|
||||
Armor.Tk_M4_Sherman,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
),
|
||||
)
|
||||
|
||||
Atgm = (
|
||||
"ATGM",
|
||||
(
|
||||
Armor.ATGM_HMMWV,
|
||||
Armor.ATGM_VAB_Mephisto,
|
||||
Armor.ATGM_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
# WW2 (Tank Destroyers)
|
||||
# Axxis
|
||||
Armor.SPG_StuG_III_Ausf__G,
|
||||
Armor.SPG_StuG_IV,
|
||||
Armor.SPG_Jagdpanzer_IV,
|
||||
Armor.SPG_Jagdpanther_G1,
|
||||
Armor.SPG_Sd_Kfz_184_Elefant,
|
||||
# Allies
|
||||
Armor.SPG_M10_GMC,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
),
|
||||
)
|
||||
|
||||
Ifv = (
|
||||
"IFV",
|
||||
(
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_Warrior,
|
||||
Armor.SPG_Stryker_MGS,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13,
|
||||
),
|
||||
)
|
||||
|
||||
Apc = (
|
||||
"APC",
|
||||
(
|
||||
Armor.IFV_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.IFV_BTR_82A,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_AAV_7_Amphibious,
|
||||
Armor.APC_TPz_Fuchs,
|
||||
Armor.APC_BTR_RD,
|
||||
# WW2
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
),
|
||||
)
|
||||
|
||||
Artillery = (
|
||||
"Artillery",
|
||||
(
|
||||
Artillery.Grad_MRL_FDDM__FC,
|
||||
Artillery.MLRS_9A52_Smerch_HE_300mm,
|
||||
Artillery.SPH_2S1_Gvozdika_122mm,
|
||||
Artillery.SPH_2S3_Akatsia_152mm,
|
||||
Artillery.MLRS_BM_21_Grad_122mm,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
|
||||
Artillery.SPH_M109_Paladin_155mm,
|
||||
Artillery.MLRS_M270_227mm,
|
||||
Artillery.SPM_2S9_Nona_120mm_M,
|
||||
Artillery.SPH_Dana_vz77_152mm,
|
||||
Artillery.SPH_T155_Firtina_155mm,
|
||||
Artillery.PLZ_05,
|
||||
Artillery.SPH_2S19_Msta_152mm,
|
||||
Artillery.MLRS_9A52_Smerch_CM_300mm,
|
||||
# WW2
|
||||
Artillery.SPG_M12_GMC_155mm,
|
||||
),
|
||||
)
|
||||
|
||||
Logistics = (
|
||||
"Logistics",
|
||||
(
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
Unarmed.Truck_M818_6x6,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
Unarmed.Truck_Ural_375,
|
||||
Unarmed.Truck_GAZ_66,
|
||||
Unarmed.Truck_GAZ_3307,
|
||||
Unarmed.Truck_GAZ_3308,
|
||||
Unarmed.Truck_Ural_4320_31_Arm_d,
|
||||
Unarmed.Truck_Ural_4320T,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||
Unarmed.LUV_Kettenrad,
|
||||
Unarmed.Car_Willys_Jeep,
|
||||
Unarmed.LUV_Land_Rover_109,
|
||||
Unarmed.Truck_Land_Rover_101_FC,
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
),
|
||||
)
|
||||
|
||||
Recon = (
|
||||
"Recon",
|
||||
(
|
||||
Armor.Scout_HMMWV,
|
||||
Armor.Scout_Cobra,
|
||||
Armor.LT_PT_76,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.Scout_BRDM_2,
|
||||
# WW2
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.Car_M8_Greyhound_Armored,
|
||||
Armor.Car_Daimler_Armored,
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
),
|
||||
)
|
||||
|
||||
Infantry = (
|
||||
"Infantry",
|
||||
(
|
||||
Infantry.Insurgent_AK_74,
|
||||
Infantry.Infantry_AK_74,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Infantry_M4_Georgia,
|
||||
Infantry.Infantry_AK_74_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Infantry_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Infantry_RPG,
|
||||
),
|
||||
)
|
||||
|
||||
Shorads = (
|
||||
"SHORADS",
|
||||
(
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger__Stinger,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AAA_QF_3_7,
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, class_name: str, unit_list: tuple[Type[VehicleType], ...]
|
||||
) -> None:
|
||||
self.class_name = class_name
|
||||
self.unit_list = unit_list
|
||||
|
||||
def __contains__(self, unit_type: Type[VehicleType]) -> bool:
|
||||
return unit_type in self.unit_list
|
||||
@ -1251,6 +1251,7 @@ REWARDS = {
|
||||
"ammo": 2,
|
||||
"farp": 1,
|
||||
# TODO: Should generate no cash once they generate units.
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/1036
|
||||
"factory": 10,
|
||||
"comms": 10,
|
||||
"oil": 10,
|
||||
|
||||
@ -24,7 +24,7 @@ from game import db
|
||||
from game.theater import Airfield, ControlPoint
|
||||
from game.transfers import CargoShip
|
||||
from game.unitmap import (
|
||||
AirliftUnit,
|
||||
AirliftUnits,
|
||||
Building,
|
||||
ConvoyUnit,
|
||||
FrontLineUnit,
|
||||
@ -75,8 +75,8 @@ class GroundLosses:
|
||||
player_cargo_ships: List[CargoShip] = field(default_factory=list)
|
||||
enemy_cargo_ships: List[CargoShip] = field(default_factory=list)
|
||||
|
||||
player_airlifts: List[AirliftUnit] = field(default_factory=list)
|
||||
enemy_airlifts: List[AirliftUnit] = field(default_factory=list)
|
||||
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
|
||||
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
||||
@ -160,7 +160,7 @@ class Debriefing:
|
||||
yield from self.ground_losses.enemy_cargo_ships
|
||||
|
||||
@property
|
||||
def airlift_losses(self) -> Iterator[AirliftUnit]:
|
||||
def airlift_losses(self) -> Iterator[AirliftUnits]:
|
||||
yield from self.ground_losses.player_airlifts
|
||||
yield from self.ground_losses.enemy_airlifts
|
||||
|
||||
@ -220,7 +220,8 @@ class Debriefing:
|
||||
else:
|
||||
losses = self.ground_losses.enemy_airlifts
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
for unit_type in loss.cargo:
|
||||
losses_by_type[unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
|
||||
@ -144,7 +144,7 @@ class Event:
|
||||
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
for idx, pilot in enumerate(flight.pilots):
|
||||
for idx, pilot in enumerate(flight.roster.pilots):
|
||||
if pilot is None:
|
||||
logging.error(
|
||||
f"Cannot award experience to pilot #{idx} of {flight} "
|
||||
@ -202,19 +202,17 @@ class Event:
|
||||
@staticmethod
|
||||
def commit_airlift_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.airlift_losses:
|
||||
unit_type = loss.unit_type
|
||||
transfer = loss.transfer
|
||||
available = loss.transfer.units.get(unit_type, 0)
|
||||
airlift_name = f"airlift from {transfer.origin} to {transfer.destination}"
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {unit_type} in {airlift_name} but that airlift has "
|
||||
"none available."
|
||||
)
|
||||
continue
|
||||
|
||||
logging.info(f"{unit_type} destroyed in {airlift_name}")
|
||||
transfer.kill_unit(unit_type)
|
||||
for unit_type in loss.cargo:
|
||||
try:
|
||||
transfer.kill_unit(unit_type)
|
||||
logging.info(f"{unit_type} destroyed in {airlift_name}")
|
||||
except KeyError:
|
||||
logging.exception(
|
||||
f"Found killed {unit_type} in {airlift_name} but that airlift "
|
||||
"has none available."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def commit_ground_object_losses(debriefing: Debriefing) -> None:
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
@ -133,6 +134,16 @@ class Faction:
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool:
|
||||
has_access = False
|
||||
for vehicle in unitclass.unit_list:
|
||||
if vehicle in self.frontline_units:
|
||||
return True
|
||||
if vehicle in self.artillery_units:
|
||||
return True
|
||||
|
||||
return has_access
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
faction = Faction(locales=json.get("locales"))
|
||||
|
||||
50
game/game.py
@ -113,8 +113,6 @@ class Game:
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||
self.__culling_zones: List[Point] = []
|
||||
# Culling Points are for individual theater ground objects that we don't wish to cull.
|
||||
self.__culling_points: List[Point] = []
|
||||
self.__destroyed_units: List[str] = []
|
||||
self.savepath = ""
|
||||
self.budget = player_budget
|
||||
@ -124,8 +122,8 @@ class Game:
|
||||
|
||||
self.conditions = self.generate_conditions()
|
||||
|
||||
self.blue_transit_network = self.compute_transit_network_for(player=True)
|
||||
self.red_transit_network = self.compute_transit_network_for(player=False)
|
||||
self.blue_transit_network = TransitNetwork()
|
||||
self.red_transit_network = TransitNetwork()
|
||||
|
||||
self.blue_procurement_requests: List[AircraftProcurementRequest] = []
|
||||
self.red_procurement_requests: List[AircraftProcurementRequest] = []
|
||||
@ -148,7 +146,7 @@ class Game:
|
||||
self.blue_air_wing = AirWing(self, player=True)
|
||||
self.red_air_wing = AirWing(self, player=False)
|
||||
|
||||
self.on_load()
|
||||
self.on_load(game_still_initializing=True)
|
||||
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
@ -301,11 +299,12 @@ class Game:
|
||||
else:
|
||||
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
||||
|
||||
def on_load(self) -> None:
|
||||
def on_load(self, game_still_initializing: bool = False) -> None:
|
||||
LuaPluginManager.load_settings(self.settings)
|
||||
ObjectiveDistanceCache.set_theater(self.theater)
|
||||
self.compute_conflicts_position()
|
||||
self.compute_threat_zones()
|
||||
if not game_still_initializing:
|
||||
self.compute_threat_zones()
|
||||
self.blue_faker = Faker(self.faction_for(player=True).locales)
|
||||
self.red_faker = Faker(self.faction_for(player=False).locales)
|
||||
|
||||
@ -439,8 +438,8 @@ class Game:
|
||||
# gets much more of the budget that turn. Otherwise budget (after
|
||||
# repairs) is split evenly between air and ground. For the default
|
||||
# starting budget of 2000 this gives 600 to ground forces and 1400 to
|
||||
# aircraft.
|
||||
ground_portion = 0.3 if self.turn == 0 else 0.5
|
||||
# aircraft. After that the budget will be spend proportionally based on how much is already invested
|
||||
|
||||
self.budget = ProcurementAi(
|
||||
self,
|
||||
for_player=True,
|
||||
@ -448,7 +447,6 @@ class Game:
|
||||
manage_runways=self.settings.automate_runway_repair,
|
||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||
front_line_budget_share=ground_portion,
|
||||
).spend_budget(self.budget)
|
||||
|
||||
self.enemy_budget = ProcurementAi(
|
||||
@ -458,7 +456,6 @@ class Game:
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True,
|
||||
front_line_budget_share=ground_portion,
|
||||
).spend_budget(self.enemy_budget)
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
@ -519,7 +516,6 @@ class Game:
|
||||
:return: List of points of interests
|
||||
"""
|
||||
zones = []
|
||||
points = []
|
||||
|
||||
# By default, use the existing frontline conflict position
|
||||
for front_line in self.theater.conflicts():
|
||||
@ -529,11 +525,6 @@ class Game:
|
||||
zones.append(front_line.red_cp.position)
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
# Don't cull missile sites - their range is long enough to make them
|
||||
# easily culled despite being a threat.
|
||||
for tgo in cp.ground_objects:
|
||||
if isinstance(tgo, MissileSiteGroundObject):
|
||||
points.append(tgo.position)
|
||||
# If do_not_cull_carrier is enabled, add carriers as culling point
|
||||
if self.settings.perf_do_not_cull_carrier:
|
||||
if cp.is_carrier or cp.is_lha:
|
||||
@ -577,7 +568,6 @@ class Game:
|
||||
zones.append(Point(0, 0))
|
||||
|
||||
self.__culling_zones = zones
|
||||
self.__culling_points = points
|
||||
|
||||
def add_destroyed_units(self, data):
|
||||
pos = Point(data["x"], data["z"])
|
||||
@ -593,19 +583,12 @@ class Game:
|
||||
:param pos: Position you are tryng to spawn stuff at
|
||||
:return: True if units can not be added at given position
|
||||
"""
|
||||
if self.settings.perf_culling == False:
|
||||
if not self.settings.perf_culling:
|
||||
return False
|
||||
else:
|
||||
for z in self.__culling_zones:
|
||||
if (
|
||||
z.distance_to_point(pos)
|
||||
< self.settings.perf_culling_distance * 1000
|
||||
):
|
||||
return False
|
||||
for p in self.__culling_points:
|
||||
if p.distance_to_point(pos) < 2500:
|
||||
return False
|
||||
return True
|
||||
for z in self.__culling_zones:
|
||||
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_culling_zones(self):
|
||||
"""
|
||||
@ -614,13 +597,6 @@ class Game:
|
||||
"""
|
||||
return self.__culling_zones
|
||||
|
||||
def get_culling_points(self):
|
||||
"""
|
||||
Check culling points
|
||||
:return: List of culling points
|
||||
"""
|
||||
return self.__culling_points
|
||||
|
||||
# 1 = red, 2 = blue
|
||||
def get_player_coalition_id(self):
|
||||
return 2
|
||||
|
||||
@ -8,17 +8,19 @@ from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple, Type
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import ControlPoint, MissionTarget
|
||||
from game.utils import Distance
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
FRONTLINE_RESERVES_FACTOR = 1.3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftProcurementRequest:
|
||||
@ -43,10 +45,7 @@ class ProcurementAi:
|
||||
manage_runways: bool,
|
||||
manage_front_line: bool,
|
||||
manage_aircraft: bool,
|
||||
front_line_budget_share: float,
|
||||
) -> None:
|
||||
if front_line_budget_share > 1.0:
|
||||
raise ValueError
|
||||
|
||||
self.game = game
|
||||
self.is_player = for_player
|
||||
@ -55,14 +54,34 @@ class ProcurementAi:
|
||||
self.manage_runways = manage_runways
|
||||
self.manage_front_line = manage_front_line
|
||||
self.manage_aircraft = manage_aircraft
|
||||
self.front_line_budget_share = front_line_budget_share
|
||||
self.threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
def calculate_ground_unit_budget_share(self) -> float:
|
||||
armor_investment = 0
|
||||
aircraft_investment = 0
|
||||
for cp in self.owned_points:
|
||||
cp_ground_units = cp.allocated_ground_units(self.game.transfers)
|
||||
armor_investment += cp_ground_units.total_value
|
||||
cp_aircraft = cp.allocated_aircraft(self.game)
|
||||
aircraft_investment += cp_aircraft.total_value
|
||||
|
||||
total_investment = aircraft_investment + armor_investment
|
||||
if total_investment == 0:
|
||||
# Turn 0 or all units were destroyed. Either way, split 30/70.
|
||||
return 0.3
|
||||
|
||||
# the more planes we have, the more ground units we want and vice versa
|
||||
ground_unit_share = aircraft_investment / total_investment
|
||||
if ground_unit_share > 1.0:
|
||||
raise ValueError
|
||||
|
||||
return ground_unit_share
|
||||
|
||||
def spend_budget(self, budget: float) -> float:
|
||||
if self.manage_runways:
|
||||
budget = self.repair_runways(budget)
|
||||
if self.manage_front_line:
|
||||
armor_budget = math.ceil(budget * self.front_line_budget_share)
|
||||
armor_budget = budget * self.calculate_ground_unit_budget_share()
|
||||
budget -= armor_budget
|
||||
budget += self.reinforce_front_line(armor_budget)
|
||||
|
||||
@ -114,28 +133,14 @@ class ProcurementAi:
|
||||
)
|
||||
return budget
|
||||
|
||||
def random_affordable_ground_unit(
|
||||
self, budget: float, cp: ControlPoint
|
||||
def affordable_ground_unit_of_class(
|
||||
self, budget: float, unit_class: GroundUnitClass
|
||||
) -> Optional[Type[VehicleType]]:
|
||||
affordable_units = [
|
||||
u
|
||||
for u in self.faction.frontline_units + self.faction.artillery_units
|
||||
if db.PRICES[u] <= budget
|
||||
]
|
||||
|
||||
total_number_aa = (
|
||||
cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
|
||||
faction_units = set(self.faction.frontline_units) | set(
|
||||
self.faction.artillery_units
|
||||
)
|
||||
total_non_aa = (
|
||||
cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
|
||||
)
|
||||
max_aa = math.ceil(total_non_aa / 8)
|
||||
|
||||
# Limit the number of AA units the AI will buy
|
||||
if not total_number_aa < max_aa:
|
||||
for unit in [u for u in affordable_units if u in TYPE_SHORAD]:
|
||||
affordable_units.remove(unit)
|
||||
|
||||
of_class = set(unit_class.unit_list) & faction_units
|
||||
affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
|
||||
if not affordable_units:
|
||||
return None
|
||||
return random.choice(affordable_units)
|
||||
@ -147,12 +152,12 @@ class ProcurementAi:
|
||||
# TODO: Attempt to transfer from reserves.
|
||||
|
||||
while budget > 0:
|
||||
candidates = self.front_line_candidates()
|
||||
if not candidates:
|
||||
cp = self.ground_reinforcement_candidate()
|
||||
if cp is None:
|
||||
break
|
||||
|
||||
cp = random.choice(candidates)
|
||||
unit = self.random_affordable_ground_unit(budget, cp)
|
||||
most_needed_type = self.most_needed_unit_class(cp)
|
||||
unit = self.affordable_ground_unit_of_class(budget, most_needed_type)
|
||||
if unit is None:
|
||||
# Can't afford any more units.
|
||||
break
|
||||
@ -162,6 +167,31 @@ class ProcurementAi:
|
||||
|
||||
return budget
|
||||
|
||||
def most_needed_unit_class(self, cp: ControlPoint) -> GroundUnitClass:
|
||||
worst_balanced: Optional[GroundUnitClass] = None
|
||||
worst_fulfillment = math.inf
|
||||
for unit_class in GroundUnitClass:
|
||||
if not self.faction.has_access_to_unittype(unit_class):
|
||||
continue
|
||||
|
||||
current_ratio = self.cost_ratio_of_ground_unit(cp, unit_class)
|
||||
desired_ratio = (
|
||||
self.faction.doctrine.ground_unit_procurement_ratios.for_unit_class(
|
||||
unit_class
|
||||
)
|
||||
)
|
||||
if not desired_ratio:
|
||||
continue
|
||||
if current_ratio >= desired_ratio:
|
||||
continue
|
||||
fulfillment = current_ratio / desired_ratio
|
||||
if fulfillment < worst_fulfillment:
|
||||
worst_fulfillment = fulfillment
|
||||
worst_balanced = unit_class
|
||||
if worst_balanced is None:
|
||||
return GroundUnitClass.Tank
|
||||
return worst_balanced
|
||||
|
||||
def _affordable_aircraft_for_task(
|
||||
self,
|
||||
task: FlightType,
|
||||
@ -179,7 +209,7 @@ class ProcurementAi:
|
||||
continue
|
||||
|
||||
for squadron in self.air_wing.squadrons_for(unit):
|
||||
if task in squadron.mission_types:
|
||||
if task in squadron.auto_assignable_mission_types:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
@ -244,11 +274,9 @@ class ProcurementAi:
|
||||
) -> Iterator[ControlPoint]:
|
||||
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
|
||||
threatened = []
|
||||
for cp in distance_cache.airfields_within(request.range):
|
||||
for cp in distance_cache.operational_airfields_within(request.range):
|
||||
if not cp.is_friendly(self.is_player):
|
||||
continue
|
||||
if not cp.runway_is_operational():
|
||||
continue
|
||||
if cp.unclaimed_parking(self.game) < request.number:
|
||||
continue
|
||||
if self.threat_zones.threatened(cp.position):
|
||||
@ -256,56 +284,69 @@ class ProcurementAi:
|
||||
yield cp
|
||||
yield from threatened
|
||||
|
||||
def front_line_candidates(self) -> List[ControlPoint]:
|
||||
candidates = []
|
||||
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
|
||||
worst_supply = math.inf
|
||||
understaffed: Optional[ControlPoint] = None
|
||||
|
||||
# Prefer to buy front line units at active front lines that are not
|
||||
# already overloaded.
|
||||
for cp in self.owned_points:
|
||||
|
||||
total_ground_units_allocated_to_this_control_point = (
|
||||
self.total_ground_units_allocated_to(cp)
|
||||
)
|
||||
if not cp.has_active_frontline:
|
||||
continue
|
||||
|
||||
if not cp.has_ground_unit_source(self.game):
|
||||
# No source of ground units, so can't buy anything.
|
||||
continue
|
||||
|
||||
if (
|
||||
total_ground_units_allocated_to_this_control_point >= 50
|
||||
or total_ground_units_allocated_to_this_control_point
|
||||
>= cp.frontline_unit_count_limit
|
||||
):
|
||||
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
|
||||
allocated = cp.allocated_ground_units(self.game.transfers)
|
||||
if allocated.total >= purchase_target:
|
||||
# Control point is already sufficiently defended.
|
||||
continue
|
||||
for connected in cp.connected_points:
|
||||
if not connected.is_friendly(to_player=self.is_player):
|
||||
candidates.append(cp)
|
||||
if allocated.total < worst_supply:
|
||||
worst_supply = allocated.total
|
||||
understaffed = cp
|
||||
|
||||
if not candidates:
|
||||
# Otherwise buy reserves, but don't exceed 10 reserve units per CP.
|
||||
# These units do not exist in the world until the CP becomes
|
||||
# connected to an active front line, at which point all these units
|
||||
# will suddenly appear at the gates of the newly captured CP.
|
||||
#
|
||||
# To avoid sudden overwhelming numbers of units we avoid buying
|
||||
# many.
|
||||
#
|
||||
# Also, do not bother buying units at bases that will never connect
|
||||
# to a front line.
|
||||
for cp in self.owned_points:
|
||||
if not cp.can_recruit_ground_units(self.game):
|
||||
continue
|
||||
if self.total_ground_units_allocated_to(cp) >= 10:
|
||||
continue
|
||||
if cp.is_global:
|
||||
continue
|
||||
candidates.append(cp)
|
||||
if understaffed is not None:
|
||||
return understaffed
|
||||
|
||||
return candidates
|
||||
# Otherwise buy reserves, but don't exceed the amount defined in the settings.
|
||||
# These units do not exist in the world until the CP becomes
|
||||
# connected to an active front line, at which point all these units
|
||||
# will suddenly appear at the gates of the newly captured CP.
|
||||
#
|
||||
# To avoid sudden overwhelming numbers of units we avoid buying
|
||||
# many.
|
||||
#
|
||||
# Also, do not bother buying units at bases that will never connect
|
||||
# to a front line.
|
||||
for cp in self.owned_points:
|
||||
if cp.is_global:
|
||||
continue
|
||||
if not cp.can_recruit_ground_units(self.game):
|
||||
continue
|
||||
|
||||
def total_ground_units_allocated_to(self, control_point: ControlPoint) -> int:
|
||||
total = control_point.expected_ground_units_next_turn.total
|
||||
for transfer in self.game.transfers:
|
||||
if transfer.destination == control_point:
|
||||
total += sum(transfer.units.values())
|
||||
return total
|
||||
allocated = cp.allocated_ground_units(self.game.transfers)
|
||||
if allocated.total >= self.game.settings.reserves_procurement_target:
|
||||
continue
|
||||
|
||||
if allocated.total < worst_supply:
|
||||
worst_supply = allocated.total
|
||||
understaffed = cp
|
||||
|
||||
return understaffed
|
||||
|
||||
def cost_ratio_of_ground_unit(
|
||||
self, control_point: ControlPoint, unit_class: GroundUnitClass
|
||||
) -> float:
|
||||
allocations = control_point.allocated_ground_units(self.game.transfers)
|
||||
class_cost = 0
|
||||
total_cost = 0
|
||||
for unit_type, count in allocations.all.items():
|
||||
cost = db.PRICES[unit_type] * count
|
||||
total_cost += cost
|
||||
if unit_type in unit_class:
|
||||
class_cost += cost
|
||||
if not total_cost:
|
||||
return 0
|
||||
return class_cost / total_cost
|
||||
|
||||
@ -43,11 +43,11 @@ class Settings:
|
||||
automate_front_line_reinforcements: bool = False
|
||||
automate_aircraft_reinforcements: bool = False
|
||||
restrict_weapons_by_date: bool = False
|
||||
disable_legacy_aewc: bool = False
|
||||
disable_legacy_aewc: bool = True
|
||||
generate_dark_kneeboard: bool = False
|
||||
invulnerable_player_pilots: bool = True
|
||||
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
|
||||
auto_ato_player_missions_asap: bool = False
|
||||
auto_ato_player_missions_asap: bool = True
|
||||
|
||||
# Performance oriented
|
||||
perf_red_alert_state: bool = True
|
||||
@ -57,6 +57,7 @@ class Settings:
|
||||
perf_moving_units: bool = True
|
||||
perf_infantry: bool = True
|
||||
perf_destroyed_units: bool = True
|
||||
reserves_procurement_target: int = 10
|
||||
|
||||
# Performance culling
|
||||
perf_culling: bool = False
|
||||
|
||||
@ -10,10 +10,8 @@ from pathlib import Path
|
||||
from typing import (
|
||||
Type,
|
||||
Tuple,
|
||||
List,
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
)
|
||||
@ -83,9 +81,12 @@ class Squadron:
|
||||
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)
|
||||
mission_types: tuple[FlightType, ...]
|
||||
pilots: list[Pilot]
|
||||
available_pilots: list[Pilot] = field(init=False, hash=False, compare=False)
|
||||
auto_assignable_mission_types: set[FlightType] = 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
|
||||
@ -95,6 +96,7 @@ class Squadron:
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.available_pilots = list(self.active_pilots)
|
||||
self.auto_assignable_mission_types = set(self.mission_types)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.name} "{self.nickname}"'
|
||||
@ -142,8 +144,12 @@ class Squadron:
|
||||
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 return_pilots(self, pilots: Sequence[Pilot]) -> None:
|
||||
# Return in reverse so that returning two pilots and then getting two more
|
||||
# results in the same ordering. This happens commonly when resetting rosters in
|
||||
# the UI, when we clear the roster because the UI is updating, then end up
|
||||
# repopulating the same size flight from the same squadron.
|
||||
self.available_pilots.extend(reversed(pilots))
|
||||
|
||||
def enlist_new_pilots(self, count: int) -> None:
|
||||
new_pilots = [Pilot(self.faker.name()) for _ in range(count)]
|
||||
@ -160,6 +166,9 @@ class Squadron:
|
||||
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
|
||||
return [p for p in self.pilots if p.status == status]
|
||||
|
||||
def _pilots_without_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)
|
||||
@ -169,8 +178,12 @@ class Squadron:
|
||||
return self._pilots_with_status(PilotStatus.OnLeave)
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return len(self.active_pilots) + len(self.pilots_on_leave)
|
||||
def number_of_pilots_including_dead(self) -> int:
|
||||
return len(self.pilots)
|
||||
|
||||
@property
|
||||
def number_of_living_pilots(self) -> int:
|
||||
return len(self._pilots_without_status(PilotStatus.Dead))
|
||||
|
||||
def pilot_at_index(self, index: int) -> Pilot:
|
||||
return self.pilots[index]
|
||||
@ -213,6 +226,12 @@ class Squadron:
|
||||
player=player,
|
||||
)
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
# TODO: Remove save compat.
|
||||
if "auto_assignable_mission_types" not in state:
|
||||
state["auto_assignable_mission_types"] = set(state["mission_types"])
|
||||
self.__dict__.update(state)
|
||||
|
||||
|
||||
class SquadronLoader:
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
|
||||
@ -10,7 +10,6 @@ from dcs.vehicles import AirDefence, Armor
|
||||
|
||||
from game import db
|
||||
from game.db import PRICES
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
|
||||
STRENGTH_AA_ASSEMBLE_MIN = 0.2
|
||||
PLANES_SCRAMBLE_MIN_BASE = 2
|
||||
@ -25,6 +24,7 @@ class Base:
|
||||
def __init__(self):
|
||||
self.aircraft: Dict[Type[FlyingType], int] = {}
|
||||
self.armor: Dict[Type[VehicleType], int] = {}
|
||||
# TODO: Appears unused.
|
||||
self.aa: Dict[AirDefence, int] = {}
|
||||
self.commision_points: Dict[Type, float] = {}
|
||||
self.strength = 1
|
||||
@ -47,10 +47,6 @@ class Base:
|
||||
logging.exception(f"No price found for {unit_type.id}")
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_frontline_aa(self) -> int:
|
||||
return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD])
|
||||
|
||||
@property
|
||||
def total_aa(self) -> int:
|
||||
return sum(self.aa.values())
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
@ -40,10 +39,6 @@ from dcs.unitgroup import (
|
||||
VehicleGroup,
|
||||
)
|
||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||
|
||||
from .latlon import LatLon
|
||||
from ..helipad import Helipad
|
||||
from ..scenery_group import SceneryGroup
|
||||
from pyproj import CRS, Transformer
|
||||
from shapely import geometry, ops
|
||||
|
||||
@ -58,10 +53,12 @@ from .controlpoint import (
|
||||
)
|
||||
from .frontline import FrontLine
|
||||
from .landmap import Landmap, load_landmap, poly_contains
|
||||
from .latlon import LatLon
|
||||
from .projections import TransverseMercator
|
||||
from ..point_with_heading import PointWithHeading
|
||||
from ..profiling import logged_duration
|
||||
from ..utils import Distance, meters, nautical_miles
|
||||
from ..scenery_group import SceneryGroup
|
||||
from ..utils import Distance, meters
|
||||
|
||||
SIZE_TINY = 150
|
||||
SIZE_SMALL = 600
|
||||
@ -88,42 +85,39 @@ class MizCampaignLoader:
|
||||
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
|
||||
FARP_HELIPAD = "SINGLE_HELIPAD"
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
|
||||
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id
|
||||
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_Grison.id
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
|
||||
|
||||
# Multiple options for the required SAMs so campaign designers can more
|
||||
# accurately see the coverage of their IADS for the expected type.
|
||||
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
# Multiple options for air defenses so campaign designers can more accurately see
|
||||
# the coverage of their IADS for the expected type.
|
||||
LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Patriot_LN.id,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
|
||||
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id,
|
||||
}
|
||||
|
||||
REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Hawk_LN_M192.id,
|
||||
AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
|
||||
AirDefence.SAM_SA_3_S_125_Goa_LN.id,
|
||||
}
|
||||
|
||||
REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES = {
|
||||
SHORT_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.SAM_Avenger__Stinger.id,
|
||||
AirDefence.SAM_Rapier_LN.id,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison.id,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id,
|
||||
}
|
||||
|
||||
REQUIRED_AAA_UNIT_TYPES = {
|
||||
AAA_UNIT_TYPES = {
|
||||
AirDefence.AAA_8_8cm_Flak_18.id,
|
||||
AirDefence.SPAAA_Vulcan_M163.id,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id,
|
||||
}
|
||||
|
||||
REQUIRED_EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
|
||||
EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
|
||||
|
||||
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
|
||||
|
||||
@ -131,9 +125,7 @@ class MizCampaignLoader:
|
||||
|
||||
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
|
||||
|
||||
REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
||||
|
||||
BASE_DEFENSE_RADIUS = nautical_miles(2)
|
||||
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
self.theater = theater
|
||||
@ -211,98 +203,56 @@ class MizCampaignLoader:
|
||||
|
||||
@property
|
||||
def ships(self) -> Iterator[ShipGroup]:
|
||||
for group in self.blue.ship_group:
|
||||
if group.units[0].type == self.SHIP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_ships(self) -> Iterator[ShipGroup]:
|
||||
for group in self.red.ship_group:
|
||||
if group.units[0].type == self.SHIP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ewrs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.EWR_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.SAM_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def garrisons(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.GARRISON_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def offshore_strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in self.blue.static_group:
|
||||
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_offshore_strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in self.red.static_group:
|
||||
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def missile_sites(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_missile_sites(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def coastal_defenses(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.blue.vehicle_group:
|
||||
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_coastal_defenses(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_long_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
def long_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES:
|
||||
if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_medium_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
def medium_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
|
||||
if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_short_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
def short_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_SHORT_RANGE_SAM_UNIT_TYPES:
|
||||
if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_aaa(self) -> Iterator[VehicleGroup]:
|
||||
def aaa(self) -> Iterator[VehicleGroup]:
|
||||
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
|
||||
if group.units[0].type in self.REQUIRED_AAA_UNIT_TYPES:
|
||||
if group.units[0].type in self.AAA_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_ewrs(self) -> Iterator[VehicleGroup]:
|
||||
def ewrs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.REQUIRED_EWR_UNIT_TYPE:
|
||||
if group.units[0].type in self.EWR_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
@ -330,9 +280,9 @@ class MizCampaignLoader:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def required_strike_targets(self) -> Iterator[StaticGroup]:
|
||||
def strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.REQUIRED_STRIKE_TARGET_UNIT_TYPE:
|
||||
if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
@ -441,112 +391,57 @@ class MizCampaignLoader:
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
for group in self.garrisons:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_garrisons.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
else:
|
||||
logging.warning(f"Found garrison unit too far from base: {group.name}")
|
||||
|
||||
for group in self.sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_air_defense.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
else:
|
||||
closest.preset_locations.strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
if distance < self.BASE_DEFENSE_RADIUS:
|
||||
closest.preset_locations.base_ewrs.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
else:
|
||||
closest.preset_locations.ewrs.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.offshore_strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_offshore_strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_offshore_strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ships:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ships.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_ships:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_ships.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.missile_sites:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.missile_sites.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_missile_sites:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_missile_sites.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.coastal_defenses:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.coastal_defenses.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_coastal_defenses:
|
||||
for group in self.long_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_coastal_defenses.append(
|
||||
closest.preset_locations.long_range_sams.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_long_range_sams:
|
||||
for group in self.medium_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_long_range_sams.append(
|
||||
closest.preset_locations.medium_range_sams.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_medium_range_sams:
|
||||
for group in self.short_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_medium_range_sams.append(
|
||||
closest.preset_locations.short_range_sams.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_short_range_sams:
|
||||
for group in self.aaa:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_short_range_sams.append(
|
||||
closest.preset_locations.aaa.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_aaa:
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_aaa.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_ewrs.append(
|
||||
closest.preset_locations.ewrs.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
@ -574,9 +469,9 @@ class MizCampaignLoader:
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.required_strike_targets:
|
||||
for group in self.strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.required_strike_locations.append(
|
||||
closest.preset_locations.strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
|
||||
@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
import heapq
|
||||
import itertools
|
||||
import logging
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from functools import total_ordering
|
||||
from enum import Enum, unique, auto, IntEnum
|
||||
from functools import total_ordering, cached_property
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
@ -33,24 +33,19 @@ from dcs.ships import (
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||
from dcs.unit import Unit
|
||||
from dcs.unittype import FlyingType
|
||||
from dcs.unittype import FlyingType, VehicleType
|
||||
|
||||
from game import db
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.scenery_group import SceneryGroup
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from gen.runways import RunwayAssigner, RunwayData
|
||||
from .base import Base
|
||||
from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import (
|
||||
BaseDefenseGroundObject,
|
||||
EwrGroundObject,
|
||||
GenericCarrierGroundObject,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from ..db import PRICES
|
||||
from ..helipad import Helipad
|
||||
@ -60,6 +55,7 @@ from ..weather import Conditions
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from gen.flights.flight import FlightType
|
||||
from ..transfers import PendingTransfers
|
||||
|
||||
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
|
||||
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
|
||||
@ -79,149 +75,133 @@ class ControlPointType(Enum):
|
||||
OFF_MAP = 6
|
||||
|
||||
|
||||
class LocationType(Enum):
|
||||
BaseAirDefense = "base air defense"
|
||||
Coastal = "coastal defense"
|
||||
Ewr = "EWR"
|
||||
BaseEwr = "Base EWR"
|
||||
Garrison = "garrison"
|
||||
MissileSite = "missile site"
|
||||
OffshoreStrikeTarget = "offshore strike target"
|
||||
Sam = "SAM"
|
||||
Ship = "ship"
|
||||
Shorad = "SHORAD"
|
||||
StrikeTarget = "strike target"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetLocations:
|
||||
"""Defines the preset locations loaded from the campaign mission file."""
|
||||
|
||||
#: Locations used for spawning ground defenses for bases.
|
||||
base_garrisons: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
|
||||
#: and SHORADs.
|
||||
base_air_defense: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by EWRs.
|
||||
ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by Base EWRs.
|
||||
base_ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
|
||||
#: Locations used by non-carrier ships that will be spawned unless the faction has
|
||||
#: no navy or the player has disabled ship generation for the owning side.
|
||||
ships: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by non-carrier ships that will be spawned unless the faction has
|
||||
#: no navy or the player has disable ship generation for the original owning side.
|
||||
required_ships: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by coastal defenses.
|
||||
#: Locations used by coastal defenses that are generated if the faction is capable.
|
||||
coastal_defenses: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by coastal defenses that are always generated if the faction is
|
||||
#: capable.
|
||||
required_coastal_defenses: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by ground based strike objectives.
|
||||
strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by ground based strike objectives that will always be spawned.
|
||||
required_strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by offshore strike objectives.
|
||||
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by offshore strike objectives that will always be spawned.
|
||||
required_offshore_strike_locations: List[PointWithHeading] = field(
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
#: Locations used by missile sites like scuds and V-2s.
|
||||
#: Locations used by missile sites like scuds and V-2s that are generated if the
|
||||
#: faction is capable.
|
||||
missile_sites: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations used by missile sites like scuds and V-2s that are always generated if
|
||||
#: the faction is capable.
|
||||
required_missile_sites: List[PointWithHeading] = field(default_factory=list)
|
||||
#: Locations of long range SAMs.
|
||||
long_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of long range SAMs which should always be spawned.
|
||||
required_long_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
#: Locations of medium range SAMs.
|
||||
medium_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of medium range SAMs which should always be spawned.
|
||||
required_medium_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
#: Locations of short range SAMs.
|
||||
short_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of short range SAMs which should always be spawned.
|
||||
required_short_range_sams: List[PointWithHeading] = field(default_factory=list)
|
||||
#: Locations of AAA groups.
|
||||
aaa: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of AAA groups which should always be spawned.
|
||||
required_aaa: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of EWRs which should always be spawned.
|
||||
required_ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||
#: Locations of EWRs.
|
||||
ewrs: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of map scenery to create zones for.
|
||||
scenery: List[SceneryGroup] = field(default_factory=list)
|
||||
|
||||
#: Locations of factories for producing ground units. These will always be spawned.
|
||||
#: Locations of factories for producing ground units.
|
||||
factories: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
#: Locations of ammo depots for controlling number of units on the front line at a control point.
|
||||
#: 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.
|
||||
armor_groups: List[PointWithHeading] = field(default_factory=list)
|
||||
|
||||
@staticmethod
|
||||
def _random_from(points: List[PointWithHeading]) -> Optional[PointWithHeading]:
|
||||
"""Finds, removes, and returns a random position from the given list."""
|
||||
if not points:
|
||||
return None
|
||||
point = random.choice(points)
|
||||
points.remove(point)
|
||||
return point
|
||||
|
||||
def random_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
|
||||
"""Returns a position suitable for the given location type.
|
||||
|
||||
The location, if found, will be claimed by the caller and not available
|
||||
to subsequent calls.
|
||||
"""
|
||||
if location_type == LocationType.BaseAirDefense:
|
||||
return self._random_from(self.base_air_defense)
|
||||
if location_type == LocationType.Coastal:
|
||||
return self._random_from(self.coastal_defenses)
|
||||
if location_type == LocationType.Ewr:
|
||||
return self._random_from(self.ewrs)
|
||||
if location_type == LocationType.BaseEwr:
|
||||
return self._random_from(self.base_ewrs)
|
||||
if location_type == LocationType.Garrison:
|
||||
return self._random_from(self.base_garrisons)
|
||||
if location_type == LocationType.MissileSite:
|
||||
return self._random_from(self.missile_sites)
|
||||
if location_type == LocationType.OffshoreStrikeTarget:
|
||||
return self._random_from(self.offshore_strike_locations)
|
||||
if location_type == LocationType.Sam:
|
||||
return self._random_from(self.strike_locations)
|
||||
if location_type == LocationType.Ship:
|
||||
return self._random_from(self.ships)
|
||||
if location_type == LocationType.Shorad:
|
||||
return self._random_from(self.base_garrisons)
|
||||
if location_type == LocationType.StrikeTarget:
|
||||
return self._random_from(self.strike_locations)
|
||||
logging.error(f"Unknown location type: {location_type}")
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PendingOccupancy:
|
||||
present: int
|
||||
ordered: int
|
||||
transferring: int
|
||||
class AircraftAllocations:
|
||||
present: dict[Type[FlyingType], int]
|
||||
ordered: dict[Type[FlyingType], int]
|
||||
transferring: dict[Type[FlyingType], int]
|
||||
|
||||
@property
|
||||
def total_value(self) -> int:
|
||||
total: int = 0
|
||||
for unit_type, count in self.present.items():
|
||||
total += PRICES[unit_type] * count
|
||||
for unit_type, count in self.ordered.items():
|
||||
total += PRICES[unit_type] * count
|
||||
for unit_type, count in self.transferring.items():
|
||||
total += PRICES[unit_type] * count
|
||||
|
||||
return total
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return self.present + self.ordered + self.transferring
|
||||
return self.total_present + self.total_ordered + self.total_transferring
|
||||
|
||||
@property
|
||||
def total_present(self) -> int:
|
||||
return sum(self.present.values())
|
||||
|
||||
@property
|
||||
def total_ordered(self) -> int:
|
||||
return sum(self.ordered.values())
|
||||
|
||||
@property
|
||||
def total_transferring(self) -> int:
|
||||
return sum(self.transferring.values())
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitAllocations:
|
||||
present: dict[Type[VehicleType], int]
|
||||
ordered: dict[Type[VehicleType], int]
|
||||
transferring: dict[Type[VehicleType], int]
|
||||
|
||||
@property
|
||||
def all(self) -> dict[Type[VehicleType], int]:
|
||||
combined: dict[Type[VehicleType], int] = defaultdict(int)
|
||||
for unit_type, count in itertools.chain(
|
||||
self.present.items(), self.ordered.items(), self.transferring.items()
|
||||
):
|
||||
combined[unit_type] += count
|
||||
return dict(combined)
|
||||
|
||||
@property
|
||||
def total_value(self) -> int:
|
||||
total: int = 0
|
||||
for unit_type, count in self.present.items():
|
||||
total += PRICES[unit_type] * count
|
||||
for unit_type, count in self.ordered.items():
|
||||
total += PRICES[unit_type] * count
|
||||
for unit_type, count in self.transferring.items():
|
||||
total += PRICES[unit_type] * count
|
||||
|
||||
return total
|
||||
|
||||
@cached_property
|
||||
def total(self) -> int:
|
||||
return self.total_present + self.total_ordered + self.total_transferring
|
||||
|
||||
@cached_property
|
||||
def total_present(self) -> int:
|
||||
return sum(self.present.values())
|
||||
|
||||
@cached_property
|
||||
def total_ordered(self) -> int:
|
||||
return sum(self.ordered.values())
|
||||
|
||||
@cached_property
|
||||
def total_transferring(self) -> int:
|
||||
return sum(self.transferring.values())
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -285,6 +265,13 @@ class GroundUnitDestination:
|
||||
return self.total_value < other.total_value
|
||||
|
||||
|
||||
@unique
|
||||
class ControlPointStatus(IntEnum):
|
||||
Functional = auto()
|
||||
Damaged = auto()
|
||||
Destroyed = auto()
|
||||
|
||||
|
||||
class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
position = None # type: Point
|
||||
@ -315,7 +302,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
self.full_name = name
|
||||
self.at = at
|
||||
self.connected_objectives: List[TheaterGroundObject] = []
|
||||
self.base_defenses: List[BaseDefenseGroundObject] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
self.helipads: List[Helipad] = []
|
||||
|
||||
@ -344,7 +330,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
||||
return list(itertools.chain(self.connected_objectives, self.base_defenses))
|
||||
return list(self.connected_objectives)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
@ -553,24 +539,6 @@ class ControlPoint(MissionTarget, ABC):
|
||||
def is_friendly_to(self, control_point: ControlPoint) -> bool:
|
||||
return control_point.is_friendly(self.captured)
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def clear_base_defenses(self) -> None:
|
||||
for base_defense in self.base_defenses:
|
||||
p = PointWithHeading.from_point(base_defense.position, base_defense.heading)
|
||||
if isinstance(base_defense, EwrGroundObject):
|
||||
self.preset_locations.base_ewrs.append(p)
|
||||
elif isinstance(base_defense, SamGroundObject):
|
||||
self.preset_locations.base_air_defense.append(p)
|
||||
elif isinstance(base_defense, VehicleGroupGroundObject):
|
||||
self.preset_locations.base_garrisons.append(p)
|
||||
else:
|
||||
logging.error(
|
||||
"Could not determine preset location type for "
|
||||
f"{base_defense}. Assuming garrison type."
|
||||
)
|
||||
self.preset_locations.base_garrisons.append(p)
|
||||
self.base_defenses = []
|
||||
|
||||
def capture_equipment(self, game: Game) -> None:
|
||||
total = self.base.total_armor_value
|
||||
self.base.armor.clear()
|
||||
@ -625,7 +593,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
max_retreat_distance = nautical_miles(200)
|
||||
# Skip the first airbase because that's the airbase we're retreating
|
||||
# from.
|
||||
airfields = list(closest.airfields_within(max_retreat_distance))[1:]
|
||||
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
|
||||
for airbase in airfields:
|
||||
if not airbase.can_operate(airframe):
|
||||
continue
|
||||
@ -655,11 +623,17 @@ class ControlPoint(MissionTarget, ABC):
|
||||
airframe, count = self.base.aircraft.popitem()
|
||||
self._retreat_air_units(game, airframe, count)
|
||||
|
||||
def depopulate_uncapturable_tgos(self) -> None:
|
||||
for tgo in self.connected_objectives:
|
||||
if not tgo.capturable:
|
||||
tgo.clear()
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def capture(self, game: Game, for_player: bool) -> None:
|
||||
self.pending_unit_deliveries.refund_all(game)
|
||||
self.retreat_ground_units(game)
|
||||
self.retreat_air_units(game)
|
||||
self.depopulate_uncapturable_tgos()
|
||||
|
||||
if for_player:
|
||||
self.captured = True
|
||||
@ -668,46 +642,29 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
self.base.set_strength_to_minimum()
|
||||
|
||||
self.clear_base_defenses()
|
||||
from .start_generator import BaseDefenseGenerator
|
||||
|
||||
BaseDefenseGenerator(game, self).generate()
|
||||
|
||||
@abstractmethod
|
||||
def can_operate(self, aircraft: Type[FlyingType]) -> bool:
|
||||
...
|
||||
|
||||
def aircraft_transferring(self, game: Game) -> int:
|
||||
def aircraft_transferring(self, game: Game) -> dict[Type[FlyingType], int]:
|
||||
if self.captured:
|
||||
ato = game.blue_ato
|
||||
else:
|
||||
ato = game.red_ato
|
||||
|
||||
total = 0
|
||||
transferring: defaultdict[Type[FlyingType], int] = defaultdict(int)
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
if flight.departure == flight.arrival:
|
||||
continue
|
||||
if flight.departure == self:
|
||||
total -= flight.count
|
||||
transferring[flight.unit_type] -= flight.count
|
||||
elif flight.arrival == self:
|
||||
total += flight.count
|
||||
return total
|
||||
|
||||
def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
|
||||
return PendingOccupancy(
|
||||
self.base.total_aircraft, on_order, self.aircraft_transferring(game)
|
||||
)
|
||||
transferring[flight.unit_type] += flight.count
|
||||
return transferring
|
||||
|
||||
def unclaimed_parking(self, game: Game) -> int:
|
||||
return (
|
||||
self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
|
||||
)
|
||||
return self.total_aircraft_parking - self.allocated_aircraft(game).total
|
||||
|
||||
@abstractmethod
|
||||
def active_runway(
|
||||
@ -757,47 +714,34 @@ class ControlPoint(MissionTarget, ABC):
|
||||
u.position.x = u.position.x + delta.x
|
||||
u.position.y = u.position.y + delta.y
|
||||
|
||||
@property
|
||||
def pending_frontline_aa_deliveries_count(self):
|
||||
"""
|
||||
Get number of pending frontline aa units
|
||||
"""
|
||||
if self.pending_unit_deliveries:
|
||||
return sum(
|
||||
[
|
||||
v
|
||||
for k, v in self.pending_unit_deliveries.units.items()
|
||||
if k in TYPE_SHORAD
|
||||
]
|
||||
)
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def pending_deliveries_count(self):
|
||||
"""
|
||||
Get number of pending units
|
||||
"""
|
||||
if self.pending_unit_deliveries:
|
||||
return sum([v for k, v in self.pending_unit_deliveries.units.items()])
|
||||
else:
|
||||
return 0
|
||||
|
||||
@property
|
||||
def expected_ground_units_next_turn(self) -> PendingOccupancy:
|
||||
on_order = 0
|
||||
for unit_bought in self.pending_unit_deliveries.units:
|
||||
def allocated_aircraft(self, game: Game) -> AircraftAllocations:
|
||||
on_order = {}
|
||||
for unit_bought, count in self.pending_unit_deliveries.units.items():
|
||||
if issubclass(unit_bought, FlyingType):
|
||||
continue
|
||||
if unit_bought in TYPE_SHORAD:
|
||||
continue
|
||||
on_order += self.pending_unit_deliveries.units[unit_bought]
|
||||
on_order[unit_bought] = count
|
||||
|
||||
return PendingOccupancy(
|
||||
self.base.total_armor,
|
||||
return AircraftAllocations(
|
||||
self.base.aircraft, on_order, self.aircraft_transferring(game)
|
||||
)
|
||||
|
||||
def allocated_ground_units(
|
||||
self, transfers: PendingTransfers
|
||||
) -> GroundUnitAllocations:
|
||||
on_order = {}
|
||||
for unit_bought, count in self.pending_unit_deliveries.units.items():
|
||||
if issubclass(unit_bought, VehicleType):
|
||||
on_order[unit_bought] = count
|
||||
|
||||
transferring: dict[Type[VehicleType], int] = defaultdict(int)
|
||||
for transfer in transfers:
|
||||
if transfer.destination == self:
|
||||
for unit_type, count in transfer.units.items():
|
||||
transferring[unit_type] += count
|
||||
|
||||
return GroundUnitAllocations(
|
||||
self.base.armor,
|
||||
on_order,
|
||||
# Ground unit transfers not yet implemented.
|
||||
transferring=0,
|
||||
transferring,
|
||||
)
|
||||
|
||||
@property
|
||||
@ -816,18 +760,27 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
@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
|
||||
+ self.active_ammo_depots_count * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION
|
||||
)
|
||||
|
||||
@property
|
||||
def active_ammo_depots_count(self) -> int:
|
||||
"""Return the number of available ammo depots"""
|
||||
return len(
|
||||
[
|
||||
obj
|
||||
for obj in self.connected_objectives
|
||||
if obj.category == "ammo" and not obj.is_dead
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def total_ammo_depots_count(self) -> int:
|
||||
"""Return the number of ammo depots, including dead ones"""
|
||||
return len([obj for obj in self.connected_objectives if obj.category == "ammo"])
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
return []
|
||||
@ -837,6 +790,11 @@ class ControlPoint(MissionTarget, ABC):
|
||||
def category(self) -> str:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def status(self) -> ControlPointStatus:
|
||||
...
|
||||
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
def __init__(
|
||||
@ -921,6 +879,15 @@ class Airfield(ControlPoint):
|
||||
def category(self) -> str:
|
||||
return "airfield"
|
||||
|
||||
@property
|
||||
def status(self) -> ControlPointStatus:
|
||||
runway_staus = self.runway_status
|
||||
if runway_staus.needs_repair:
|
||||
return ControlPointStatus.Destroyed
|
||||
elif runway_staus.damaged:
|
||||
return ControlPointStatus.Damaged
|
||||
return ControlPointStatus.Functional
|
||||
|
||||
|
||||
class NavalControlPoint(ControlPoint, ABC):
|
||||
@property
|
||||
@ -945,20 +912,24 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
def heading(self) -> int:
|
||||
return 0 # TODO compute heading
|
||||
|
||||
def find_main_tgo(self) -> TheaterGroundObject:
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier in ["CARRIER", "LHA"]:
|
||||
return g
|
||||
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
|
||||
|
||||
def runway_is_operational(self) -> bool:
|
||||
# Necessary because it's possible for the carrier itself to have sunk
|
||||
# while its escorts are still alive.
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier in ["CARRIER", "LHA"]:
|
||||
for group in g.groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
]:
|
||||
return True
|
||||
for group in self.find_main_tgo().groups:
|
||||
for u in group.units:
|
||||
if db.unit_type_from_name(u.type) in [
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def active_runway(
|
||||
@ -984,6 +955,14 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
def can_deploy_ground_units(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def status(self) -> ControlPointStatus:
|
||||
if not self.runway_is_operational():
|
||||
return ControlPointStatus.Destroyed
|
||||
if self.find_main_tgo().dead_units:
|
||||
return ControlPointStatus.Damaged
|
||||
return ControlPointStatus.Functional
|
||||
|
||||
|
||||
class Carrier(NavalControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
@ -1113,6 +1092,10 @@ class OffMapSpawn(ControlPoint):
|
||||
def category(self) -> str:
|
||||
return "offmap"
|
||||
|
||||
@property
|
||||
def status(self) -> ControlPointStatus:
|
||||
return ControlPointStatus.Functional
|
||||
|
||||
|
||||
class Fob(ControlPoint):
|
||||
def __init__(self, name: str, at: Point, cp_id: int):
|
||||
@ -1176,3 +1159,7 @@ class Fob(ControlPoint):
|
||||
@property
|
||||
def category(self) -> str:
|
||||
return "fob"
|
||||
|
||||
@property
|
||||
def status(self) -> ControlPointStatus:
|
||||
return ControlPointStatus.Functional
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
from __future__ import annotations
|
||||
from game.scenery_group import SceneryGroup
|
||||
|
||||
import logging
|
||||
import pickle
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, List, Optional, Set
|
||||
from typing import Any, Dict, Iterable, List, Set
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import CAP, CAS, PinpointStrike
|
||||
@ -14,7 +13,8 @@ from dcs.vehicles import AirDefence
|
||||
|
||||
from game import Game, db
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import Carrier, Lha, LocationType, PointWithHeading
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater import Carrier, Lha, PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
@ -39,8 +39,8 @@ from gen.fleet.ship_group_generator import (
|
||||
)
|
||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||
from gen.sam.sam_group_generator import generate_anti_air_group
|
||||
from gen.sam.ewr_group_generator import generate_ewr_group
|
||||
from gen.sam.sam_group_generator import generate_anti_air_group
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
@ -145,24 +145,6 @@ class GameGenerator:
|
||||
cp.captured = True
|
||||
|
||||
|
||||
class LocationFinder:
|
||||
def __init__(self, control_point: ControlPoint) -> None:
|
||||
self.control_point = control_point
|
||||
|
||||
def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
|
||||
position = self.control_point.preset_locations.random_for(location_type)
|
||||
if position is not None:
|
||||
logging.warning(
|
||||
f"Campaign relies on random generation of %s at %s. Support for random "
|
||||
"objectives will be removed soon.",
|
||||
location_type.value,
|
||||
self.control_point,
|
||||
)
|
||||
return position
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(
|
||||
self,
|
||||
@ -173,7 +155,6 @@ class ControlPointGroundObjectGenerator:
|
||||
self.game = game
|
||||
self.generator_settings = generator_settings
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
@ -203,19 +184,9 @@ class ControlPointGroundObjectGenerator:
|
||||
if not self.control_point.captured and skip_enemy_navy:
|
||||
return
|
||||
|
||||
self.generate_required_ships()
|
||||
for _ in range(self.faction.navy_group_count):
|
||||
self.generate_ship()
|
||||
|
||||
def generate_required_ships(self) -> None:
|
||||
for position in self.control_point.preset_locations.required_ships:
|
||||
for position in self.control_point.preset_locations.ships:
|
||||
self.generate_ship_at(position)
|
||||
|
||||
def generate_ship(self) -> None:
|
||||
point = self.location_finder.location_for(LocationType.Ship)
|
||||
if point is not None:
|
||||
self.generate_ship_at(point)
|
||||
|
||||
def generate_ship_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
@ -289,159 +260,6 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
return True
|
||||
|
||||
|
||||
class BaseDefenseGenerator:
|
||||
def __init__(self, game: Game, control_point: ControlPoint) -> None:
|
||||
self.game = game
|
||||
self.control_point = control_point
|
||||
self.location_finder = LocationFinder(control_point)
|
||||
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_name
|
||||
else:
|
||||
return self.game.enemy_name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
return db.FACTIONS[self.faction_name]
|
||||
|
||||
def generate(self) -> None:
|
||||
self.generate_ewr()
|
||||
self.generate_garrison()
|
||||
self.generate_base_defenses()
|
||||
|
||||
def generate_ewr(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.BaseEwr)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = EwrGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
True,
|
||||
)
|
||||
|
||||
group = generate_ewr_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
logging.error(f"Could not generate EWR at {self.control_point}")
|
||||
return
|
||||
|
||||
g.groups = [group]
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_base_defenses(self) -> None:
|
||||
# First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD,
|
||||
# and a 1/6 chance of a garrison.
|
||||
#
|
||||
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
|
||||
# being a garrison.
|
||||
for i in range(random.randint(2, 5)):
|
||||
if i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_sam()
|
||||
elif random.randint(0, 2) == 1:
|
||||
self.generate_shorad()
|
||||
else:
|
||||
self.generate_garrison()
|
||||
|
||||
def generate_garrison(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Garrison)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = VehicleGroupGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
for_airbase=True,
|
||||
)
|
||||
|
||||
group = generate_armor_group(self.faction_name, self.game, g)
|
||||
if group is None:
|
||||
logging.error(f"Could not generate garrison at {self.control_point}")
|
||||
return
|
||||
g.groups.append(group)
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_sam(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.BaseAirDefense)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
for_airbase=True,
|
||||
)
|
||||
|
||||
groups = generate_anti_air_group(self.game, g, self.faction)
|
||||
if not groups:
|
||||
logging.error(f"Could not generate SAM at {self.control_point}")
|
||||
return
|
||||
g.groups = groups
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
def generate_shorad(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.BaseAirDefense)
|
||||
if position is None:
|
||||
return
|
||||
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
for_airbase=True,
|
||||
)
|
||||
|
||||
groups = generate_anti_air_group(
|
||||
self.game,
|
||||
g,
|
||||
self.faction,
|
||||
ranges=[{AirDefenseRange.Short, AirDefenseRange.AAA}],
|
||||
)
|
||||
if not groups:
|
||||
logging.error(f"Could not generate SHORAD group at {self.control_point}")
|
||||
return
|
||||
g.groups = groups
|
||||
self.control_point.base_defenses.append(g)
|
||||
|
||||
|
||||
class FobDefenseGenerator(BaseDefenseGenerator):
|
||||
def generate(self) -> None:
|
||||
self.generate_garrison()
|
||||
self.generate_fob_defenses()
|
||||
|
||||
def generate_fob_defenses(self):
|
||||
# First group has a 1/2 chance of being a SHORAD,
|
||||
# and a 1/2 chance of a garrison.
|
||||
#
|
||||
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
|
||||
# being a garrison.
|
||||
for i in range(random.randint(2, 5)):
|
||||
if i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_shorad()
|
||||
elif i == 0 and random.randint(0, 1) == 0:
|
||||
self.generate_garrison()
|
||||
elif random.randint(0, 2) == 1:
|
||||
self.generate_shorad()
|
||||
else:
|
||||
self.generate_garrison()
|
||||
|
||||
|
||||
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
@ -457,16 +275,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
if not super().generate():
|
||||
return False
|
||||
|
||||
BaseDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_ground_points()
|
||||
|
||||
return True
|
||||
|
||||
def generate_ground_points(self) -> None:
|
||||
"""Generate ground objects and AA sites for the control point."""
|
||||
self.generate_armor_groups()
|
||||
skip_sams = self.generate_required_aa()
|
||||
skip_ewrs = self.generate_required_ewr()
|
||||
self.generate_aa()
|
||||
self.generate_ewrs()
|
||||
self.generate_scenery_sites()
|
||||
self.generate_strike_targets()
|
||||
self.generate_offshore_strike_targets()
|
||||
@ -475,35 +291,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
self.generate_required_missile_sites()
|
||||
|
||||
if self.faction.coastal_defenses:
|
||||
self.generate_coastal_sites()
|
||||
self.generate_required_coastal_sites()
|
||||
|
||||
if self.control_point.is_global:
|
||||
return
|
||||
|
||||
# Always generate at least one AA point.
|
||||
self.generate_aa_site()
|
||||
|
||||
# And between 2 and 7 other objectives.
|
||||
amount = random.randrange(2, 7)
|
||||
for i in range(amount):
|
||||
# 1 in 4 additional objectives are AA.
|
||||
if random.randint(0, 3) == 0:
|
||||
if skip_sams > 0:
|
||||
skip_sams -= 1
|
||||
else:
|
||||
self.generate_aa_site()
|
||||
# 1 in 4 additional objectives are EWR.
|
||||
elif random.randint(0, 3) == 0:
|
||||
if skip_ewrs > 0:
|
||||
skip_ewrs -= 1
|
||||
else:
|
||||
self.generate_ewr_site()
|
||||
else:
|
||||
self.generate_ground_point()
|
||||
|
||||
def generate_armor_groups(self) -> None:
|
||||
for position in self.control_point.preset_locations.armor_groups:
|
||||
@ -517,7 +307,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
for_airbase=False,
|
||||
)
|
||||
|
||||
group = generate_armor_group(self.faction_name, self.game, g)
|
||||
@ -531,14 +320,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
g.groups = [group]
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_required_aa(self) -> int:
|
||||
"""Generates the AA sites that are required by the campaign.
|
||||
|
||||
Returns:
|
||||
The number of AA sites that were generated.
|
||||
"""
|
||||
def generate_aa(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.required_long_range_sams:
|
||||
for position in presets.long_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[
|
||||
@ -548,7 +332,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
{AirDefenseRange.AAA},
|
||||
],
|
||||
)
|
||||
for position in presets.required_medium_range_sams:
|
||||
for position in presets.medium_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[
|
||||
@ -557,52 +341,21 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
{AirDefenseRange.AAA},
|
||||
],
|
||||
)
|
||||
for position in presets.required_short_range_sams:
|
||||
for position in presets.short_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}],
|
||||
)
|
||||
for position in presets.required_aaa:
|
||||
for position in presets.aaa:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[{AirDefenseRange.AAA}],
|
||||
)
|
||||
return (
|
||||
len(presets.required_long_range_sams)
|
||||
+ len(presets.required_medium_range_sams)
|
||||
+ len(presets.required_short_range_sams)
|
||||
+ len(presets.required_aaa)
|
||||
)
|
||||
|
||||
def generate_required_ewr(self) -> int:
|
||||
"""Generates the EWR sites that are required by the campaign.
|
||||
|
||||
Returns:
|
||||
The number of EWR sites that were generated.
|
||||
"""
|
||||
def generate_ewrs(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.required_ewrs:
|
||||
for position in presets.ewrs:
|
||||
self.generate_ewr_at(position)
|
||||
return len(presets.required_ewrs)
|
||||
|
||||
def generate_ground_point(self) -> None:
|
||||
try:
|
||||
category = random.choice(self.faction.building_set)
|
||||
except IndexError:
|
||||
logging.exception("Faction has no buildings defined")
|
||||
return
|
||||
|
||||
if category == "oil":
|
||||
location_type = LocationType.OffshoreStrikeTarget
|
||||
else:
|
||||
location_type = LocationType.StrikeTarget
|
||||
|
||||
# Pick from preset locations
|
||||
point = self.location_finder.location_for(location_type)
|
||||
if point is None:
|
||||
return
|
||||
|
||||
self.generate_strike_target_at(category, point)
|
||||
|
||||
def generate_strike_target_at(self, category: str, position: Point) -> None:
|
||||
|
||||
@ -635,7 +388,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.generate_strike_target_at(category="ammo", position=position)
|
||||
|
||||
def generate_factories(self) -> None:
|
||||
"""Generates the factories that are required by the campaign."""
|
||||
for position in self.control_point.preset_locations.factories:
|
||||
self.generate_factory_at(position)
|
||||
|
||||
@ -653,19 +405,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_aa_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Sam)
|
||||
if position is None:
|
||||
return
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[
|
||||
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
|
||||
{AirDefenseRange.Long, AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
],
|
||||
)
|
||||
|
||||
def generate_aa_at(
|
||||
self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
|
||||
) -> None:
|
||||
@ -676,7 +415,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
for_airbase=False,
|
||||
)
|
||||
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||
if not groups:
|
||||
@ -689,12 +427,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
g.groups = groups
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_ewr_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Ewr)
|
||||
if position is None:
|
||||
return
|
||||
self.generate_ewr_at(position)
|
||||
|
||||
def generate_ewr_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
@ -703,7 +435,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
for_airbase=False,
|
||||
)
|
||||
group = generate_ewr_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
@ -750,18 +481,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
|
||||
return
|
||||
|
||||
def generate_required_missile_sites(self) -> None:
|
||||
for position in self.control_point.preset_locations.required_missile_sites:
|
||||
self.generate_missile_site_at(position)
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
for i in range(self.faction.missiles_group_count):
|
||||
self.generate_missile_site()
|
||||
|
||||
def generate_missile_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.MissileSite)
|
||||
if position is not None:
|
||||
return self.generate_missile_site_at(position)
|
||||
for position in self.control_point.preset_locations.missile_sites:
|
||||
self.generate_missile_site_at(position)
|
||||
|
||||
def generate_missile_site_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
@ -776,17 +498,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.control_point.connected_objectives.append(g)
|
||||
return
|
||||
|
||||
def generate_required_coastal_sites(self) -> None:
|
||||
for position in self.control_point.preset_locations.required_coastal_defenses:
|
||||
self.generate_coastal_site_at(position)
|
||||
|
||||
def generate_coastal_sites(self) -> None:
|
||||
for i in range(self.faction.coastal_group_count):
|
||||
self.generate_coastal_site()
|
||||
|
||||
def generate_coastal_site(self) -> None:
|
||||
position = self.location_finder.location_for(LocationType.Coastal)
|
||||
if position is not None:
|
||||
for position in self.control_point.preset_locations.coastal_defenses:
|
||||
self.generate_coastal_site_at(position)
|
||||
|
||||
def generate_coastal_site_at(self, position: PointWithHeading) -> None:
|
||||
@ -807,46 +520,39 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
return
|
||||
|
||||
def generate_strike_targets(self) -> None:
|
||||
"""Generates the strike targets that are required by the campaign."""
|
||||
building_set = list(set(self.faction.building_set) - {"oil"})
|
||||
if not building_set:
|
||||
logging.error("Faction has no buildings defined")
|
||||
return
|
||||
for position in self.control_point.preset_locations.required_strike_locations:
|
||||
for position in self.control_point.preset_locations.strike_locations:
|
||||
category = random.choice(building_set)
|
||||
self.generate_strike_target_at(category, position)
|
||||
|
||||
def generate_offshore_strike_targets(self) -> None:
|
||||
"""Generates the offshore strike targets that are required by the campaign."""
|
||||
if "oil" not in self.faction.building_set:
|
||||
logging.error("Faction does not support offshore strike targets")
|
||||
return
|
||||
for (
|
||||
position
|
||||
) in self.control_point.preset_locations.required_offshore_strike_locations:
|
||||
for position in self.control_point.preset_locations.offshore_strike_locations:
|
||||
self.generate_strike_target_at("oil", position)
|
||||
|
||||
|
||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
self.generate_fob()
|
||||
FobDefenseGenerator(self.game, self.control_point).generate()
|
||||
self.generate_armor_groups()
|
||||
self.generate_factories()
|
||||
self.generate_ammunition_depots()
|
||||
self.generate_required_aa()
|
||||
self.generate_required_ewr()
|
||||
self.generate_aa()
|
||||
self.generate_ewrs()
|
||||
self.generate_scenery_sites()
|
||||
self.generate_strike_targets()
|
||||
self.generate_offshore_strike_targets()
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
self.generate_required_missile_sites()
|
||||
|
||||
if self.faction.coastal_defenses:
|
||||
self.generate_coastal_sites()
|
||||
self.generate_required_coastal_sites()
|
||||
|
||||
return True
|
||||
|
||||
@ -873,7 +579,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
unit["heading"],
|
||||
self.control_point,
|
||||
unit["type"],
|
||||
airbase_group=True,
|
||||
is_fob_structure=True,
|
||||
)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
|
||||
@ -12,7 +12,6 @@ from dcs.unittype import VehicleType
|
||||
|
||||
from .. import db
|
||||
from ..data.radar_db import (
|
||||
UNITS_WITH_RADAR,
|
||||
TRACK_RADARS,
|
||||
TELARS,
|
||||
LAUNCHER_TRACKER_PAIRS,
|
||||
@ -58,7 +57,6 @@ class TheaterGroundObject(MissionTarget):
|
||||
heading: int,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
airbase_group: bool,
|
||||
sea_object: bool,
|
||||
) -> None:
|
||||
super().__init__(name, position)
|
||||
@ -67,7 +65,6 @@ class TheaterGroundObject(MissionTarget):
|
||||
self.heading = heading
|
||||
self.control_point = control_point
|
||||
self.dcs_identifier = dcs_identifier
|
||||
self.airbase_group = airbase_group
|
||||
self.sea_object = sea_object
|
||||
self.groups: List[Group] = []
|
||||
|
||||
@ -193,6 +190,21 @@ class TheaterGroundObject(MissionTarget):
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
return self.units
|
||||
|
||||
@property
|
||||
def mark_locations(self) -> Iterator[Point]:
|
||||
yield self.position
|
||||
|
||||
def clear(self) -> None:
|
||||
self.groups = []
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
@ -205,7 +217,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
heading: int,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
airbase_group=False,
|
||||
is_fob_structure=False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
@ -215,9 +227,9 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
airbase_group=airbase_group,
|
||||
sea_object=False,
|
||||
)
|
||||
self.is_fob_structure = is_fob_structure
|
||||
self.object_id = object_id
|
||||
# Other TGOs track deadness based on the number of alive units, but
|
||||
# buildings don't have groups assigned to the TGO.
|
||||
@ -250,6 +262,23 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
return list(self.iter_building_group())
|
||||
|
||||
@property
|
||||
def mark_locations(self) -> Iterator[Point]:
|
||||
for building in self.iter_building_group():
|
||||
yield building.position
|
||||
|
||||
@property
|
||||
def is_control_point(self) -> bool:
|
||||
return self.is_fob_structure
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class SceneryGroundObject(BuildingGroundObject):
|
||||
def __init__(
|
||||
@ -272,7 +301,7 @@ class SceneryGroundObject(BuildingGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier=dcs_identifier,
|
||||
airbase_group=False,
|
||||
is_fob_structure=False,
|
||||
)
|
||||
self.zone = zone
|
||||
try:
|
||||
@ -305,7 +334,7 @@ class FactoryGroundObject(BuildingGroundObject):
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier="Workshop A",
|
||||
airbase_group=False,
|
||||
is_fob_structure=False,
|
||||
)
|
||||
|
||||
|
||||
@ -321,6 +350,14 @@ class NavalGroundObject(TheaterGroundObject):
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class GenericCarrierGroundObject(NavalGroundObject):
|
||||
@property
|
||||
@ -339,7 +376,6 @@ class CarrierGroundObject(GenericCarrierGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="CARRIER",
|
||||
airbase_group=True,
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@ -361,7 +397,6 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="LHA",
|
||||
airbase_group=True,
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
@ -384,10 +419,17 @@ class MissileSiteGroundObject(TheaterGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
@ -406,26 +448,28 @@ class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
heading=heading,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
class BaseDefenseGroundObject(TheaterGroundObject):
|
||||
"""Base type for all base defenses."""
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
# TODO: Differentiate types.
|
||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
||||
# be split into their own types.
|
||||
class SamGroundObject(BaseDefenseGroundObject):
|
||||
class SamGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
for_airbase: bool,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
@ -435,7 +479,6 @@ class SamGroundObject(BaseDefenseGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False,
|
||||
)
|
||||
# Set by the SAM unit generator if the generated group is compatible
|
||||
@ -495,15 +538,22 @@ class SamGroundObject(BaseDefenseGroundObject):
|
||||
else:
|
||||
return max(max_tel_range, max_telar_range, max_non_radar)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class VehicleGroupGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
for_airbase: bool,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
@ -513,19 +563,25 @@ class VehicleGroupGroundObject(BaseDefenseGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
class EwrGroundObject(BaseDefenseGroundObject):
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class EwrGroundObject(TheaterGroundObject):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
for_airbase: bool,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
@ -535,7 +591,6 @@ class EwrGroundObject(BaseDefenseGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="EWR",
|
||||
airbase_group=for_airbase,
|
||||
sea_object=False,
|
||||
)
|
||||
|
||||
@ -555,6 +610,14 @@ class EwrGroundObject(BaseDefenseGroundObject):
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def capturable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchasable(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class ShipGroundObject(NavalGroundObject):
|
||||
def __init__(
|
||||
@ -568,7 +631,6 @@ class ShipGroundObject(NavalGroundObject):
|
||||
heading=0,
|
||||
control_point=control_point,
|
||||
dcs_identifier="AA",
|
||||
airbase_group=False,
|
||||
sea_object=True,
|
||||
)
|
||||
|
||||
|
||||
@ -40,6 +40,10 @@ class ThreatZones:
|
||||
)
|
||||
return DcsPoint(boundary.x, boundary.y)
|
||||
|
||||
def distance_to_threat(self, point: DcsPoint) -> Distance:
|
||||
boundary = self.closest_boundary(point)
|
||||
return meters(boundary.distance_to_point(point))
|
||||
|
||||
@singledispatchmethod
|
||||
def threatened(self, position) -> bool:
|
||||
raise NotImplementedError
|
||||
@ -124,7 +128,7 @@ class ThreatZones:
|
||||
cls, location: ControlPoint, max_distance: Distance
|
||||
) -> Optional[ControlPoint]:
|
||||
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
for airfield in airfields.airfields_within(max_distance):
|
||||
for airfield in airfields.all_airfields_within(max_distance):
|
||||
if airfield.captured != location.captured:
|
||||
return airfield
|
||||
return None
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from functools import singledispatchmethod
|
||||
@ -89,10 +90,9 @@ class TransferOrder:
|
||||
self.units.clear()
|
||||
|
||||
def kill_unit(self, unit_type: Type[VehicleType]) -> None:
|
||||
if unit_type in self.units:
|
||||
self.units[unit_type] -= 1
|
||||
return
|
||||
raise KeyError
|
||||
if unit_type not in self.units or not self.units[unit_type]:
|
||||
raise KeyError(f"{self.destination} has no {unit_type} remaining")
|
||||
self.units[unit_type] -= 1
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
@ -238,7 +238,7 @@ class AirliftPlanner:
|
||||
for s in self.game.air_wing_for(self.for_player).squadrons_for(
|
||||
unit_type
|
||||
)
|
||||
if FlightType.TRANSPORT in s.mission_types
|
||||
if FlightType.TRANSPORT in s.auto_assignable_mission_types
|
||||
]
|
||||
if not squadrons:
|
||||
continue
|
||||
@ -254,11 +254,13 @@ class AirliftPlanner:
|
||||
self, squadron: Squadron, inventory: ControlPointAircraftInventory
|
||||
) -> int:
|
||||
available = inventory.available(squadron.aircraft)
|
||||
# 4 is the max flight size in DCS.
|
||||
flight_size = min(self.transfer.size, available, 4)
|
||||
capacity_each = 1 if squadron.aircraft.helicopter else 2
|
||||
required = math.ceil(self.transfer.size / capacity_each)
|
||||
flight_size = min(required, available, squadron.aircraft.group_size_max)
|
||||
capacity = flight_size * capacity_each
|
||||
|
||||
if flight_size < self.transfer.size:
|
||||
transfer = self.game.transfers.split_transfer(self.transfer, flight_size)
|
||||
if capacity < self.transfer.size:
|
||||
transfer = self.game.transfers.split_transfer(self.transfer, capacity)
|
||||
else:
|
||||
transfer = self.transfer
|
||||
|
||||
@ -530,33 +532,35 @@ class PendingTransfers:
|
||||
return new_transfer
|
||||
|
||||
@singledispatchmethod
|
||||
def cancel_transport(self, transfer: TransferOrder, transport) -> None:
|
||||
def cancel_transport(self, transport, transfer: TransferOrder) -> None:
|
||||
pass
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_air(
|
||||
self, _transfer: TransferOrder, transport: Airlift
|
||||
self, transport: Airlift, _transfer: TransferOrder
|
||||
) -> None:
|
||||
flight = transport.flight
|
||||
flight.package.remove_flight(flight)
|
||||
if not flight.package.flights:
|
||||
self.game.ato_for(transport.player_owned).remove_package(flight.package)
|
||||
self.game.aircraft_inventory.return_from_flight(flight)
|
||||
flight.clear_roster()
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_convoy(
|
||||
self, transfer: TransferOrder, transport: Convoy
|
||||
self, transport: Convoy, transfer: TransferOrder
|
||||
) -> None:
|
||||
self.convoys.remove(transport, transfer)
|
||||
|
||||
@cancel_transport.register
|
||||
def _cancel_transport_cargo_ship(
|
||||
self, transfer: TransferOrder, transport: CargoShip
|
||||
self, transport: CargoShip, transfer: TransferOrder
|
||||
) -> None:
|
||||
self.cargo_ships.remove(transport, transfer)
|
||||
|
||||
def cancel_transfer(self, transfer: TransferOrder) -> None:
|
||||
if transfer.transport is not None:
|
||||
self.cancel_transport(transfer, transfer.transport)
|
||||
self.cancel_transport(transfer.transport, transfer)
|
||||
self.pending_transfers.remove(transfer)
|
||||
transfer.origin.base.commision_units(transfer.units)
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
"""Maps generated units back to their Liberation types."""
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, Type
|
||||
|
||||
@ -40,8 +42,8 @@ class ConvoyUnit:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirliftUnit:
|
||||
unit_type: Type[VehicleType]
|
||||
class AirliftUnits:
|
||||
cargo: tuple[Type[VehicleType], ...]
|
||||
transfer: TransferOrder
|
||||
|
||||
|
||||
@ -59,10 +61,10 @@ class UnitMap:
|
||||
self.buildings: Dict[str, Building] = {}
|
||||
self.convoys: Dict[str, ConvoyUnit] = {}
|
||||
self.cargo_ships: Dict[str, CargoShip] = {}
|
||||
self.airlifts: Dict[str, AirliftUnit] = {}
|
||||
self.airlifts: Dict[str, AirliftUnits] = {}
|
||||
|
||||
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
for pilot, unit in zip(flight.pilots, group.units):
|
||||
for pilot, unit in zip(flight.roster.pilots, group.units):
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(unit.name)
|
||||
@ -177,15 +179,26 @@ class UnitMap:
|
||||
return self.cargo_ships.get(name, None)
|
||||
|
||||
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
|
||||
for transport, cargo_type in zip(group.units, transfer.iter_units()):
|
||||
capacity_each = math.ceil(transfer.size / len(group.units))
|
||||
for idx, transport in enumerate(group.units):
|
||||
# Slice the units in groups based on the capacity of each unit. Cargo is
|
||||
# assigned arbitrarily to units in the order of the group. The last unit in
|
||||
# the group will receive a partial load if there is not enough cargo to fill
|
||||
# every transport.
|
||||
base_idx = idx * capacity_each
|
||||
cargo = tuple(
|
||||
itertools.islice(
|
||||
transfer.iter_units(), base_idx, base_idx + capacity_each
|
||||
)
|
||||
)
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
name = str(transport.name)
|
||||
if name in self.airlifts:
|
||||
raise RuntimeError(f"Duplicate airlift unit: {name}")
|
||||
self.airlifts[name] = AirliftUnit(cargo_type, transfer)
|
||||
self.airlifts[name] = AirliftUnits(cargo, transfer)
|
||||
|
||||
def airlift_unit(self, name: str) -> Optional[AirliftUnit]:
|
||||
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
|
||||
return self.airlifts.get(name, None)
|
||||
|
||||
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
|
||||
|
||||
@ -2,7 +2,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["3.0"]
|
||||
components = ["4.0"]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
@ -75,10 +75,16 @@ VERSION = _build_version_string()
|
||||
#: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
#:
|
||||
#: 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)
|
||||
#: * 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.
|
||||
#:
|
||||
#: Version 6.0
|
||||
#: * Random objective generation no is longer supported. Fixed objective locations were
|
||||
#: added in 4.1.
|
||||
CAMPAIGN_FORMAT_VERSION = (6, 0)
|
||||
|
||||
@ -803,7 +803,7 @@ class AircraftConflictGenerator:
|
||||
self._setup_payload(flight, group)
|
||||
self._setup_livery(flight, group)
|
||||
|
||||
for unit, pilot in zip(group.units, flight.pilots):
|
||||
for unit, pilot in zip(group.units, flight.roster.pilots):
|
||||
player = pilot is not None and pilot.player
|
||||
self.set_skill(unit, pilot, blue=flight.departure.captured)
|
||||
# Do not generate player group with late activation.
|
||||
|
||||
@ -162,7 +162,7 @@ class AircraftAllocator:
|
||||
self, flight: ProposedFlight, task: FlightType
|
||||
) -> Optional[Tuple[ControlPoint, Squadron]]:
|
||||
types = aircraft_for_task(task)
|
||||
airfields_in_range = self.closest_airfields.airfields_within(
|
||||
airfields_in_range = self.closest_airfields.operational_airfields_within(
|
||||
flight.max_distance
|
||||
)
|
||||
|
||||
@ -180,7 +180,7 @@ class AircraftAllocator:
|
||||
# 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:
|
||||
if task not in squadron.auto_assignable_mission_types:
|
||||
continue
|
||||
if len(squadron.available_pilots) >= flight.num_aircraft:
|
||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||
@ -258,7 +258,9 @@ class PackageBuilder:
|
||||
self, aircraft: Type[FlyingType], arrival: ControlPoint
|
||||
) -> Optional[ControlPoint]:
|
||||
divert_limit = nautical_miles(150)
|
||||
for airfield in self.closest_airfields.airfields_within(divert_limit):
|
||||
for airfield in self.closest_airfields.operational_airfields_within(
|
||||
divert_limit
|
||||
):
|
||||
if airfield.captured != self.is_player:
|
||||
continue
|
||||
if airfield == arrival:
|
||||
@ -433,7 +435,7 @@ class ObjectiveFinder:
|
||||
|
||||
is_building = isinstance(ground_object, BuildingGroundObject)
|
||||
is_fob = isinstance(enemy_cp, Fob)
|
||||
if is_building and is_fob and ground_object.airbase_group:
|
||||
if is_building and is_fob and ground_object.is_control_point:
|
||||
# This is the FOB structure itself. Can't be repaired or
|
||||
# targeted by the player, so shouldn't be targetable by the
|
||||
# AI.
|
||||
@ -467,8 +469,10 @@ class ObjectiveFinder:
|
||||
# Off-map spawn locations don't need protection.
|
||||
continue
|
||||
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||
airfields_in_threat_range = airfields_in_proximity.airfields_within(
|
||||
self.AIRFIELD_THREAT_RANGE
|
||||
airfields_in_threat_range = (
|
||||
airfields_in_proximity.operational_airfields_within(
|
||||
self.AIRFIELD_THREAT_RANGE
|
||||
)
|
||||
)
|
||||
for airfield in airfields_in_threat_range:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
@ -502,31 +506,23 @@ class ObjectiveFinder:
|
||||
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
|
||||
)
|
||||
|
||||
def farthest_friendly_control_point(self) -> Optional[ControlPoint]:
|
||||
"""
|
||||
Iterates over all friendly control points and find the one farthest away from the frontline
|
||||
BUT! prefer Cvs. Everybody likes CVs!
|
||||
"""
|
||||
from_frontline = 0
|
||||
cp = None
|
||||
first_friendly_cp = None
|
||||
def farthest_friendly_control_point(self) -> ControlPoint:
|
||||
"""Finds the friendly control point that is farthest from any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
for c in self.game.theater.controlpoints:
|
||||
if c.is_friendly(self.is_player):
|
||||
if first_friendly_cp is None:
|
||||
first_friendly_cp = c
|
||||
if c.is_carrier:
|
||||
return c
|
||||
if c.has_active_frontline:
|
||||
if c.distance_to(self.front_lines().__next__()) > from_frontline:
|
||||
from_frontline = c.distance_to(self.front_lines().__next__())
|
||||
cp = c
|
||||
farthest = None
|
||||
max_distance = meters(0)
|
||||
for cp in self.friendly_control_points():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
continue
|
||||
distance = threat_zones.distance_to_threat(cp.position)
|
||||
if distance > max_distance:
|
||||
farthest = cp
|
||||
max_distance = distance
|
||||
|
||||
# If no frontlines on the map, return the first friendly cp
|
||||
if cp is None:
|
||||
return first_friendly_cp
|
||||
else:
|
||||
return cp
|
||||
if farthest is None:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return farthest
|
||||
|
||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all enemy control points."""
|
||||
@ -608,7 +604,7 @@ class CoalitionMissionPlanner:
|
||||
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
|
||||
if (
|
||||
squadron.aircraft in all_compatible
|
||||
and mission_type in squadron.mission_types
|
||||
and mission_type in squadron.auto_assignable_mission_types
|
||||
):
|
||||
return True
|
||||
return False
|
||||
@ -624,15 +620,13 @@ class CoalitionMissionPlanner:
|
||||
eliminated this turn.
|
||||
"""
|
||||
|
||||
# Find farthest, friendly CP for AEWC
|
||||
cp = self.objective_finder.farthest_friendly_control_point()
|
||||
if cp is not None:
|
||||
yield ProposedMission(
|
||||
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 farthest, friendly CP for AEWC.
|
||||
yield ProposedMission(
|
||||
self.objective_finder.farthest_friendly_control_point(),
|
||||
[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.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
@ -1012,7 +1006,7 @@ class CoalitionMissionPlanner:
|
||||
interval = (latest - earliest) // count
|
||||
for time in range(earliest, latest, interval):
|
||||
error = random.randint(-margin, margin)
|
||||
yield timedelta(minutes=max(0, time + error))
|
||||
yield timedelta(seconds=max(0, time + error))
|
||||
|
||||
dca_types = {
|
||||
FlightType.BARCAP,
|
||||
@ -1026,11 +1020,11 @@ class CoalitionMissionPlanner:
|
||||
|
||||
start_time = start_time_generator(
|
||||
count=len(non_dca_packages),
|
||||
earliest=5,
|
||||
earliest=5 * 60,
|
||||
latest=int(
|
||||
self.game.settings.desired_player_mission_duration.total_seconds() / 60
|
||||
self.game.settings.desired_player_mission_duration.total_seconds()
|
||||
),
|
||||
margin=5,
|
||||
margin=5 * 60,
|
||||
)
|
||||
for package in self.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
|
||||
@ -31,17 +31,35 @@ class ClosestAirfields:
|
||||
if c.runway_is_operational() or c.has_helipads
|
||||
)
|
||||
|
||||
def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
|
||||
def _airfields_within(
|
||||
self, distance: Distance, operational: bool
|
||||
) -> Iterator[ControlPoint]:
|
||||
airfields = (
|
||||
self.operational_airfields if operational else self.closest_airfields
|
||||
)
|
||||
for cp in airfields:
|
||||
if cp.distance_to(self.target) < distance.meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
|
||||
def operational_airfields_within(
|
||||
self, distance: Distance
|
||||
) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
for cp in self.closest_airfields:
|
||||
if cp.distance_to(self.target) < distance.meters:
|
||||
yield cp
|
||||
else:
|
||||
break
|
||||
return self._airfields_within(distance, operational=True)
|
||||
|
||||
def all_airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all airfields within the given range of the target.
|
||||
|
||||
Note that this iterates over *all* airfields, not just friendly
|
||||
airfields.
|
||||
"""
|
||||
return self._airfields_within(distance, operational=False)
|
||||
|
||||
|
||||
class ObjectiveDistanceCache:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING, Type, Union
|
||||
@ -202,6 +203,49 @@ class FlightWaypoint:
|
||||
return waypoint
|
||||
|
||||
|
||||
class FlightRoster:
|
||||
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
|
||||
self.squadron = squadron
|
||||
self.pilots: list[Optional[Pilot]] = []
|
||||
self.resize(initial_size)
|
||||
|
||||
@property
|
||||
def max_size(self) -> int:
|
||||
return len(self.pilots)
|
||||
|
||||
@property
|
||||
def player_count(self) -> int:
|
||||
return len([p for p in self.pilots if p is not None and p.player])
|
||||
|
||||
@property
|
||||
def missing_pilots(self) -> int:
|
||||
return len([p for p in self.pilots if p is None])
|
||||
|
||||
def resize(self, new_size: int) -> None:
|
||||
if self.max_size > 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.max_size)
|
||||
]
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def clear(self) -> None:
|
||||
self.squadron.return_pilots([p for p in self.pilots if p is not None])
|
||||
|
||||
|
||||
class Flight:
|
||||
def __init__(
|
||||
self,
|
||||
@ -216,11 +260,15 @@ class Flight:
|
||||
divert: Optional[ControlPoint],
|
||||
custom_name: Optional[str] = None,
|
||||
cargo: Optional[TransferOrder] = None,
|
||||
roster: Optional[FlightRoster] = None,
|
||||
) -> None:
|
||||
self.package = package
|
||||
self.country = country
|
||||
self.squadron = squadron
|
||||
self.pilots = [squadron.claim_available_pilot() for _ in range(count)]
|
||||
if roster is None:
|
||||
self.roster = FlightRoster(self.squadron, initial_size=count)
|
||||
else:
|
||||
self.roster = roster
|
||||
self.departure = departure
|
||||
self.arrival = arrival
|
||||
self.divert = divert
|
||||
@ -246,11 +294,11 @@ class Flight:
|
||||
|
||||
@property
|
||||
def count(self) -> int:
|
||||
return len(self.pilots)
|
||||
return self.roster.max_size
|
||||
|
||||
@property
|
||||
def client_count(self) -> int:
|
||||
return len([p for p in self.pilots if p is not None and p.player])
|
||||
return self.roster.player_count
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Type[FlyingType]:
|
||||
@ -265,32 +313,17 @@ class Flight:
|
||||
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)
|
||||
]
|
||||
)
|
||||
self.roster.resize(new_size)
|
||||
|
||||
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
|
||||
self.roster.set_pilot(index, pilot)
|
||||
|
||||
@property
|
||||
def missing_pilots(self) -> int:
|
||||
return len([p for p in self.pilots if p is None])
|
||||
return self.roster.missing_pilots
|
||||
|
||||
def clear_roster(self) -> None:
|
||||
self.squadron.return_pilots([p for p in self.pilots if p is not None])
|
||||
self.roster.clear()
|
||||
|
||||
def __repr__(self):
|
||||
name = db.unit_type_name(self.unit_type)
|
||||
|
||||
@ -1057,7 +1057,7 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
start = self.aewc_orbit(location)
|
||||
orbit_location = self.aewc_orbit(location)
|
||||
|
||||
# As high as possible to maximize detection and on-station time.
|
||||
if flight.unit_type == E_2C:
|
||||
@ -1072,22 +1072,22 @@ class FlightPlanBuilder:
|
||||
patrol_alt = feet(25000)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start = builder.orbit(start, patrol_alt)
|
||||
orbit_location = builder.orbit(orbit_location, patrol_alt)
|
||||
|
||||
return AwacsFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, start.position, patrol_alt
|
||||
flight.departure.position, orbit_location.position, patrol_alt
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
start.position, flight.arrival.position, patrol_alt
|
||||
orbit_location.position, flight.arrival.position, patrol_alt
|
||||
),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
hold=start,
|
||||
hold=orbit_location,
|
||||
hold_duration=timedelta(hours=4),
|
||||
)
|
||||
|
||||
@ -1339,20 +1339,24 @@ class FlightPlanBuilder:
|
||||
return start, end
|
||||
|
||||
def aewc_orbit(self, location: MissionTarget) -> Point:
|
||||
# in threat zone
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
heading_to_threat_boundary = location.position.heading_between_point(
|
||||
closest_boundary
|
||||
)
|
||||
distance_to_threat = meters(
|
||||
location.position.distance_to_point(closest_boundary)
|
||||
)
|
||||
orbit_heading = heading_to_threat_boundary
|
||||
# Station 100nm outside the threat zone.
|
||||
threat_buffer = nautical_miles(100)
|
||||
if self.threat_zones.threatened(location.position):
|
||||
# Borderpoint
|
||||
closest_boundary = self.threat_zones.closest_boundary(location.position)
|
||||
|
||||
# Heading + Distance to border point
|
||||
heading = location.position.heading_between_point(closest_boundary)
|
||||
distance = location.position.distance_to_point(closest_boundary)
|
||||
|
||||
return location.position.point_from_heading(heading, distance)
|
||||
|
||||
# this Part is fine. No threat zone, just use our point
|
||||
orbit_distance = distance_to_threat + threat_buffer
|
||||
else:
|
||||
return location.position
|
||||
orbit_distance = distance_to_threat - threat_buffer
|
||||
|
||||
return location.position.point_from_heading(
|
||||
orbit_heading, orbit_distance.meters
|
||||
)
|
||||
|
||||
def racetrack_for_frontline(
|
||||
self, origin: Point, front_line: FrontLine
|
||||
@ -1807,7 +1811,7 @@ class FlightPlanBuilder:
|
||||
# We'll always have a package, but if this is being planned via the UI
|
||||
# it could be the first flight in the package.
|
||||
if not self.package.flights:
|
||||
raise RuntimeError(
|
||||
raise PlanningError(
|
||||
"Cannot determine source airfield for package with no flights"
|
||||
)
|
||||
|
||||
@ -1819,5 +1823,4 @@ class FlightPlanBuilder:
|
||||
for flight in self.package.flights:
|
||||
if flight.departure == airfield:
|
||||
return airfield
|
||||
|
||||
raise RuntimeError("Could not find any airfield assigned to this package")
|
||||
raise PlanningError("Could not find any airfield assigned to this package")
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import random
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
@ -5,7 +6,8 @@ from typing import Dict, List
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from gen.ground_forces.ai_ground_planner_db import *
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
MAX_COMBAT_GROUP_PER_CP = 10
|
||||
@ -91,37 +93,35 @@ class GroundPlanner:
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
|
||||
|
||||
# Create combat groups and assign them randomly to each enemy CP
|
||||
for key in self.cp.base.armor.keys():
|
||||
|
||||
role = None
|
||||
collection = None
|
||||
if key in TYPE_TANKS:
|
||||
for unit_type in self.cp.base.armor:
|
||||
if unit_type in GroundUnitClass.Tank:
|
||||
collection = self.tank_groups
|
||||
role = CombatGroupRole.TANK
|
||||
elif key in TYPE_APC:
|
||||
elif unit_type in GroundUnitClass.Apc:
|
||||
collection = self.apc_group
|
||||
role = CombatGroupRole.APC
|
||||
elif key in TYPE_ARTILLERY:
|
||||
elif unit_type in GroundUnitClass.Artillery:
|
||||
collection = self.art_group
|
||||
role = CombatGroupRole.ARTILLERY
|
||||
elif key in TYPE_IFV:
|
||||
elif unit_type in GroundUnitClass.Ifv:
|
||||
collection = self.ifv_group
|
||||
role = CombatGroupRole.IFV
|
||||
elif key in TYPE_LOGI:
|
||||
elif unit_type in GroundUnitClass.Logistics:
|
||||
collection = self.logi_groups
|
||||
role = CombatGroupRole.LOGI
|
||||
elif key in TYPE_ATGM:
|
||||
elif unit_type in GroundUnitClass.Atgm:
|
||||
collection = self.atgm_group
|
||||
role = CombatGroupRole.ATGM
|
||||
elif key in TYPE_SHORAD:
|
||||
elif unit_type in GroundUnitClass.Shorads:
|
||||
collection = self.shorad_groups
|
||||
role = CombatGroupRole.SHORAD
|
||||
else:
|
||||
print("Warning unit type not handled by ground generator")
|
||||
print(key)
|
||||
logging.warning(
|
||||
f"Unused front line vehicle at base {unit_type}: unknown unit class"
|
||||
)
|
||||
continue
|
||||
|
||||
available = self.cp.base.armor[key]
|
||||
available = self.cp.base.armor[unit_type]
|
||||
|
||||
if available > remaining_available_frontline_units:
|
||||
available = remaining_available_frontline_units
|
||||
@ -151,7 +151,7 @@ class GroundPlanner:
|
||||
group.assigned_enemy_cp = "__reserve__"
|
||||
|
||||
for i in range(n):
|
||||
group.units.append(key)
|
||||
group.units.append(unit_type)
|
||||
collection.append(group)
|
||||
|
||||
if remaining_available_frontline_units == 0:
|
||||
@ -161,7 +161,7 @@ class GroundPlanner:
|
||||
print("Ground Planner : ")
|
||||
print(self.cp.name)
|
||||
print("------------------")
|
||||
for key in self.units_per_cp.keys():
|
||||
print("For : #" + str(key))
|
||||
for group in self.units_per_cp[key]:
|
||||
for unit_type in self.units_per_cp.keys():
|
||||
print("For : #" + str(unit_type))
|
||||
for group in self.units_per_cp[unit_type]:
|
||||
print(str(group))
|
||||
|
||||
@ -1,189 +0,0 @@
|
||||
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
|
||||
|
||||
from pydcs_extensions.frenchpack import frenchpack
|
||||
|
||||
TYPE_TANKS = [
|
||||
Armor.MBT_T_55,
|
||||
Armor.MBT_T_72B,
|
||||
Armor.MBT_T_72B3,
|
||||
Armor.MBT_T_80U,
|
||||
Armor.MBT_T_90,
|
||||
Armor.MBT_Leopard_2A4,
|
||||
Armor.MBT_Leopard_2A4_Trs,
|
||||
Armor.MBT_Leopard_2A5,
|
||||
Armor.MBT_Leopard_2A6M,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.MBT_Challenger_II,
|
||||
Armor.MBT_Chieftain_Mk_3,
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.MBT_Merkava_IV,
|
||||
Armor.ZTZ_96B,
|
||||
Armor.LT_PT_76,
|
||||
# WW2
|
||||
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
|
||||
Armor.Tk_PzIV_H,
|
||||
Armor.HT_Pz_Kpfw_VI_Tiger_I,
|
||||
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
|
||||
Armor.Tk_M4_Sherman,
|
||||
Armor.MT_M4A4_Sherman_Firefly,
|
||||
Armor.SPG_StuG_IV,
|
||||
Armor.CT_Centaur_IV,
|
||||
Armor.CT_Cromwell_IV,
|
||||
Armor.HIT_Churchill_VII,
|
||||
Armor.LT_Mk_VII_Tetrarch,
|
||||
Armor.SPG_Sturmpanzer_IV_Brummbar,
|
||||
# Mods
|
||||
frenchpack.DIM__TOYOTA_BLUE,
|
||||
frenchpack.DIM__TOYOTA_GREEN,
|
||||
frenchpack.DIM__TOYOTA_DESERT,
|
||||
frenchpack.DIM__KAMIKAZE,
|
||||
frenchpack.AMX_10RCR,
|
||||
frenchpack.AMX_10RCR_SEPAR,
|
||||
frenchpack.AMX_30B2,
|
||||
frenchpack.Leclerc_Serie_XXI,
|
||||
]
|
||||
|
||||
TYPE_ATGM = [
|
||||
Armor.ATGM_HMMWV,
|
||||
Armor.ATGM_VAB_Mephisto,
|
||||
Armor.ATGM_Stryker,
|
||||
Armor.IFV_BMP_2,
|
||||
# WW2 (Tank Destroyers)
|
||||
Unarmed.Carrier_M30_Cargo,
|
||||
Armor.SPG_Jagdpanzer_IV,
|
||||
Armor.SPG_Jagdpanther_G1,
|
||||
Armor.SPG_M10_GMC,
|
||||
# Mods
|
||||
frenchpack.VBAE_CRAB_MMP,
|
||||
frenchpack.VAB_MEPHISTO,
|
||||
frenchpack.TRM_2000_PAMELA,
|
||||
]
|
||||
|
||||
TYPE_IFV = [
|
||||
Armor.IFV_BMP_3,
|
||||
Armor.IFV_BMP_2,
|
||||
Armor.IFV_BMP_1,
|
||||
Armor.IFV_Marder,
|
||||
Armor.IFV_Warrior,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.SPG_Stryker_MGS,
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_BMD_1,
|
||||
Armor.ZBD_04A,
|
||||
# WW2
|
||||
Armor.IFV_Sd_Kfz_234_2_Puma,
|
||||
Armor.Car_M8_Greyhound_Armored,
|
||||
Armor.Car_Daimler_Armored,
|
||||
# Mods
|
||||
frenchpack.ERC_90,
|
||||
frenchpack.VBAE_CRAB,
|
||||
frenchpack.VAB_T20_13,
|
||||
]
|
||||
|
||||
TYPE_APC = [
|
||||
Armor.Scout_HMMWV,
|
||||
Armor.IFV_M1126_Stryker_ICV,
|
||||
Armor.APC_M113,
|
||||
Armor.APC_BTR_80,
|
||||
Armor.IFV_BTR_82A,
|
||||
Armor.APC_MTLB,
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.Scout_Cobra,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
Armor.APC_AAV_7_Amphibious,
|
||||
Armor.APC_TPz_Fuchs,
|
||||
Armor.Scout_BRDM_2,
|
||||
Armor.APC_BTR_RD,
|
||||
Artillery.Grad_MRL_FDDM__FC,
|
||||
# WW2
|
||||
Armor.APC_M2A1_Halftrack,
|
||||
Armor.APC_Sd_Kfz_251_Halftrack,
|
||||
# Mods
|
||||
frenchpack.VAB__50,
|
||||
frenchpack.VBL__50,
|
||||
frenchpack.VBL_AANF1,
|
||||
]
|
||||
|
||||
TYPE_ARTILLERY = [
|
||||
Artillery.MLRS_9A52_Smerch_HE_300mm,
|
||||
Artillery.SPH_2S1_Gvozdika_122mm,
|
||||
Artillery.SPH_2S3_Akatsia_152mm,
|
||||
Artillery.MLRS_BM_21_Grad_122mm,
|
||||
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
|
||||
Artillery.SPH_M109_Paladin_155mm,
|
||||
Artillery.MLRS_M270_227mm,
|
||||
Artillery.SPM_2S9_Nona_120mm_M,
|
||||
Artillery.SPH_Dana_vz77_152mm,
|
||||
Artillery.SPH_T155_Firtina_155mm,
|
||||
Artillery.PLZ_05,
|
||||
Artillery.SPH_2S19_Msta_152mm,
|
||||
Artillery.MLRS_9A52_Smerch_CM_300mm,
|
||||
# WW2
|
||||
Artillery.SPG_M12_GMC_155mm,
|
||||
]
|
||||
|
||||
TYPE_LOGI = [
|
||||
Unarmed.Truck_M818_6x6,
|
||||
Unarmed.Truck_KAMAZ_43101,
|
||||
Unarmed.Truck_Ural_375,
|
||||
Unarmed.Truck_GAZ_66,
|
||||
Unarmed.Truck_GAZ_3307,
|
||||
Unarmed.Truck_GAZ_3308,
|
||||
Unarmed.Truck_Ural_4320_31_Arm_d,
|
||||
Unarmed.Truck_Ural_4320T,
|
||||
Unarmed.Truck_Opel_Blitz,
|
||||
Unarmed.LUV_Kubelwagen_82,
|
||||
Unarmed.Carrier_Sd_Kfz_7_Tractor,
|
||||
Unarmed.LUV_Kettenrad,
|
||||
Unarmed.Car_Willys_Jeep,
|
||||
Unarmed.LUV_Land_Rover_109,
|
||||
Unarmed.Truck_Land_Rover_101_FC,
|
||||
# Mods
|
||||
frenchpack.VBL,
|
||||
frenchpack.VAB,
|
||||
]
|
||||
|
||||
TYPE_INFANTRY = [
|
||||
Infantry.Insurgent_AK_74,
|
||||
Infantry.Infantry_AK_74,
|
||||
Infantry.Infantry_M1_Garand,
|
||||
Infantry.Infantry_Mauser_98,
|
||||
Infantry.Infantry_SMLE_No_4_Mk_1,
|
||||
Infantry.Infantry_M4_Georgia,
|
||||
Infantry.Infantry_AK_74_Rus,
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
Infantry.Infantry_M249,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Infantry_RPG,
|
||||
]
|
||||
|
||||
TYPE_SHORAD = [
|
||||
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
|
||||
AirDefence.SPAAA_ZSU_57_2,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
|
||||
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
|
||||
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
|
||||
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
|
||||
AirDefence.SAM_SA_15_Tor_Gauntlet,
|
||||
AirDefence.SAM_SA_19_Tunguska_Grison,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_Vulcan_M163,
|
||||
AirDefence.SAM_Linebacker___Bradley_M6,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
AirDefence.SAM_Avenger__Stinger,
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm,
|
||||
AirDefence.AAA_S_60_57mm,
|
||||
AirDefence.AAA_M1_37mm,
|
||||
AirDefence.AAA_QF_3_7,
|
||||
]
|
||||
@ -15,7 +15,7 @@ from dcs import Mission, Point, unitgroup
|
||||
from dcs.action import SceneryDestructionZone
|
||||
from dcs.country import Country
|
||||
from dcs.point import StaticPoint
|
||||
from dcs.statics import Fortification, fortification_map, warehouse_map, Warehouse
|
||||
from dcs.statics import Fortification, fortification_map, warehouse_map
|
||||
from dcs.task import (
|
||||
ActivateBeaconCommand,
|
||||
ActivateICLSCommand,
|
||||
@ -24,7 +24,7 @@ from dcs.task import (
|
||||
FireAtPoint,
|
||||
)
|
||||
from dcs.triggers import TriggerStart, TriggerZone
|
||||
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad, Static
|
||||
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unittype import StaticType, UnitType
|
||||
from dcs.vehicles import vehicle_map
|
||||
@ -76,8 +76,12 @@ class GenericGroundObjectGenerator:
|
||||
self.m = mission
|
||||
self.unit_map = unit_map
|
||||
|
||||
@property
|
||||
def culled(self) -> bool:
|
||||
return self.game.position_culled(self.ground_object.position)
|
||||
|
||||
def generate(self) -> None:
|
||||
if self.game.position_culled(self.ground_object.position):
|
||||
if self.culled:
|
||||
return
|
||||
|
||||
for group in self.ground_object.groups:
|
||||
@ -130,6 +134,12 @@ class GenericGroundObjectGenerator:
|
||||
|
||||
|
||||
class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
@property
|
||||
def culled(self) -> bool:
|
||||
# Don't cull missile sites - their range is long enough to make them easily
|
||||
# culled despite being a threat.
|
||||
return False
|
||||
|
||||
def generate(self) -> None:
|
||||
super(MissileSiteGenerator, self).generate()
|
||||
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
||||
|
||||
@ -312,10 +312,6 @@ class NameGenerator:
|
||||
db.unit_type_name(unit_type),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def next_basedefense_name():
|
||||
return "basedefense_aa|0|0|"
|
||||
|
||||
@classmethod
|
||||
def next_awacs_name(cls, country: Country):
|
||||
cls.number += 1
|
||||
@ -352,7 +348,7 @@ class NameGenerator:
|
||||
|
||||
for _ in range(10):
|
||||
alpha = random.choice(ALPHA_MILITARY).upper()
|
||||
number = str(random.randint(0, 100))
|
||||
number = 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)
|
||||
|
||||
@ -21,6 +21,8 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
||||
This is the base for all SAM group generators
|
||||
"""
|
||||
|
||||
price: int
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
|
||||
ground_object.skynet_capable = True
|
||||
super().__init__(game, ground_object)
|
||||
|
||||
@ -17,8 +17,8 @@ from gen.sam.ewrs import (
|
||||
SnowDriftGenerator,
|
||||
StraightFlushGenerator,
|
||||
TallRackGenerator,
|
||||
EwrGenerator,
|
||||
)
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
EWR_MAP = {
|
||||
"BoxSpringGenerator": BoxSpringGenerator,
|
||||
@ -36,7 +36,7 @@ EWR_MAP = {
|
||||
|
||||
def get_faction_possible_ewrs_generator(
|
||||
faction: Faction,
|
||||
) -> List[Type[GroupGenerator]]:
|
||||
) -> List[Type[EwrGenerator]]:
|
||||
"""
|
||||
Return the list of possible EWR generators for the given faction
|
||||
:param faction: Faction name to search units for
|
||||
|
||||
@ -5,9 +5,16 @@ from gen.sam.group_generator import GroupGenerator
|
||||
|
||||
|
||||
class EwrGenerator(GroupGenerator):
|
||||
@property
|
||||
def unit_type(self) -> VehicleType:
|
||||
raise NotImplementedError
|
||||
unit_type: VehicleType
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return cls.unit_type.name
|
||||
|
||||
@staticmethod
|
||||
def price() -> int:
|
||||
# TODO: Differentiate sites.
|
||||
return 20
|
||||
|
||||
def generate(self) -> None:
|
||||
self.add_unit(
|
||||
|
||||
@ -10,14 +10,12 @@ from dcs.condition import (
|
||||
FlagIsFalse,
|
||||
FlagIsTrue,
|
||||
)
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.mission import Mission
|
||||
from dcs.task import Option
|
||||
from dcs.translation import String
|
||||
from dcs.triggers import (
|
||||
Event,
|
||||
TriggerOnce,
|
||||
TriggerZone,
|
||||
TriggerCondition,
|
||||
)
|
||||
from dcs.unit import Skill
|
||||
@ -25,7 +23,6 @@ from dcs.unit import Skill
|
||||
from game.theater import Airfield
|
||||
from game.theater.controlpoint import Fob
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
@ -115,19 +112,22 @@ class TriggersGenerator:
|
||||
mark_trigger.add_condition(TimeAfter(1))
|
||||
v = 10
|
||||
for cp in self.game.theater.controlpoints:
|
||||
added = []
|
||||
seen = set()
|
||||
for ground_object in cp.ground_objects:
|
||||
if ground_object.obj_name not in added:
|
||||
if ground_object.obj_name in seen:
|
||||
continue
|
||||
|
||||
seen.add(ground_object.obj_name)
|
||||
for location in ground_object.mark_locations:
|
||||
zone = self.mission.triggers.add_triggerzone(
|
||||
ground_object.position, radius=10, hidden=True, name="MARK"
|
||||
location, radius=10, hidden=True, name="MARK"
|
||||
)
|
||||
if cp.captured:
|
||||
name = ground_object.obj_name + " [ALLY]"
|
||||
else:
|
||||
name = ground_object.obj_name + " [ENEMY]"
|
||||
mark_trigger.add_action(MarkToAll(v, zone.id, String(name)))
|
||||
v = v + 1
|
||||
added.append(ground_object.obj_name)
|
||||
v += 1
|
||||
self.mission.triggerrules.triggers.append(mark_trigger)
|
||||
|
||||
def _generate_capture_triggers(
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
"""Visibility options for the game map."""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Iterator, Optional, Union
|
||||
|
||||
|
||||
@dataclass
|
||||
class DisplayRule:
|
||||
name: str
|
||||
_value: bool
|
||||
debug_only: bool = field(default=False)
|
||||
|
||||
@property
|
||||
def menu_text(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def value(self) -> bool:
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: bool) -> None:
|
||||
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
|
||||
|
||||
self._value = value
|
||||
if QLiberationMap.instance is not None:
|
||||
QLiberationMap.instance.reload_scene()
|
||||
QLiberationMap.instance.update()
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return self.value
|
||||
|
||||
|
||||
class DisplayGroup:
|
||||
def __init__(self, name: Optional[str], debug_only: bool = False) -> None:
|
||||
self.name = name
|
||||
self.debug_only = debug_only
|
||||
|
||||
def __iter__(self) -> Iterator[DisplayRule]:
|
||||
# Python 3.6 enforces that __dict__ is order preserving by default.
|
||||
for value in self.__dict__.values():
|
||||
if isinstance(value, DisplayRule):
|
||||
yield value
|
||||
|
||||
|
||||
class FlightPathOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Flight Paths")
|
||||
self.hide = DisplayRule("Hide Flight Paths", False)
|
||||
self.only_selected = DisplayRule("Show Selected Flight Path", False)
|
||||
self.all = DisplayRule("Show All Flight Paths", True)
|
||||
|
||||
|
||||
class ThreatZoneOptions(DisplayGroup):
|
||||
def __init__(self, coalition_name: str) -> None:
|
||||
super().__init__(f"{coalition_name} Threat Zones")
|
||||
self.none = DisplayRule(f"Hide {coalition_name.lower()} threat zones", True)
|
||||
self.all = DisplayRule(
|
||||
f"Show full {coalition_name.lower()} threat zones", False
|
||||
)
|
||||
self.aircraft = DisplayRule(
|
||||
f"Show {coalition_name.lower()} aircraft threat tones", False
|
||||
)
|
||||
self.air_defenses = DisplayRule(
|
||||
f"Show {coalition_name.lower()} air defenses threat zones", False
|
||||
)
|
||||
|
||||
|
||||
class NavMeshOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Navmeshes", debug_only=True)
|
||||
self.hide = DisplayRule("DEBUG Hide Navmeshes", True)
|
||||
self.blue_navmesh = DisplayRule("DEBUG Show blue navmesh", False)
|
||||
self.red_navmesh = DisplayRule("DEBUG Show red navmesh", False)
|
||||
|
||||
|
||||
class PathDebugFactionOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Faction for path debugging", debug_only=True)
|
||||
self.blue = DisplayRule("Debug blue paths", True)
|
||||
self.red = DisplayRule("Debug red paths", False)
|
||||
|
||||
|
||||
class PathDebugOptions(DisplayGroup):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("Shortest paths", debug_only=True)
|
||||
self.hide = DisplayRule("DEBUG Hide paths", True)
|
||||
self.shortest_path = DisplayRule("DEBUG Show shortest path", False)
|
||||
self.barcap = DisplayRule("DEBUG Show BARCAP plan", False)
|
||||
self.cas = DisplayRule("DEBUG Show CAS plan", False)
|
||||
self.sweep = DisplayRule("DEBUG Show fighter sweep plan", False)
|
||||
self.strike = DisplayRule("DEBUG Show strike plan", False)
|
||||
self.tarcap = DisplayRule("DEBUG Show TARCAP plan", False)
|
||||
|
||||
|
||||
class DisplayOptions:
|
||||
ground_objects = DisplayRule("Ground Objects", True)
|
||||
control_points = DisplayRule("Control Points", True)
|
||||
lines = DisplayRule("Lines", True)
|
||||
sam_ranges = DisplayRule("Ally SAM Threat Range", False)
|
||||
enemy_sam_ranges = DisplayRule("Enemy SAM Threat Range", True)
|
||||
detection_range = DisplayRule("SAM Detection Range", False)
|
||||
map_poly = DisplayRule("Map Polygon Debug Mode", False)
|
||||
waypoint_info = DisplayRule("Waypoint Information", True)
|
||||
culling = DisplayRule("Display Culling Zones", False)
|
||||
actual_frontline_pos = DisplayRule("Display Actual Frontline Location", False)
|
||||
patrol_engagement_range = DisplayRule(
|
||||
"Display selected patrol engagement range", True
|
||||
)
|
||||
flight_paths = FlightPathOptions()
|
||||
blue_threat_zones = ThreatZoneOptions("Blue")
|
||||
red_threat_zones = ThreatZoneOptions("Red")
|
||||
navmeshes = NavMeshOptions()
|
||||
path_debug_faction = PathDebugFactionOptions()
|
||||
path_debug = PathDebugOptions()
|
||||
|
||||
@classmethod
|
||||
def menu_items(cls) -> Iterator[Union[DisplayGroup, DisplayRule]]:
|
||||
debug = False # Set to True to enable debug options.
|
||||
# Python 3.6 enforces that __dict__ is order preserving by default.
|
||||
for value in cls.__dict__.values():
|
||||
if isinstance(value, DisplayRule):
|
||||
if value.debug_only and not debug:
|
||||
continue
|
||||
yield value
|
||||
elif isinstance(value, DisplayGroup):
|
||||
if value.debug_only and not debug:
|
||||
continue
|
||||
yield value
|
||||
@ -57,7 +57,7 @@ def inject_custom_payloads(user_path: Path) -> None:
|
||||
PayloadDirectories.set_preferred(user_path / "MissionEditor" / "UnitPayloads")
|
||||
|
||||
|
||||
def run_ui(game: Optional[Game], new_map: bool) -> None:
|
||||
def run_ui(game: Optional[Game]) -> None:
|
||||
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@ -111,7 +111,7 @@ def run_ui(game: Optional[Game], new_map: bool) -> None:
|
||||
GameUpdateSignal()
|
||||
|
||||
# Start window
|
||||
window = QLiberationWindow(game, new_map)
|
||||
window = QLiberationWindow(game)
|
||||
window.showMaximized()
|
||||
splash.finish(window)
|
||||
qt_execution_code = app.exec_()
|
||||
@ -139,16 +139,8 @@ def parse_args() -> argparse.Namespace:
|
||||
help="Emits a warning for weapons without date or fallback information.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--new-map",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Use the new map. Functional but missing many display options.",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--old-map", dest="new_map", action="store_false", help="Use the old map."
|
||||
)
|
||||
parser.add_argument("--new-map", help="Deprecated. Does nothing.")
|
||||
parser.add_argument("--old-map", help="Deprecated. Does nothing.")
|
||||
|
||||
new_game = subparsers.add_parser("new-game")
|
||||
|
||||
@ -267,7 +259,7 @@ def main():
|
||||
args.cheats,
|
||||
)
|
||||
|
||||
run_ui(game, args.new_map)
|
||||
run_ui(game)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -18,7 +18,7 @@ from game.squadrons import Squadron, Pilot
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
from game.transfers import TransferOrder
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flight import Flight, FlightType
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
from qt_ui.uiconstants import AIRCRAFT_ICONS
|
||||
|
||||
@ -424,7 +424,7 @@ class SquadronModel(QAbstractListModel):
|
||||
self.squadron = squadron
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
|
||||
return self.squadron.size
|
||||
return self.squadron.number_of_pilots_including_dead
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
|
||||
if not index.isValid():
|
||||
@ -467,6 +467,15 @@ class SquadronModel(QAbstractListModel):
|
||||
pilot.send_on_leave()
|
||||
self.endResetModel()
|
||||
|
||||
def is_auto_assignable(self, task: FlightType) -> bool:
|
||||
return task in self.squadron.auto_assignable_mission_types
|
||||
|
||||
def set_auto_assignable(self, task: FlightType, auto_assignable: bool) -> None:
|
||||
if auto_assignable:
|
||||
self.squadron.auto_assignable_mission_types.add(task)
|
||||
else:
|
||||
self.squadron.auto_assignable_mission_types.remove(task)
|
||||
|
||||
|
||||
class GameModel:
|
||||
"""A model for the Game object.
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
from PySide2.QtGui import QColor, QFont, QPixmap
|
||||
from PySide2.QtGui import QPixmap
|
||||
|
||||
from game.theater.theatergroundobject import NAME_BY_CATEGORY
|
||||
from .liberation_theme import get_theme_icons
|
||||
@ -16,51 +16,6 @@ URLS: Dict[str, str] = {
|
||||
LABELS_OPTIONS = ["Full", "Abbreviated", "Dot Only", "Off"]
|
||||
SKILL_OPTIONS = ["Average", "Good", "High", "Excellent"]
|
||||
|
||||
FONT_SIZE = 8
|
||||
FONT_NAME = "Arial"
|
||||
# FONT = QFont("Arial", 12, weight=5, italic=True)
|
||||
FONT_PRIMARY = QFont(FONT_NAME, FONT_SIZE, weight=5, italic=False)
|
||||
FONT_PRIMARY_I = QFont(FONT_NAME, FONT_SIZE, weight=5, italic=True)
|
||||
FONT_PRIMARY_B = QFont(FONT_NAME, FONT_SIZE, weight=75, italic=False)
|
||||
FONT_MAP = QFont(FONT_NAME, 10, weight=75, italic=False)
|
||||
|
||||
COLORS: Dict[str, QColor] = {
|
||||
"white": QColor(255, 255, 255),
|
||||
"white_transparent": QColor(255, 255, 255, 35),
|
||||
"light_red": QColor(231, 92, 83, 90),
|
||||
"red": QColor(200, 80, 80),
|
||||
"dark_red": QColor(140, 20, 20),
|
||||
"red_transparent": QColor(227, 32, 0, 20),
|
||||
"transparent": QColor(255, 255, 255, 0),
|
||||
"light_blue": QColor(105, 182, 240, 90),
|
||||
"blue": QColor(0, 132, 255),
|
||||
"dark_blue": QColor(45, 62, 80),
|
||||
"sea_blue": QColor(52, 68, 85),
|
||||
"sea_blue_transparent": QColor(52, 68, 85, 150),
|
||||
"blue_transparent": QColor(0, 132, 255, 20),
|
||||
"purple": QColor(187, 137, 255),
|
||||
"yellow": QColor(238, 225, 123),
|
||||
"bright_red": QColor(150, 80, 80),
|
||||
"super_red": QColor(227, 32, 0),
|
||||
"green": QColor(128, 186, 128),
|
||||
"light_green": QColor(223, 255, 173),
|
||||
"light_green_transparent": QColor(180, 255, 140, 50),
|
||||
"bright_green": QColor(64, 200, 64),
|
||||
"black": QColor(0, 0, 0),
|
||||
"black_transparent": QColor(0, 0, 0, 5),
|
||||
"orange": QColor(254, 125, 10),
|
||||
"night_overlay": QColor(12, 20, 69),
|
||||
"dawn_dust_overlay": QColor(46, 38, 85),
|
||||
"grey": QColor(150, 150, 150),
|
||||
"grey_transparent": QColor(150, 150, 150, 150),
|
||||
"dark_grey": QColor(75, 75, 75),
|
||||
"dark_grey_transparent": QColor(75, 75, 75, 150),
|
||||
"dark_dark_grey": QColor(48, 48, 48),
|
||||
"dark_dark_grey_transparent": QColor(48, 48, 48, 150),
|
||||
}
|
||||
|
||||
CP_SIZE = 12
|
||||
|
||||
AIRCRAFT_BANNERS: Dict[str, QPixmap] = {}
|
||||
AIRCRAFT_ICONS: Dict[str, QPixmap] = {}
|
||||
VEHICLE_BANNERS: Dict[str, QPixmap] = {}
|
||||
@ -138,17 +93,6 @@ def load_icons():
|
||||
"./resources/ui/misc/" + get_theme_icons() + "/ordnance_icon.png"
|
||||
)
|
||||
|
||||
ICONS["target"] = QPixmap("./resources/ui/ground_assets/target.png")
|
||||
ICONS["cleared"] = QPixmap("./resources/ui/ground_assets/cleared.png")
|
||||
for category in NAME_BY_CATEGORY.keys():
|
||||
ICONS[category] = QPixmap("./resources/ui/ground_assets/" + category + ".png")
|
||||
ICONS[category + "_blue"] = QPixmap(
|
||||
"./resources/ui/ground_assets/" + category + "_blue.png"
|
||||
)
|
||||
ICONS["destroyed"] = QPixmap("./resources/ui/ground_assets/destroyed.png")
|
||||
ICONS["nothreat"] = QPixmap("./resources/ui/ground_assets/nothreat.png")
|
||||
ICONS["nothreat_blue"] = QPixmap("./resources/ui/ground_assets/nothreat_blue.png")
|
||||
|
||||
ICONS["Generator"] = QPixmap(
|
||||
"./resources/ui/misc/" + get_theme_icons() + "/generator.png"
|
||||
)
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
from PySide2 import QtCore, QtGui, QtWidgets
|
||||
from PySide2 import QtCore, QtGui
|
||||
from PySide2.QtWidgets import QCalendarWidget
|
||||
|
||||
from qt_ui.uiconstants import COLORS
|
||||
|
||||
|
||||
class QLiberationCalendar(QCalendarWidget):
|
||||
def __init__(self, parent=None):
|
||||
@ -29,7 +27,7 @@ class QLiberationCalendar(QCalendarWidget):
|
||||
painter.save()
|
||||
painter.fillRect(rect, QtGui.QColor("#D3D3D3"))
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.setBrush(QtGui.QColor(COLORS["sea_blue"]))
|
||||
painter.setBrush(QtGui.QColor(52, 68, 85))
|
||||
r = QtCore.QRect(
|
||||
QtCore.QPoint(), min(rect.width(), rect.height()) * QtCore.QSize(1, 1)
|
||||
)
|
||||
|
||||
@ -25,8 +25,6 @@ from qt_ui.windows.AirWingDialog import AirWingDialog
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
|
||||
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
|
||||
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
|
||||
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
|
||||
|
||||
|
||||
class QTopPanel(QFrame):
|
||||
@ -74,26 +72,12 @@ class QTopPanel(QFrame):
|
||||
self.transfers.setProperty("style", "btn-primary")
|
||||
self.transfers.clicked.connect(self.open_transfers)
|
||||
|
||||
self.settings = QPushButton("Settings")
|
||||
self.settings.setDisabled(True)
|
||||
self.settings.setIcon(CONST.ICONS["Settings"])
|
||||
self.settings.setProperty("style", "btn-primary")
|
||||
self.settings.clicked.connect(self.openSettings)
|
||||
|
||||
self.statistics = QPushButton("Statistics")
|
||||
self.statistics.setDisabled(True)
|
||||
self.statistics.setIcon(CONST.ICONS["Statistics"])
|
||||
self.statistics.setProperty("style", "btn-primary")
|
||||
self.statistics.clicked.connect(self.openStatisticsWindow)
|
||||
|
||||
self.intel_box = QIntelBox(self.game)
|
||||
|
||||
self.buttonBox = QGroupBox("Misc")
|
||||
self.buttonBoxLayout = QHBoxLayout()
|
||||
self.buttonBoxLayout.addWidget(self.air_wing)
|
||||
self.buttonBoxLayout.addWidget(self.transfers)
|
||||
self.buttonBoxLayout.addWidget(self.settings)
|
||||
self.buttonBoxLayout.addWidget(self.statistics)
|
||||
self.buttonBox.setLayout(self.buttonBoxLayout)
|
||||
|
||||
self.proceedBox = QGroupBox("Proceed")
|
||||
@ -123,8 +107,6 @@ class QTopPanel(QFrame):
|
||||
|
||||
self.air_wing.setEnabled(True)
|
||||
self.transfers.setEnabled(True)
|
||||
self.settings.setEnabled(True)
|
||||
self.statistics.setEnabled(True)
|
||||
|
||||
self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
|
||||
self.intel_box.set_game(game)
|
||||
@ -146,14 +128,6 @@ class QTopPanel(QFrame):
|
||||
self.dialog = PendingTransfersDialog(self.game_model)
|
||||
self.dialog.show()
|
||||
|
||||
def openSettings(self):
|
||||
self.dialog = QSettingsWindow(self.game)
|
||||
self.dialog.show()
|
||||
|
||||
def openStatisticsWindow(self):
|
||||
self.dialog = QStatsWindow(self.game)
|
||||
self.dialog.show()
|
||||
|
||||
def passTurn(self):
|
||||
with logged_duration("Skipping turn"):
|
||||
self.game.pass_turn(no_action=True)
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
"""Common base for objects drawn on the game map."""
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QPen
|
||||
from PySide2.QtWidgets import (
|
||||
QAction,
|
||||
QGraphicsLineItem,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneHoverEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QMenu,
|
||||
)
|
||||
|
||||
import qt_ui.uiconstants as const
|
||||
from game.theater import FrontLine
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
|
||||
|
||||
|
||||
class QFrontLine(QGraphicsLineItem):
|
||||
"""Base class for objects drawn on the game map.
|
||||
|
||||
Game map objects have an on_click behavior that triggers on left click, and
|
||||
change the mouse cursor on hover.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
x1: float,
|
||||
y1: float,
|
||||
x2: float,
|
||||
y2: float,
|
||||
mission_target: FrontLine,
|
||||
game_model: GameModel,
|
||||
) -> None:
|
||||
super().__init__(x1, y1, x2, y2)
|
||||
self.mission_target = mission_target
|
||||
self.game_model = game_model
|
||||
self.new_package_dialog: Optional[QNewPackageDialog] = None
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
pen = QPen(brush=const.COLORS["bright_red"])
|
||||
pen.setColor(const.COLORS["orange"])
|
||||
pen.setWidth(8)
|
||||
self.setPen(pen)
|
||||
|
||||
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.on_click()
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
|
||||
menu = QMenu("Menu")
|
||||
|
||||
object_details_action = QAction(self.object_dialog_text)
|
||||
object_details_action.triggered.connect(self.on_click)
|
||||
menu.addAction(object_details_action)
|
||||
|
||||
new_package_action = QAction(f"New package")
|
||||
new_package_action.triggered.connect(self.open_new_package_dialog)
|
||||
menu.addAction(new_package_action)
|
||||
|
||||
if self.game_model.game.settings.enable_frontline_cheats:
|
||||
cheat_forward = QAction(f"CHEAT: Advance Frontline")
|
||||
cheat_forward.triggered.connect(self.cheat_forward)
|
||||
menu.addAction(cheat_forward)
|
||||
|
||||
cheat_backward = QAction(f"CHEAT: Retreat Frontline")
|
||||
cheat_backward.triggered.connect(self.cheat_backward)
|
||||
menu.addAction(cheat_backward)
|
||||
|
||||
menu.exec_(event.screenPos())
|
||||
|
||||
@property
|
||||
def object_dialog_text(self) -> str:
|
||||
"""Text to for the object's dialog in the context menu.
|
||||
|
||||
Right clicking a map object will open a context menu and the first item
|
||||
will open the details dialog for this object. This menu action has the
|
||||
same behavior as the on_click event.
|
||||
|
||||
Return:
|
||||
The text that should be displayed for the menu item.
|
||||
"""
|
||||
return "Details"
|
||||
|
||||
def on_click(self) -> None:
|
||||
"""The action to take when this map object is left-clicked.
|
||||
|
||||
Typically this should open a details view of the object.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def open_new_package_dialog(self) -> None:
|
||||
"""Opens the dialog for planning a new mission package."""
|
||||
Dialog.open_new_package_dialog(self.mission_target)
|
||||
|
||||
def cheat_forward(self) -> None:
|
||||
self.mission_target.blue_cp.base.affect_strength(0.1)
|
||||
self.mission_target.red_cp.base.affect_strength(-0.1)
|
||||
# Clear the ATO to replan missions affected by the front line.
|
||||
self.game_model.game.reset_ato()
|
||||
self.game_model.game.initialize_turn()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
|
||||
def cheat_backward(self) -> None:
|
||||
self.mission_target.blue_cp.base.affect_strength(-0.1)
|
||||
self.mission_target.red_cp.base.affect_strength(0.1)
|
||||
# Clear the ATO to replan missions affected by the front line.
|
||||
self.game_model.game.reset_ato()
|
||||
self.game_model.game.initialize_turn()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
@ -1,21 +0,0 @@
|
||||
from PySide2.QtWidgets import QGraphicsScene, QGraphicsSceneMouseEvent
|
||||
|
||||
import qt_ui.uiconstants as CONST
|
||||
|
||||
|
||||
class QLiberationScene(QGraphicsScene):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
item = self.addText(
|
||||
'Go to "File/New Game" to setup a new campaign or go to "File/Open" to load an existing save game.',
|
||||
CONST.FONT_PRIMARY,
|
||||
)
|
||||
item.setDefaultTextColor(CONST.COLORS["white"])
|
||||
|
||||
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
super(QLiberationScene, self).mouseMoveEvent(event)
|
||||
self.parent().sceneMouseMovedEvent(event)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
super(QLiberationScene, self).mousePressEvent(event)
|
||||
self.parent().sceneMousePressEvent(event)
|
||||
@ -1,125 +0,0 @@
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtGui import QColor, QPainter
|
||||
from PySide2.QtWidgets import QAction, QMenu
|
||||
|
||||
import qt_ui.uiconstants as const
|
||||
from game.theater import ControlPoint, NavalControlPoint
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
|
||||
from .QMapObject import QMapObject
|
||||
from ...displayoptions import DisplayOptions
|
||||
from ...windows.GameUpdateSignal import GameUpdateSignal
|
||||
|
||||
|
||||
class QMapControlPoint(QMapObject):
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
x: float,
|
||||
y: float,
|
||||
w: float,
|
||||
h: float,
|
||||
control_point: ControlPoint,
|
||||
game_model: GameModel,
|
||||
) -> None:
|
||||
super().__init__(x, y, w, h, mission_target=control_point)
|
||||
self.game_model = game_model
|
||||
self.control_point = control_point
|
||||
self.parent = parent
|
||||
self.setZValue(1)
|
||||
self.setToolTip(self.control_point.name)
|
||||
self.base_details_dialog: Optional[QBaseMenu2] = None
|
||||
self.capture_action = QAction(f"CHEAT: Capture {self.control_point.name}")
|
||||
self.capture_action.triggered.connect(self.cheat_capture)
|
||||
|
||||
self.move_action = QAction("Move")
|
||||
self.move_action.triggered.connect(self.move)
|
||||
|
||||
self.cancel_move_action = QAction("Cancel Move")
|
||||
self.cancel_move_action.triggered.connect(self.cancel_move)
|
||||
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
if DisplayOptions.control_points:
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
painter.setBrush(self.brush_color)
|
||||
painter.setPen(self.pen_color)
|
||||
|
||||
if not self.control_point.runway_is_operational():
|
||||
painter.setBrush(const.COLORS["black"])
|
||||
painter.setPen(self.brush_color)
|
||||
|
||||
r = option.rect
|
||||
painter.drawEllipse(r.x(), r.y(), r.width(), r.height())
|
||||
# TODO: Draw sunk carriers differently.
|
||||
# Either don't draw them at all, or perhaps use a sunk ship icon.
|
||||
painter.restore()
|
||||
|
||||
@property
|
||||
def brush_color(self) -> QColor:
|
||||
if self.control_point.captured:
|
||||
return const.COLORS["blue"]
|
||||
else:
|
||||
return const.COLORS["super_red"]
|
||||
|
||||
@property
|
||||
def pen_color(self) -> QColor:
|
||||
return const.COLORS["white"]
|
||||
|
||||
@property
|
||||
def object_dialog_text(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return "Open base menu"
|
||||
else:
|
||||
return "Open intel menu"
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.base_details_dialog = QBaseMenu2(
|
||||
self.window(), self.control_point, self.game_model
|
||||
)
|
||||
self.base_details_dialog.show()
|
||||
|
||||
def add_context_menu_actions(self, menu: QMenu) -> None:
|
||||
|
||||
if self.control_point.moveable and self.control_point.captured:
|
||||
menu.addAction(self.move_action)
|
||||
if self.control_point.target_position is not None:
|
||||
menu.addAction(self.cancel_move_action)
|
||||
|
||||
if self.control_point.is_fleet:
|
||||
return
|
||||
|
||||
if self.control_point.captured:
|
||||
return
|
||||
|
||||
for connected in self.control_point.connected_points:
|
||||
if (
|
||||
connected.captured
|
||||
and self.game_model.game.settings.enable_base_capture_cheat
|
||||
):
|
||||
menu.addAction(self.capture_action)
|
||||
break
|
||||
|
||||
def cheat_capture(self) -> None:
|
||||
self.control_point.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)
|
||||
|
||||
def move(self):
|
||||
self.parent.setSelectedUnit(self)
|
||||
|
||||
def cancel_move(self):
|
||||
self.control_point.target_position = None
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
|
||||
def open_new_package_dialog(self) -> None:
|
||||
"""Extends the default packagedialog to redirect to base menu for red air base."""
|
||||
is_navy = isinstance(self.control_point, NavalControlPoint)
|
||||
if self.control_point.captured or is_navy:
|
||||
super().open_new_package_dialog()
|
||||
return
|
||||
self.on_click()
|
||||
@ -1,167 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtCore import QRect
|
||||
from PySide2.QtGui import QBrush
|
||||
from PySide2.QtWidgets import QGraphicsItem
|
||||
|
||||
import qt_ui.uiconstants as const
|
||||
from game import Game
|
||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||
from game.db import REWARDS
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
MissileSiteGroundObject,
|
||||
CoastalSiteGroundObject,
|
||||
)
|
||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||
from .QMapObject import QMapObject
|
||||
from ...displayoptions import DisplayOptions
|
||||
|
||||
|
||||
class QMapGroundObject(QMapObject):
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
x: float,
|
||||
y: float,
|
||||
w: float,
|
||||
h: float,
|
||||
control_point: ControlPoint,
|
||||
ground_object: TheaterGroundObject,
|
||||
game: Game,
|
||||
buildings: Optional[List[TheaterGroundObject]] = None,
|
||||
) -> None:
|
||||
super().__init__(x, y, w, h, mission_target=ground_object)
|
||||
self.ground_object = ground_object
|
||||
self.control_point = control_point
|
||||
self.parent = parent
|
||||
self.game = game
|
||||
self.setZValue(2)
|
||||
self.buildings = buildings if buildings is not None else []
|
||||
self.setFlag(QGraphicsItem.ItemIgnoresTransformations, False)
|
||||
self.ground_object_dialog: Optional[QGroundObjectMenu] = None
|
||||
self.setToolTip(self.tooltip)
|
||||
|
||||
@property
|
||||
def tooltip(self) -> str:
|
||||
lines = [
|
||||
f"[{self.ground_object.obj_name}]",
|
||||
f"${self.production_per_turn} per turn",
|
||||
]
|
||||
if self.ground_object.groups:
|
||||
units = {}
|
||||
for g in self.ground_object.groups:
|
||||
for u in g.units:
|
||||
if u.type in units:
|
||||
units[u.type] = units[u.type] + 1
|
||||
else:
|
||||
units[u.type] = 1
|
||||
|
||||
for unit in units.keys():
|
||||
lines.append(f"{unit} x {units[unit]}")
|
||||
else:
|
||||
for building in self.buildings:
|
||||
if not building.is_dead:
|
||||
lines.append(f"{building.dcs_identifier}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
@property
|
||||
def production_per_turn(self) -> int:
|
||||
production = 0
|
||||
for building in self.buildings:
|
||||
if building.is_dead:
|
||||
continue
|
||||
if building.category in REWARDS.keys():
|
||||
production += REWARDS[building.category]
|
||||
return production
|
||||
|
||||
def paint(self, painter, option, widget=None) -> None:
|
||||
player_icons = "_blue"
|
||||
enemy_icons = ""
|
||||
|
||||
if DisplayOptions.ground_objects:
|
||||
painter.save()
|
||||
|
||||
cat = self.ground_object.category
|
||||
|
||||
rect = QRect(
|
||||
option.rect.x() + 2,
|
||||
option.rect.y(),
|
||||
option.rect.width() - 2,
|
||||
option.rect.height(),
|
||||
)
|
||||
|
||||
is_dead = self.ground_object.is_dead
|
||||
for building in self.buildings:
|
||||
if not building.is_dead:
|
||||
is_dead = False
|
||||
break
|
||||
|
||||
if cat == "aa":
|
||||
has_threat = False
|
||||
for group in self.ground_object.groups:
|
||||
if self.ground_object.threat_range(group).distance_in_meters > 0:
|
||||
has_threat = True
|
||||
|
||||
if not is_dead and not self.control_point.captured:
|
||||
if cat == "aa" and not has_threat:
|
||||
painter.drawPixmap(rect, const.ICONS["nothreat" + enemy_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + enemy_icons])
|
||||
elif not is_dead:
|
||||
if cat == "aa" and not has_threat:
|
||||
painter.drawPixmap(rect, const.ICONS["nothreat" + player_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS[cat + player_icons])
|
||||
else:
|
||||
painter.drawPixmap(rect, const.ICONS["destroyed"])
|
||||
|
||||
self.draw_health_gauge(painter, option)
|
||||
painter.restore()
|
||||
|
||||
def draw_health_gauge(self, painter, option) -> None:
|
||||
units_alive = 0
|
||||
units_dead = 0
|
||||
|
||||
if len(self.ground_object.groups) == 0:
|
||||
for building in self.buildings:
|
||||
if building.dcs_identifier in FORTIFICATION_BUILDINGS:
|
||||
continue
|
||||
if building.is_dead:
|
||||
units_dead += 1
|
||||
else:
|
||||
units_alive += 1
|
||||
|
||||
for g in self.ground_object.groups:
|
||||
units_alive += len(g.units)
|
||||
if hasattr(g, "units_losts"):
|
||||
units_dead += len(g.units_losts)
|
||||
|
||||
if units_dead + units_alive > 0:
|
||||
ratio = float(units_alive) / (float(units_dead) + float(units_alive))
|
||||
bar_height = ratio * option.rect.height()
|
||||
painter.fillRect(
|
||||
option.rect.x(),
|
||||
option.rect.y(),
|
||||
2,
|
||||
option.rect.height(),
|
||||
QBrush(const.COLORS["dark_red"]),
|
||||
)
|
||||
painter.fillRect(
|
||||
option.rect.x(),
|
||||
option.rect.y(),
|
||||
2,
|
||||
bar_height,
|
||||
QBrush(const.COLORS["green"]),
|
||||
)
|
||||
|
||||
def on_click(self) -> None:
|
||||
self.ground_object_dialog = QGroundObjectMenu(
|
||||
self.window(),
|
||||
self.ground_object,
|
||||
self.buildings,
|
||||
self.control_point,
|
||||
self.game,
|
||||
)
|
||||
self.ground_object_dialog.show()
|
||||
@ -1,84 +0,0 @@
|
||||
"""Common base for objects drawn on the game map."""
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QAction,
|
||||
QGraphicsRectItem,
|
||||
QGraphicsSceneContextMenuEvent,
|
||||
QGraphicsSceneHoverEvent,
|
||||
QGraphicsSceneMouseEvent,
|
||||
QMenu,
|
||||
)
|
||||
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.windows.mission.QPackageDialog import QNewPackageDialog
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
|
||||
|
||||
class QMapObject(QGraphicsRectItem):
|
||||
"""Base class for objects drawn on the game map.
|
||||
|
||||
Game map objects have an on_click behavior that triggers on left click, and
|
||||
change the mouse cursor on hover.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, x: float, y: float, w: float, h: float, mission_target: MissionTarget
|
||||
) -> None:
|
||||
super().__init__(x, y, w, h)
|
||||
self.mission_target = mission_target
|
||||
self.new_package_dialog: Optional[QNewPackageDialog] = None
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
|
||||
self.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
def mousePressEvent(self, event: QGraphicsSceneMouseEvent):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.on_click()
|
||||
|
||||
def add_context_menu_actions(self, menu: QMenu) -> None:
|
||||
pass
|
||||
|
||||
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None:
|
||||
menu = QMenu("Menu", self.parent)
|
||||
|
||||
object_details_action = QAction(self.object_dialog_text)
|
||||
object_details_action.triggered.connect(self.on_click)
|
||||
menu.addAction(object_details_action)
|
||||
|
||||
# Not all locations have valid objectives. Off-map spawns, for example,
|
||||
# have no mission types.
|
||||
if list(self.mission_target.mission_types(for_player=True)):
|
||||
new_package_action = QAction(f"New package")
|
||||
new_package_action.triggered.connect(self.open_new_package_dialog)
|
||||
menu.addAction(new_package_action)
|
||||
|
||||
self.add_context_menu_actions(menu)
|
||||
|
||||
menu.exec_(event.screenPos())
|
||||
|
||||
@property
|
||||
def object_dialog_text(self) -> str:
|
||||
"""Text to for the object's dialog in the context menu.
|
||||
|
||||
Right clicking a map object will open a context menu and the first item
|
||||
will open the details dialog for this object. This menu action has the
|
||||
same behavior as the on_click event.
|
||||
|
||||
Return:
|
||||
The text that should be displayed for the menu item.
|
||||
"""
|
||||
return "Details"
|
||||
|
||||
def on_click(self) -> None:
|
||||
"""The action to take when this map object is left-clicked.
|
||||
|
||||
Typically this should open a details view of the object.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def open_new_package_dialog(self) -> None:
|
||||
"""Opens the dialog for planning a new mission package."""
|
||||
Dialog.open_new_package_dialog(self.mission_target)
|
||||
@ -1,70 +0,0 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QColor, QPen
|
||||
from PySide2.QtWidgets import (
|
||||
QGraphicsItem,
|
||||
QGraphicsLineItem,
|
||||
)
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from game.transfers import CargoShip
|
||||
from qt_ui.uiconstants import COLORS
|
||||
|
||||
|
||||
class ShippingLaneSegment(QGraphicsLineItem):
|
||||
def __init__(
|
||||
self,
|
||||
x0: float,
|
||||
y0: float,
|
||||
x1: float,
|
||||
y1: float,
|
||||
control_point_a: ControlPoint,
|
||||
control_point_b: ControlPoint,
|
||||
ships: List[CargoShip],
|
||||
parent: Optional[QGraphicsItem] = None,
|
||||
) -> None:
|
||||
super().__init__(x0, y0, x1, y1, parent)
|
||||
self.control_point_a = control_point_a
|
||||
self.control_point_b = control_point_b
|
||||
self.ships = ships
|
||||
self.setPen(self.make_pen())
|
||||
self.setToolTip(self.make_tooltip())
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
@property
|
||||
def has_ships(self) -> bool:
|
||||
return bool(self.ships)
|
||||
|
||||
def make_tooltip(self) -> str:
|
||||
if not self.has_ships:
|
||||
return "No ships present in this shipping lane."
|
||||
|
||||
ships = []
|
||||
for ship in self.ships:
|
||||
units = "units" if ship.size > 1 else "unit"
|
||||
ships.append(
|
||||
f"{ship.size} {units} transferring from {ship.origin} to "
|
||||
f"{ship.destination}."
|
||||
)
|
||||
return "\n".join(ships)
|
||||
|
||||
@property
|
||||
def line_color(self) -> QColor:
|
||||
if self.control_point_a.captured:
|
||||
return COLORS["dark_blue"]
|
||||
else:
|
||||
return COLORS["dark_red"]
|
||||
|
||||
@property
|
||||
def line_style(self) -> Qt.PenStyle:
|
||||
if self.has_ships:
|
||||
return Qt.PenStyle.SolidLine
|
||||
return Qt.PenStyle.DotLine
|
||||
|
||||
def make_pen(self) -> QPen:
|
||||
pen = QPen(brush=self.line_color)
|
||||
pen.setColor(self.line_color)
|
||||
pen.setStyle(self.line_style)
|
||||
pen.setWidth(2)
|
||||
return pen
|
||||
@ -1,76 +0,0 @@
|
||||
from functools import cached_property
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtGui import QColor, QPen
|
||||
from PySide2.QtWidgets import (
|
||||
QGraphicsItem,
|
||||
QGraphicsLineItem,
|
||||
)
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from game.transfers import Convoy
|
||||
from qt_ui.uiconstants import COLORS
|
||||
|
||||
|
||||
class SupplyRouteSegment(QGraphicsLineItem):
|
||||
def __init__(
|
||||
self,
|
||||
x0: float,
|
||||
y0: float,
|
||||
x1: float,
|
||||
y1: float,
|
||||
control_point_a: ControlPoint,
|
||||
control_point_b: ControlPoint,
|
||||
convoys: List[Convoy],
|
||||
parent: Optional[QGraphicsItem] = None,
|
||||
) -> None:
|
||||
super().__init__(x0, y0, x1, y1, parent)
|
||||
self.control_point_a = control_point_a
|
||||
self.control_point_b = control_point_b
|
||||
self.convoys = convoys
|
||||
self.setPen(self.make_pen())
|
||||
self.setToolTip(self.make_tooltip())
|
||||
self.setAcceptHoverEvents(True)
|
||||
|
||||
@property
|
||||
def has_convoys(self) -> bool:
|
||||
return bool(self.convoys)
|
||||
|
||||
def make_tooltip(self) -> str:
|
||||
if not self.has_convoys:
|
||||
return "No convoys present on this supply route."
|
||||
|
||||
convoys = []
|
||||
for convoy in self.convoys:
|
||||
units = "units" if convoy.size > 1 else "unit"
|
||||
convoys.append(
|
||||
f"{convoy.size} {units} transferring from {convoy.origin} to "
|
||||
f"{convoy.destination}"
|
||||
)
|
||||
return "\n".join(convoys)
|
||||
|
||||
@property
|
||||
def line_color(self) -> QColor:
|
||||
if self.control_point_a.front_is_active(self.control_point_b):
|
||||
return COLORS["red"]
|
||||
elif self.control_point_a.captured:
|
||||
return COLORS["dark_blue"]
|
||||
else:
|
||||
return COLORS["dark_red"]
|
||||
|
||||
@property
|
||||
def line_style(self) -> Qt.PenStyle:
|
||||
if (
|
||||
self.control_point_a.front_is_active(self.control_point_b)
|
||||
or self.has_convoys
|
||||
):
|
||||
return Qt.PenStyle.SolidLine
|
||||
return Qt.PenStyle.DotLine
|
||||
|
||||
def make_pen(self) -> QPen:
|
||||
pen = QPen(brush=self.line_color)
|
||||
pen.setColor(self.line_color)
|
||||
pen.setStyle(self.line_style)
|
||||
pen.setWidth(6)
|
||||
return pen
|
||||
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import List, Optional, Tuple, Union, Iterator
|
||||
|
||||
from PySide2.QtCore import Property, QObject, Signal, Slot
|
||||
from dcs import Point
|
||||
@ -20,6 +20,7 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
FrontLine,
|
||||
LatLon,
|
||||
ControlPointStatus,
|
||||
)
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import MultiGroupTransport, TransportMap
|
||||
@ -36,6 +37,8 @@ from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||
LeafletLatLon = list[float]
|
||||
LeafletPoly = list[LeafletLatLon]
|
||||
|
||||
MAX_SHIP_DISTANCE = nautical_miles(80)
|
||||
|
||||
# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL**
|
||||
#
|
||||
# https://bugreports.qt.io/browse/PYSIDE-1426
|
||||
@ -60,6 +63,16 @@ def shapely_poly_to_leaflet_points(
|
||||
return [theater.point_to_ll(Point(x, y)).as_list() for x, y in poly.exterior.coords]
|
||||
|
||||
|
||||
def shapely_to_leaflet_polys(
|
||||
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]
|
||||
|
||||
|
||||
class ControlPointJs(QObject):
|
||||
nameChanged = Signal()
|
||||
blueChanged = Signal()
|
||||
@ -67,6 +80,7 @@ class ControlPointJs(QObject):
|
||||
mobileChanged = Signal()
|
||||
destinationChanged = Signal(list)
|
||||
categoryChanged = Signal()
|
||||
statusChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -92,6 +106,17 @@ class ControlPointJs(QObject):
|
||||
def category(self) -> str:
|
||||
return self.control_point.category
|
||||
|
||||
@Property(str, notify=statusChanged)
|
||||
def status(self) -> str:
|
||||
status = self.control_point.status
|
||||
if status is ControlPointStatus.Functional:
|
||||
return "alive"
|
||||
elif status is ControlPointStatus.Damaged:
|
||||
return "damaged"
|
||||
elif status is ControlPointStatus.Destroyed:
|
||||
return "destroyed"
|
||||
raise ValueError(f"Unhandled ControlPointStatus: {status.name}")
|
||||
|
||||
@Property(list, notify=positionChanged)
|
||||
def position(self) -> LeafletLatLon:
|
||||
ll = self.theater.point_to_ll(self.control_point.position)
|
||||
@ -109,8 +134,6 @@ class ControlPointJs(QObject):
|
||||
return self.theater.point_to_ll(self.control_point.target_position).as_list()
|
||||
|
||||
def destination_in_range(self, destination: Point) -> bool:
|
||||
from qt_ui.widgets.map.QLiberationMap import MAX_SHIP_DISTANCE
|
||||
|
||||
move_distance = meters(
|
||||
destination.distance_to_point(self.control_point.position)
|
||||
)
|
||||
@ -122,8 +145,6 @@ class ControlPointJs(QObject):
|
||||
|
||||
@Slot(list, result=str)
|
||||
def setDestination(self, destination: LeafletLatLon) -> str:
|
||||
from qt_ui.widgets.map.QLiberationMap import MAX_SHIP_DISTANCE
|
||||
|
||||
if not self.control_point.moveable:
|
||||
return f"{self.control_point} is not mobile"
|
||||
if not self.control_point.captured:
|
||||
@ -581,23 +602,13 @@ class ThreatZonesJs(QObject):
|
||||
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),
|
||||
shapely_to_leaflet_polys(zones.all, theater),
|
||||
shapely_to_leaflet_polys(zones.airbases, theater),
|
||||
shapely_to_leaflet_polys(zones.air_defenses, theater),
|
||||
shapely_to_leaflet_polys(zones.radar_sam_threats, theater),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -658,6 +669,70 @@ class NavMeshJs(QObject):
|
||||
)
|
||||
|
||||
|
||||
class MapZonesJs(QObject):
|
||||
inclusionZonesChanged = Signal()
|
||||
exclusionZonesChanged = Signal()
|
||||
seaZonesChanged = Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
inclusion_zones: list[LeafletPoly],
|
||||
exclusion_zones: list[LeafletPoly],
|
||||
sea_zones: list[LeafletPoly],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._inclusion_zones = inclusion_zones
|
||||
self._exclusion_zones = exclusion_zones
|
||||
self._sea_zones = sea_zones
|
||||
|
||||
@Property(list, notify=inclusionZonesChanged)
|
||||
def inclusionZones(self) -> list[LeafletPoly]:
|
||||
return self._inclusion_zones
|
||||
|
||||
@Property(list, notify=exclusionZonesChanged)
|
||||
def exclusionZones(self) -> list[LeafletPoly]:
|
||||
return self._exclusion_zones
|
||||
|
||||
@Property(list, notify=seaZonesChanged)
|
||||
def seaZones(self) -> list[LeafletPoly]:
|
||||
return self._sea_zones
|
||||
|
||||
@classmethod
|
||||
def from_game(cls, game: Game) -> MapZonesJs:
|
||||
zones = game.theater.landmap
|
||||
return MapZonesJs(
|
||||
shapely_to_leaflet_polys(zones.inclusion_zones, game.theater),
|
||||
shapely_to_leaflet_polys(zones.exclusion_zones, game.theater),
|
||||
shapely_to_leaflet_polys(zones.sea_zones, game.theater),
|
||||
)
|
||||
|
||||
|
||||
class UnculledZone(QObject):
|
||||
positionChanged = Signal()
|
||||
radiusChanged = Signal()
|
||||
|
||||
def __init__(self, position: LeafletLatLon, radius: float) -> None:
|
||||
super().__init__()
|
||||
self._position = position
|
||||
self._radius = radius
|
||||
|
||||
@Property(list, notify=positionChanged)
|
||||
def position(self) -> LeafletLatLon:
|
||||
return self._position
|
||||
|
||||
@Property(float, notify=radiusChanged)
|
||||
def radius(self) -> float:
|
||||
return self._radius
|
||||
|
||||
@classmethod
|
||||
def each_from_game(cls, game: Game) -> Iterator[UnculledZone]:
|
||||
for zone in game.get_culling_zones():
|
||||
ll = game.theater.point_to_ll(zone)
|
||||
yield UnculledZone(
|
||||
[ll.latitude, ll.longitude], game.settings.perf_culling_distance * 1000
|
||||
)
|
||||
|
||||
|
||||
class MapModel(QObject):
|
||||
cleared = Signal()
|
||||
|
||||
@ -669,6 +744,8 @@ class MapModel(QObject):
|
||||
frontLinesChanged = Signal()
|
||||
threatZonesChanged = Signal()
|
||||
navmeshesChanged = Signal()
|
||||
mapZonesChanged = Signal()
|
||||
unculledZonesChanged = Signal()
|
||||
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
@ -683,6 +760,8 @@ class MapModel(QObject):
|
||||
ThreatZonesJs.empty(), ThreatZonesJs.empty()
|
||||
)
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self._selected_flight_index: Optional[Tuple[int, int]] = None
|
||||
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
|
||||
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
|
||||
@ -704,6 +783,8 @@ class MapModel(QObject):
|
||||
ThreatZonesJs.empty(), ThreatZonesJs.empty()
|
||||
)
|
||||
self._navmeshes = NavMeshJs([], [])
|
||||
self._map_zones = MapZonesJs([], [], [])
|
||||
self._unculled_zones = []
|
||||
self.cleared.emit()
|
||||
|
||||
def set_package_selection(self, index: int) -> None:
|
||||
@ -749,6 +830,8 @@ class MapModel(QObject):
|
||||
self.reset_front_lines()
|
||||
self.reset_threat_zones()
|
||||
self.reset_navmeshes()
|
||||
self.reset_map_zones()
|
||||
self.reset_unculled_zones()
|
||||
|
||||
def on_game_load(self, game: Optional[Game]) -> None:
|
||||
if game is not None:
|
||||
@ -895,6 +978,22 @@ class MapModel(QObject):
|
||||
def navmeshes(self) -> NavMeshJs:
|
||||
return self._navmeshes
|
||||
|
||||
def reset_map_zones(self) -> None:
|
||||
self._map_zones = MapZonesJs.from_game(self.game)
|
||||
self.mapZonesChanged.emit()
|
||||
|
||||
@Property(MapZonesJs, notify=mapZonesChanged)
|
||||
def mapZones(self) -> NavMeshJs:
|
||||
return self._map_zones
|
||||
|
||||
def reset_unculled_zones(self) -> None:
|
||||
self._unculled_zones = list(UnculledZone.each_from_game(self.game))
|
||||
self.unculledZonesChanged.emit()
|
||||
|
||||
@Property(list, notify=unculledZonesChanged)
|
||||
def unculledZones(self) -> list[UnculledZone]:
|
||||
return self._unculled_zones
|
||||
|
||||
@property
|
||||
def game(self) -> Game:
|
||||
if self.game_model.game is None:
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Type, Iterator
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QItemSelectionModel,
|
||||
@ -8,13 +11,21 @@ from PySide2.QtCore import (
|
||||
)
|
||||
from PySide2.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QListView,
|
||||
QVBoxLayout,
|
||||
QTabWidget,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QWidget,
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.inventory import ControlPointAircraftInventory
|
||||
from game.squadrons import Squadron
|
||||
from gen.flights.flight import Flight
|
||||
from qt_ui.delegates import TwoColumnRowDelegate
|
||||
from qt_ui.models import GameModel, AirWingModel, SquadronModel
|
||||
from qt_ui.windows.SquadronDialog import SquadronDialog
|
||||
@ -41,9 +52,10 @@ class SquadronDelegate(TwoColumnRowDelegate):
|
||||
return self.squadron(index).nickname
|
||||
elif (row, column) == (1, 1):
|
||||
squadron = self.squadron(index)
|
||||
alive = squadron.number_of_living_pilots
|
||||
active = len(squadron.active_pilots)
|
||||
available = len(squadron.available_pilots)
|
||||
return f"{squadron.size} pilots, {active} active, {available} unassigned"
|
||||
return f"{alive} pilots, {active} active, {available} unassigned"
|
||||
return ""
|
||||
|
||||
|
||||
@ -75,6 +87,138 @@ class SquadronList(QListView):
|
||||
self.dialog.show()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftInventoryData:
|
||||
location: str
|
||||
unit_type: str
|
||||
task: str
|
||||
target: str
|
||||
pilot: str
|
||||
player: str
|
||||
|
||||
@classmethod
|
||||
def headers(cls) -> list[str]:
|
||||
return ["Base", "Type", "Flight Type", "Target", "Pilot", "Player"]
|
||||
|
||||
@property
|
||||
def columns(self) -> Iterator[str]:
|
||||
yield self.location
|
||||
yield self.unit_type
|
||||
yield self.task
|
||||
yield self.target
|
||||
yield self.pilot
|
||||
yield self.player
|
||||
|
||||
@classmethod
|
||||
def from_flight(cls, flight: Flight) -> Iterator[AircraftInventoryData]:
|
||||
unit_type_name = cls.format_unit_type(flight.unit_type, flight.country)
|
||||
num_units = flight.count
|
||||
flight_type = flight.flight_type.value
|
||||
target = flight.package.target.name
|
||||
for idx in range(0, num_units):
|
||||
pilot = flight.roster.pilots[idx]
|
||||
if pilot is None:
|
||||
pilot_name = "Unassigned"
|
||||
player = ""
|
||||
else:
|
||||
pilot_name = pilot.name
|
||||
player = "Player" if pilot.player else "AI"
|
||||
yield AircraftInventoryData(
|
||||
flight.departure.name,
|
||||
unit_type_name,
|
||||
flight_type,
|
||||
target,
|
||||
pilot_name,
|
||||
player,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def each_from_inventory(
|
||||
cls, inventory: ControlPointAircraftInventory, country: str
|
||||
) -> Iterator[AircraftInventoryData]:
|
||||
for unit_type, num_units in inventory.all_aircraft:
|
||||
unit_type_name = cls.format_unit_type(unit_type, country)
|
||||
for _ in range(0, num_units):
|
||||
yield AircraftInventoryData(
|
||||
inventory.control_point.name,
|
||||
unit_type_name,
|
||||
"Idle",
|
||||
"N/A",
|
||||
"N/A",
|
||||
"N/A",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def format_unit_type(aircraft: Type[FlyingType], country: str) -> str:
|
||||
return db.unit_get_expanded_info(country, aircraft, "name")
|
||||
|
||||
|
||||
class AirInventoryView(QWidget):
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.game_model = game_model
|
||||
self.country = self.game_model.game.country_for(player=True)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
self.only_unallocated_cb = QCheckBox("Unallocated Only?")
|
||||
self.only_unallocated_cb.toggled.connect(self.update_table)
|
||||
|
||||
layout.addWidget(self.only_unallocated_cb)
|
||||
|
||||
self.table = QTableWidget()
|
||||
layout.addWidget(self.table)
|
||||
|
||||
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.table.verticalHeader().setVisible(False)
|
||||
self.update_table(False)
|
||||
|
||||
def update_table(self, only_unallocated: bool) -> None:
|
||||
self.table.setSortingEnabled(False)
|
||||
self.table.clear()
|
||||
|
||||
inventory_rows = list(self.get_data(only_unallocated))
|
||||
self.table.setRowCount(len(inventory_rows))
|
||||
headers = AircraftInventoryData.headers()
|
||||
self.table.setColumnCount(len(headers))
|
||||
self.table.setHorizontalHeaderLabels(headers)
|
||||
|
||||
for row, data in enumerate(inventory_rows):
|
||||
for column, value in enumerate(data.columns):
|
||||
self.table.setItem(row, column, QTableWidgetItem(value))
|
||||
|
||||
self.table.resizeColumnsToContents()
|
||||
self.table.setSortingEnabled(True)
|
||||
|
||||
def iter_allocated_aircraft(self) -> Iterator[AircraftInventoryData]:
|
||||
for package in self.game_model.game.blue_ato.packages:
|
||||
for flight in package.flights:
|
||||
yield from AircraftInventoryData.from_flight(flight)
|
||||
|
||||
def iter_unallocated_aircraft(self) -> Iterator[AircraftInventoryData]:
|
||||
game = self.game_model.game
|
||||
for control_point, inventory in game.aircraft_inventory.inventories.items():
|
||||
if control_point.captured:
|
||||
yield from AircraftInventoryData.each_from_inventory(
|
||||
inventory, game.country_for(player=True)
|
||||
)
|
||||
|
||||
def get_data(self, only_unallocated: bool) -> Iterator[AircraftInventoryData]:
|
||||
yield from self.iter_unallocated_aircraft()
|
||||
if not only_unallocated:
|
||||
yield from self.iter_allocated_aircraft()
|
||||
|
||||
|
||||
class AirWingTabs(QTabWidget):
|
||||
def __init__(self, game_model: GameModel) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.addTab(SquadronList(game_model.blue_air_wing_model), "Squadrons")
|
||||
self.addTab(AirInventoryView(game_model), "Inventory")
|
||||
|
||||
|
||||
class AirWingDialog(QDialog):
|
||||
"""Dialog window showing the player's air wing."""
|
||||
|
||||
@ -89,4 +233,4 @@ class AirWingDialog(QDialog):
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
layout.addWidget(SquadronList(self.air_wing_model))
|
||||
layout.addWidget(AirWingTabs(game_model))
|
||||
|
||||
@ -22,12 +22,11 @@ from game import Game, VERSION, persistency
|
||||
from game.debriefing import Debriefing
|
||||
from qt_ui import liberation_install
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.uiconstants import URLS
|
||||
from qt_ui.widgets.QTopPanel import QTopPanel
|
||||
from qt_ui.widgets.ato import QAirTaskingOrderPanel
|
||||
from qt_ui.widgets.map.QLiberationMap import LeafletMap, QLiberationMap, LiberationMap
|
||||
from qt_ui.widgets.map.QLiberationMap import QLiberationMap
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
|
||||
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
|
||||
@ -35,10 +34,12 @@ from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
|
||||
from qt_ui.windows.preferences.QLiberationPreferencesWindow import (
|
||||
QLiberationPreferencesWindow,
|
||||
)
|
||||
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
|
||||
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
|
||||
|
||||
|
||||
class QLiberationWindow(QMainWindow):
|
||||
def __init__(self, game: Optional[Game], new_map: bool) -> None:
|
||||
def __init__(self, game: Optional[Game]) -> None:
|
||||
super(QLiberationWindow, self).__init__()
|
||||
|
||||
self.game = game
|
||||
@ -46,7 +47,7 @@ class QLiberationWindow(QMainWindow):
|
||||
Dialog.set_game(self.game_model)
|
||||
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
|
||||
self.info_panel = QInfoPanel(self.game)
|
||||
self.liberation_map: LiberationMap = self.create_map(new_map)
|
||||
self.liberation_map = QLiberationMap(self.game_model, self)
|
||||
|
||||
self.setGeometry(300, 100, 270, 100)
|
||||
self.setWindowTitle(f"DCS Liberation - v{VERSION}")
|
||||
@ -148,6 +149,14 @@ class QLiberationWindow(QMainWindow):
|
||||
)
|
||||
)
|
||||
|
||||
self.openSettingsAction = QAction("Settings", self)
|
||||
self.openSettingsAction.setIcon(CONST.ICONS["Settings"])
|
||||
self.openSettingsAction.triggered.connect(self.showSettingsDialog)
|
||||
|
||||
self.openStatsAction = QAction("Stats", self)
|
||||
self.openStatsAction.setIcon(CONST.ICONS["Statistics"])
|
||||
self.openStatsAction.triggered.connect(self.showStatsDialog)
|
||||
|
||||
def initToolbar(self):
|
||||
self.tool_bar = self.addToolBar("File")
|
||||
self.tool_bar.addAction(self.newGameAction)
|
||||
@ -158,7 +167,9 @@ class QLiberationWindow(QMainWindow):
|
||||
self.links_bar.addAction(self.openDiscordAction)
|
||||
self.links_bar.addAction(self.openGithubAction)
|
||||
|
||||
self.display_bar = self.addToolBar("Display")
|
||||
self.actions_bar = self.addToolBar("Actions")
|
||||
self.actions_bar.addAction(self.openSettingsAction)
|
||||
self.actions_bar.addAction(self.openStatsAction)
|
||||
|
||||
def initMenuBar(self):
|
||||
self.menu = self.menuBar()
|
||||
@ -174,30 +185,6 @@ class QLiberationWindow(QMainWindow):
|
||||
file_menu.addSeparator()
|
||||
file_menu.addAction("E&xit", self.close)
|
||||
|
||||
displayMenu = self.menu.addMenu("&Display")
|
||||
|
||||
last_was_group = False
|
||||
for item in DisplayOptions.menu_items():
|
||||
if isinstance(item, DisplayRule):
|
||||
if last_was_group:
|
||||
displayMenu.addSeparator()
|
||||
self.display_bar.addSeparator()
|
||||
action = self.make_display_rule_action(item)
|
||||
displayMenu.addAction(action)
|
||||
if action.icon():
|
||||
self.display_bar.addAction(action)
|
||||
last_was_group = False
|
||||
elif isinstance(item, DisplayGroup):
|
||||
displayMenu.addSeparator()
|
||||
self.display_bar.addSeparator()
|
||||
group = QActionGroup(displayMenu)
|
||||
for display_rule in item:
|
||||
action = self.make_display_rule_action(display_rule, group)
|
||||
displayMenu.addAction(action)
|
||||
if action.icon():
|
||||
self.display_bar.addAction(action)
|
||||
last_was_group = True
|
||||
|
||||
help_menu = self.menu.addMenu("&Help")
|
||||
help_menu.addAction(self.openDiscordAction)
|
||||
help_menu.addAction(self.openGithubAction)
|
||||
@ -284,11 +271,6 @@ class QLiberationWindow(QMainWindow):
|
||||
self.game = game
|
||||
GameUpdateSignal.get_instance().game_loaded.emit(self.game)
|
||||
|
||||
def create_map(self, new_map: bool) -> LiberationMap:
|
||||
if new_map:
|
||||
return LeafletMap(self.game_model, self)
|
||||
return QLiberationMap(self.game_model)
|
||||
|
||||
def setGame(self, game: Optional[Game]):
|
||||
try:
|
||||
self.game = game
|
||||
@ -337,6 +319,14 @@ class QLiberationWindow(QMainWindow):
|
||||
self.subwindow = QLiberationPreferencesWindow()
|
||||
self.subwindow.show()
|
||||
|
||||
def showSettingsDialog(self) -> None:
|
||||
self.dialog = QSettingsWindow(self.game)
|
||||
self.dialog.show()
|
||||
|
||||
def showStatsDialog(self):
|
||||
self.dialog = QStatsWindow(self.game)
|
||||
self.dialog.show()
|
||||
|
||||
def onDebriefing(self, debrief: Debriefing):
|
||||
logging.info("On Debriefing")
|
||||
self.debriefing = QDebriefingWindow(debrief)
|
||||
|
||||
@ -133,10 +133,10 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self.setLayout(self.layout)
|
||||
|
||||
@staticmethod
|
||||
def add_update_row(description: str, count: Sized, layout: QGridLayout) -> None:
|
||||
def add_update_row(description: str, count: int, layout: QGridLayout) -> None:
|
||||
row = layout.rowCount()
|
||||
layout.addWidget(QLabel(f"<b>{description}</b>"), row, 0)
|
||||
layout.addWidget(QLabel(f"{len(count)}"), row, 1)
|
||||
layout.addWidget(QLabel(f"{count}"), row, 1)
|
||||
|
||||
def updateLayout(self, debriefing: Debriefing) -> None:
|
||||
updateBox = QGroupBox("Mission status")
|
||||
@ -145,34 +145,36 @@ class QWaitingForMissionResultWindow(QDialog):
|
||||
self.debriefing = debriefing
|
||||
|
||||
self.add_update_row(
|
||||
"Aircraft destroyed", list(debriefing.air_losses.losses), update_layout
|
||||
"Aircraft destroyed", len(list(debriefing.air_losses.losses)), update_layout
|
||||
)
|
||||
self.add_update_row(
|
||||
"Front line units destroyed",
|
||||
list(debriefing.front_line_losses),
|
||||
len(list(debriefing.front_line_losses)),
|
||||
update_layout,
|
||||
)
|
||||
self.add_update_row(
|
||||
"Convoy units destroyed", list(debriefing.convoy_losses), update_layout
|
||||
"Convoy units destroyed", len(list(debriefing.convoy_losses)), update_layout
|
||||
)
|
||||
self.add_update_row(
|
||||
"Shipping cargo destroyed",
|
||||
list(debriefing.cargo_ship_losses),
|
||||
len(list(debriefing.cargo_ship_losses)),
|
||||
update_layout,
|
||||
)
|
||||
self.add_update_row(
|
||||
"Airlift cargo destroyed", list(debriefing.airlift_losses), update_layout
|
||||
"Airlift cargo destroyed",
|
||||
sum(len(loss.cargo) for loss in debriefing.airlift_losses),
|
||||
update_layout,
|
||||
)
|
||||
self.add_update_row(
|
||||
"Ground units lost at objective areas",
|
||||
list(debriefing.ground_object_losses),
|
||||
len(list(debriefing.ground_object_losses)),
|
||||
update_layout,
|
||||
)
|
||||
self.add_update_row(
|
||||
"Buildings destroyed", list(debriefing.building_losses), update_layout
|
||||
"Buildings destroyed", len(list(debriefing.building_losses)), update_layout
|
||||
)
|
||||
self.add_update_row(
|
||||
"Base capture events", debriefing.base_captures, update_layout
|
||||
"Base capture events", len(debriefing.base_captures), update_layout
|
||||
)
|
||||
|
||||
# Clear previous content of the window
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from PySide2.QtCore import (
|
||||
QItemSelectionModel,
|
||||
@ -13,9 +14,13 @@ from PySide2.QtWidgets import (
|
||||
QVBoxLayout,
|
||||
QPushButton,
|
||||
QHBoxLayout,
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QCheckBox,
|
||||
)
|
||||
|
||||
from game.squadrons import Pilot
|
||||
from gen.flights.flight import FlightType
|
||||
from qt_ui.delegates import TwoColumnRowDelegate
|
||||
from qt_ui.models import SquadronModel
|
||||
|
||||
@ -61,6 +66,31 @@ class PilotList(QListView):
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
|
||||
|
||||
class AutoAssignedTaskControls(QVBoxLayout):
|
||||
def __init__(self, squadron_model: SquadronModel) -> None:
|
||||
super().__init__()
|
||||
self.squadron_model = squadron_model
|
||||
|
||||
self.addWidget(QLabel("Auto-assignable mission types"))
|
||||
|
||||
def make_callback(toggled_task: FlightType) -> Callable[[bool], None]:
|
||||
def callback(checked: bool) -> None:
|
||||
self.on_toggled(toggled_task, checked)
|
||||
|
||||
return callback
|
||||
|
||||
for task in squadron_model.squadron.mission_types:
|
||||
checkbox = QCheckBox(text=task.value)
|
||||
checkbox.setChecked(squadron_model.is_auto_assignable(task))
|
||||
checkbox.toggled.connect(make_callback(task))
|
||||
self.addWidget(checkbox)
|
||||
|
||||
self.addStretch()
|
||||
|
||||
def on_toggled(self, task: FlightType, checked: bool) -> None:
|
||||
self.squadron_model.set_auto_assignable(task, checked)
|
||||
|
||||
|
||||
class SquadronDialog(QDialog):
|
||||
"""Dialog window showing a squadron."""
|
||||
|
||||
@ -75,11 +105,17 @@ class SquadronDialog(QDialog):
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
columns = QHBoxLayout()
|
||||
layout.addLayout(columns)
|
||||
|
||||
auto_assigned_tasks = AutoAssignedTaskControls(squadron_model)
|
||||
columns.addLayout(auto_assigned_tasks)
|
||||
|
||||
self.pilot_list = PilotList(squadron_model)
|
||||
self.pilot_list.selectionModel().selectionChanged.connect(
|
||||
self.on_selection_changed
|
||||
)
|
||||
layout.addWidget(self.pilot_list)
|
||||
columns.addWidget(self.pilot_list)
|
||||
|
||||
button_panel = QHBoxLayout()
|
||||
button_panel.addStretch()
|
||||
|
||||
@ -11,7 +11,12 @@ from PySide2.QtWidgets import (
|
||||
)
|
||||
|
||||
from game import Game, db
|
||||
from game.theater import ControlPoint, ControlPointType
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
ControlPointType,
|
||||
FREE_FRONTLINE_UNIT_SUPPLY,
|
||||
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION,
|
||||
)
|
||||
from gen.flights.flight import FlightType
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.models import GameModel
|
||||
@ -44,8 +49,8 @@ class QBaseMenu2(QDialog):
|
||||
|
||||
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
self.setMinimumSize(300, 200)
|
||||
self.setMinimumWidth(800)
|
||||
self.setMaximumWidth(800)
|
||||
self.setMinimumWidth(1024)
|
||||
self.setMaximumWidth(1024)
|
||||
self.setModal(True)
|
||||
|
||||
self.setWindowTitle(self.cp.name)
|
||||
@ -62,6 +67,7 @@ class QBaseMenu2(QDialog):
|
||||
title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
title.setProperty("style", "base-title")
|
||||
self.intel_summary = QLabel()
|
||||
self.intel_summary.setToolTip(self.generate_intel_tooltip())
|
||||
self.update_intel_summary()
|
||||
top_layout.addWidget(title)
|
||||
top_layout.addWidget(self.intel_summary)
|
||||
@ -195,16 +201,49 @@ class QBaseMenu2(QDialog):
|
||||
def update_intel_summary(self) -> None:
|
||||
aircraft = self.cp.base.total_aircraft
|
||||
parking = self.cp.total_aircraft_parking
|
||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||
deployable_unit_info = ""
|
||||
|
||||
allocated = self.cp.allocated_ground_units(self.game_model.game.transfers)
|
||||
unit_overage = max(
|
||||
allocated.total_present - self.cp.frontline_unit_count_limit, 0
|
||||
)
|
||||
if self.cp.has_active_frontline:
|
||||
deployable_unit_info = (
|
||||
f" (Up to {ground_unit_limit} deployable, {unit_overage} reserve)"
|
||||
)
|
||||
|
||||
self.intel_summary.setText(
|
||||
"\n".join(
|
||||
[
|
||||
f"{aircraft}/{parking} aircraft",
|
||||
f"{self.cp.base.total_armor} ground units",
|
||||
f"{self.cp.base.total_armor} ground units" + deployable_unit_info,
|
||||
f"{allocated.total_transferring} more ground units en route, {allocated.total_ordered} ordered",
|
||||
str(self.cp.runway_status),
|
||||
f"{self.cp.active_ammo_depots_count}/{self.cp.total_ammo_depots_count} ammo depots",
|
||||
f"{'Factory can produce units' if self.cp.has_factory else 'Does not have a factory'}",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def generate_intel_tooltip(self) -> str:
|
||||
tooltip = (
|
||||
f"Deployable unit limit ({self.cp.frontline_unit_count_limit}) = {FREE_FRONTLINE_UNIT_SUPPLY} (base) + "
|
||||
f" {AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION} (per connected ammo depot) * {self.cp.total_ammo_depots_count} "
|
||||
f"(depots)"
|
||||
)
|
||||
|
||||
if self.cp.has_active_frontline:
|
||||
unit_overage = max(
|
||||
self.cp.base.total_armor - self.cp.frontline_unit_count_limit, 0
|
||||
)
|
||||
tooltip += (
|
||||
f"\n{unit_overage} units will be held in reserve and will not be deployed to "
|
||||
f"connected frontlines for this turn"
|
||||
)
|
||||
|
||||
return tooltip
|
||||
|
||||
def closeEvent(self, close_event: QCloseEvent):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ from game.theater import ControlPoint, OffMapSpawn, Fob
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.basemenu.DepartingConvoysMenu import DepartingConvoysMenu
|
||||
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand
|
||||
from qt_ui.windows.basemenu.base_defenses.QBaseDefensesHQ import QBaseDefensesHQ
|
||||
from qt_ui.windows.basemenu.ground_forces.QGroundForcesHQ import QGroundForcesHQ
|
||||
from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo
|
||||
|
||||
@ -14,9 +13,6 @@ class QBaseMenuTabs(QTabWidget):
|
||||
super(QBaseMenuTabs, self).__init__()
|
||||
|
||||
if not cp.captured:
|
||||
if not cp.is_carrier and not isinstance(cp, OffMapSpawn):
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||
self.addTab(self.base_defenses_hq, "Base Defenses")
|
||||
self.intel = QIntelInfo(cp, game_model.game)
|
||||
self.addTab(self.intel, "Intel")
|
||||
|
||||
@ -30,17 +26,9 @@ class QBaseMenuTabs(QTabWidget):
|
||||
if cp.helipads:
|
||||
self.airfield_command = QAirfieldCommand(cp, game_model)
|
||||
self.addTab(self.airfield_command, "Heliport")
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||
self.addTab(self.base_defenses_hq, "Base Defenses")
|
||||
else:
|
||||
self.airfield_command = QAirfieldCommand(cp, game_model)
|
||||
self.addTab(self.airfield_command, "Airfield Command")
|
||||
|
||||
if cp.is_carrier:
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||
self.addTab(self.base_defenses_hq, "Fleet")
|
||||
elif not isinstance(cp, OffMapSpawn):
|
||||
if not isinstance(cp, OffMapSpawn):
|
||||
self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
|
||||
self.addTab(self.ground_forces_hq, "Ground Forces HQ")
|
||||
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
|
||||
self.addTab(self.base_defenses_hq, "Base Defenses")
|
||||
|
||||
@ -164,16 +164,16 @@ class QHangarStatus(QHBoxLayout):
|
||||
self.setAlignment(Qt.AlignLeft)
|
||||
|
||||
def update_label(self) -> None:
|
||||
next_turn = self.control_point.expected_aircraft_next_turn(self.game_model.game)
|
||||
next_turn = self.control_point.allocated_aircraft(self.game_model.game)
|
||||
max_amount = self.control_point.total_aircraft_parking
|
||||
|
||||
components = [f"{next_turn.present} present"]
|
||||
if next_turn.ordered > 0:
|
||||
components.append(f"{next_turn.ordered} purchased")
|
||||
elif next_turn.ordered < 0:
|
||||
components.append(f"{-next_turn.ordered} sold")
|
||||
if next_turn.total_ordered > 0:
|
||||
components.append(f"{next_turn.total_ordered} purchased")
|
||||
elif next_turn.total_ordered < 0:
|
||||
components.append(f"{-next_turn.total_ordered} sold")
|
||||
|
||||
transferring = next_turn.transferring
|
||||
transferring = next_turn.total_transferring
|
||||
if transferring > 0:
|
||||
components.append(f"{transferring} transferring in")
|
||||
if transferring < 0:
|
||||
|
||||
@ -1,106 +0,0 @@
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from qt_ui.dialogs import Dialog
|
||||
from qt_ui.uiconstants import VEHICLES_ICONS
|
||||
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
|
||||
from game import db
|
||||
from dcs import vehicles
|
||||
|
||||
|
||||
class QBaseDefenseGroupInfo(QGroupBox):
|
||||
def __init__(self, cp: ControlPoint, ground_object: TheaterGroundObject, game):
|
||||
super(QBaseDefenseGroupInfo, self).__init__("Group : " + ground_object.obj_name)
|
||||
self.ground_object = ground_object
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.buildings = game.theater.find_ground_objects_by_obj_name(
|
||||
self.ground_object.obj_name
|
||||
)
|
||||
|
||||
self.main_layout = QVBoxLayout()
|
||||
self.unit_layout = QGridLayout()
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
|
||||
self.buildLayout()
|
||||
self.main_layout.addLayout(self.unit_layout)
|
||||
if not self.cp.captured and not self.ground_object.is_dead:
|
||||
attack_button = QPushButton("Attack")
|
||||
attack_button.setProperty("style", "btn-danger")
|
||||
attack_button.setMaximumWidth(180)
|
||||
attack_button.clicked.connect(self.onAttack)
|
||||
self.main_layout.addWidget(attack_button, 0, Qt.AlignLeft)
|
||||
|
||||
if self.cp.captured:
|
||||
manage_button = QPushButton("Manage")
|
||||
manage_button.setProperty("style", "btn-success")
|
||||
manage_button.setMaximumWidth(180)
|
||||
manage_button.clicked.connect(self.onManage)
|
||||
self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft)
|
||||
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
def buildLayout(self):
|
||||
unit_dict = {}
|
||||
for i in range(self.unit_layout.rowCount()):
|
||||
for j in range(self.unit_layout.columnCount()):
|
||||
item = self.unit_layout.itemAtPosition(i, j)
|
||||
if item is not None and item.widget() is not None:
|
||||
item.widget().setParent(None)
|
||||
print("Remove " + str(i) + ", " + str(j))
|
||||
|
||||
for g in self.ground_object.groups:
|
||||
for u in g.units:
|
||||
if u.type in unit_dict.keys():
|
||||
unit_dict[u.type] = unit_dict[u.type] + 1
|
||||
else:
|
||||
unit_dict[u.type] = 1
|
||||
i = 0
|
||||
for k, v in unit_dict.items():
|
||||
icon = QLabel()
|
||||
if k in VEHICLES_ICONS.keys():
|
||||
icon.setPixmap(VEHICLES_ICONS[k])
|
||||
else:
|
||||
icon.setText("<b>" + k[:8] + "</b>")
|
||||
icon.setProperty("style", "icon-armor")
|
||||
self.unit_layout.addWidget(icon, i, 0)
|
||||
unit_display_name = k
|
||||
unit_type = vehicles.vehicle_map.get(k)
|
||||
if unit_type is not None:
|
||||
unit_display_name = db.unit_get_expanded_info(
|
||||
self.game.enemy_country, unit_type, "name"
|
||||
)
|
||||
self.unit_layout.addWidget(
|
||||
QLabel(str(v) + " x " + "<strong>" + unit_display_name + "</strong>"),
|
||||
i,
|
||||
1,
|
||||
)
|
||||
i = i + 1
|
||||
|
||||
if len(unit_dict.items()) == 0:
|
||||
self.unit_layout.addWidget(QLabel("/"), 0, 0)
|
||||
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
def onAttack(self):
|
||||
Dialog.open_new_package_dialog(self.ground_object, parent=self.window())
|
||||
|
||||
def onManage(self):
|
||||
self.edition_menu = QGroundObjectMenu(
|
||||
self.window(), self.ground_object, self.buildings, self.cp, self.game
|
||||
)
|
||||
self.edition_menu.show()
|
||||
self.edition_menu.changed.connect(self.onEdition)
|
||||
|
||||
def onEdition(self):
|
||||
self.buildLayout()
|
||||
@ -1,19 +0,0 @@
|
||||
from PySide2.QtWidgets import QFrame, QGridLayout
|
||||
|
||||
from game import Game
|
||||
from game.theater import ControlPoint
|
||||
from qt_ui.windows.basemenu.base_defenses.QBaseInformation import QBaseInformation
|
||||
|
||||
|
||||
class QBaseDefensesHQ(QFrame):
|
||||
def __init__(self, cp: ControlPoint, game: Game):
|
||||
super(QBaseDefensesHQ, self).__init__()
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
airport = self.game.theater.terrain.airport_by_id(self.cp.id)
|
||||
layout = QGridLayout()
|
||||
layout.addWidget(QBaseInformation(self.cp, airport, self.game))
|
||||
self.setLayout(layout)
|
||||
@ -1,56 +0,0 @@
|
||||
from PySide2.QtGui import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QFrame,
|
||||
QGridLayout,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from game.theater import Airport, ControlPoint, Fob
|
||||
from game.theater.theatergroundobject import BuildingGroundObject
|
||||
from qt_ui.windows.basemenu.base_defenses.QBaseDefenseGroupInfo import (
|
||||
QBaseDefenseGroupInfo,
|
||||
)
|
||||
|
||||
|
||||
class QBaseInformation(QFrame):
|
||||
def __init__(self, cp: ControlPoint, airport: Airport, game):
|
||||
super(QBaseInformation, self).__init__()
|
||||
self.cp = cp
|
||||
self.airport = airport
|
||||
self.game = game
|
||||
self.setMinimumWidth(500)
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
self.mainLayout = QVBoxLayout()
|
||||
|
||||
scroll_content = QWidget()
|
||||
task_box_layout = QGridLayout()
|
||||
scroll_content.setLayout(task_box_layout)
|
||||
|
||||
for g in self.cp.ground_objects:
|
||||
# Airbase groups are the objects that are hidden on the map because
|
||||
# they're shown in the base menu.
|
||||
if not g.airbase_group:
|
||||
continue
|
||||
|
||||
# Of these, we need to ignore the FOB structure itself since that's
|
||||
# not supposed to be targetable.
|
||||
if isinstance(self.cp, Fob) and isinstance(g, BuildingGroundObject):
|
||||
continue
|
||||
|
||||
group_info = QBaseDefenseGroupInfo(self.cp, g, self.game)
|
||||
task_box_layout.addWidget(group_info)
|
||||
|
||||
scroll_content.setLayout(task_box_layout)
|
||||
scroll = QScrollArea()
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setWidget(scroll_content)
|
||||
|
||||
self.mainLayout.addWidget(scroll)
|
||||
|
||||
self.setLayout(self.mainLayout)
|
||||
@ -1,7 +1,10 @@
|
||||
from PySide2.QtWidgets import QGroupBox, QLabel, QVBoxLayout
|
||||
from collections import Callable
|
||||
|
||||
from PySide2.QtWidgets import QGroupBox, QLabel, QVBoxLayout, QPushButton
|
||||
|
||||
from game import Game
|
||||
from game.theater import ControlPoint
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import (
|
||||
QGroundForcesStrategySelector,
|
||||
)
|
||||
@ -15,10 +18,44 @@ class QGroundForcesStrategy(QGroupBox):
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
def make_cheat_callback(
|
||||
enemy_point: ControlPoint, advance: bool
|
||||
) -> Callable[[], None]:
|
||||
def cheat() -> None:
|
||||
self.cheat_alter_front_line(enemy_point, advance)
|
||||
|
||||
return cheat
|
||||
|
||||
layout = QVBoxLayout()
|
||||
for enemy_cp in self.cp.connected_points:
|
||||
if not enemy_cp.captured:
|
||||
layout.addWidget(QLabel(enemy_cp.name))
|
||||
layout.addWidget(QGroundForcesStrategySelector(self.cp, enemy_cp))
|
||||
if self.game.settings.enable_frontline_cheats:
|
||||
advance_button = QPushButton("CHEAT: Advance")
|
||||
advance_button.setProperty("style", "btn-danger")
|
||||
layout.addWidget(advance_button)
|
||||
advance_button.clicked.connect(
|
||||
make_cheat_callback(enemy_cp, advance=True)
|
||||
)
|
||||
|
||||
retreat_button = QPushButton("CHEAT: Retreat")
|
||||
retreat_button.setProperty("style", "btn-danger")
|
||||
layout.addWidget(retreat_button)
|
||||
retreat_button.clicked.connect(
|
||||
make_cheat_callback(enemy_cp, advance=False)
|
||||
)
|
||||
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
def cheat_alter_front_line(self, enemy_point: ControlPoint, advance: bool) -> None:
|
||||
amount = 0.2
|
||||
if not advance:
|
||||
amount *= -1
|
||||
self.cp.base.affect_strength(amount)
|
||||
enemy_point.base.affect_strength(-amount)
|
||||
# Clear the ATO to replan missions affected by the front line.
|
||||
self.game.reset_ato()
|
||||
self.game.initialize_turn()
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
@ -21,8 +21,15 @@ from game import Game, db
|
||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||
from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import NavalGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
NavalGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
SamGroundObject,
|
||||
EwrGroundObject,
|
||||
BuildingGroundObject,
|
||||
)
|
||||
from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
|
||||
from gen.sam.ewr_group_generator import get_faction_possible_ewrs_generator
|
||||
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
@ -32,9 +39,6 @@ from dcs import vehicles
|
||||
|
||||
|
||||
class QGroundObjectMenu(QDialog):
|
||||
|
||||
changed = QtCore.Signal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
@ -70,12 +74,12 @@ class QGroundObjectMenu(QDialog):
|
||||
|
||||
self.doLayout()
|
||||
|
||||
if self.ground_object.dcs_identifier == "AA":
|
||||
self.mainLayout.addWidget(self.intelBox)
|
||||
else:
|
||||
if isinstance(self.ground_object, BuildingGroundObject):
|
||||
self.mainLayout.addWidget(self.buildingBox)
|
||||
if self.cp.captured:
|
||||
self.mainLayout.addWidget(self.financesBox)
|
||||
else:
|
||||
self.mainLayout.addWidget(self.intelBox)
|
||||
|
||||
self.actionLayout = QHBoxLayout()
|
||||
|
||||
@ -87,12 +91,12 @@ class QGroundObjectMenu(QDialog):
|
||||
self.buy_replace.clicked.connect(self.buy_group)
|
||||
self.buy_replace.setProperty("style", "btn-success")
|
||||
|
||||
if not isinstance(self.ground_object, NavalGroundObject):
|
||||
if self.ground_object.purchasable:
|
||||
if self.total_value > 0:
|
||||
self.actionLayout.addWidget(self.sell_all_button)
|
||||
self.actionLayout.addWidget(self.buy_replace)
|
||||
|
||||
if self.cp.captured and self.ground_object.dcs_identifier == "AA":
|
||||
if self.cp.captured and self.ground_object.purchasable:
|
||||
self.mainLayout.addLayout(self.actionLayout)
|
||||
self.setLayout(self.mainLayout)
|
||||
|
||||
@ -196,23 +200,21 @@ class QGroundObjectMenu(QDialog):
|
||||
self.actionLayout.setParent(None)
|
||||
|
||||
self.doLayout()
|
||||
if self.ground_object.dcs_identifier == "AA":
|
||||
self.mainLayout.addWidget(self.intelBox)
|
||||
else:
|
||||
if isinstance(self.ground_object, BuildingGroundObject):
|
||||
self.mainLayout.addWidget(self.buildingBox)
|
||||
else:
|
||||
self.mainLayout.addWidget(self.intelBox)
|
||||
|
||||
self.actionLayout = QHBoxLayout()
|
||||
if self.total_value > 0:
|
||||
self.actionLayout.addWidget(self.sell_all_button)
|
||||
self.actionLayout.addWidget(self.buy_replace)
|
||||
|
||||
if self.cp.captured and self.ground_object.dcs_identifier == "AA":
|
||||
if self.cp.captured and self.ground_object.purchasable:
|
||||
self.mainLayout.addLayout(self.actionLayout)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logging.exception(e)
|
||||
self.update_total_value()
|
||||
self.changed.emit()
|
||||
|
||||
def update_total_value(self):
|
||||
total_value = 0
|
||||
@ -244,7 +246,6 @@ class QGroundObjectMenu(QDialog):
|
||||
logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type))
|
||||
|
||||
self.do_refresh_layout()
|
||||
self.changed.emit()
|
||||
|
||||
def sell_all(self):
|
||||
self.update_total_value()
|
||||
@ -294,9 +295,6 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
self.buySamBox = QGroupBox("Buy SAM site :")
|
||||
self.buyArmorBox = QGroupBox("Buy defensive position :")
|
||||
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
faction = self.game.player_faction
|
||||
|
||||
# Sams
|
||||
@ -317,6 +315,30 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
|
||||
self.buySamButton.clicked.connect(self.buySam)
|
||||
|
||||
# EWRs
|
||||
|
||||
buy_ewr_box = QGroupBox("Buy EWR:")
|
||||
buy_ewr_layout = QGridLayout()
|
||||
buy_ewr_box.setLayout(buy_ewr_layout)
|
||||
|
||||
buy_ewr_layout.addWidget(QLabel("Radar type:"), 0, 0, Qt.AlignLeft)
|
||||
|
||||
self.ewr_selector = QComboBox()
|
||||
buy_ewr_layout.addWidget(self.ewr_selector, 0, 1, alignment=Qt.AlignRight)
|
||||
ewr_types = get_faction_possible_ewrs_generator(faction)
|
||||
for ewr_type in ewr_types:
|
||||
self.ewr_selector.addItem(
|
||||
f"{ewr_type.name()} [${ewr_type.price()}M]", ewr_type
|
||||
)
|
||||
self.ewr_selector.currentIndexChanged.connect(self.on_ewr_selection_changed)
|
||||
|
||||
self.buy_ewr_button = QPushButton("Buy")
|
||||
self.buy_ewr_button.clicked.connect(self.buy_ewr)
|
||||
buy_ewr_layout.addWidget(self.buy_ewr_button, 1, 1, alignment=Qt.AlignRight)
|
||||
stretch = QVBoxLayout()
|
||||
stretch.addStretch()
|
||||
buy_ewr_layout.addLayout(stretch, 2, 0)
|
||||
|
||||
# Armored units
|
||||
|
||||
armored_units = db.find_unittype(
|
||||
@ -354,16 +376,20 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
self.buyArmorBox.setLayout(self.buyArmorLayout)
|
||||
|
||||
self.mainLayout = QHBoxLayout()
|
||||
self.mainLayout.addWidget(self.buySamBox)
|
||||
|
||||
if self.ground_object.airbase_group:
|
||||
if isinstance(self.ground_object, SamGroundObject):
|
||||
self.mainLayout.addWidget(self.buySamBox)
|
||||
elif isinstance(self.ground_object, VehicleGroupGroundObject):
|
||||
self.mainLayout.addWidget(self.buyArmorBox)
|
||||
elif isinstance(self.ground_object, EwrGroundObject):
|
||||
self.mainLayout.addWidget(buy_ewr_box)
|
||||
|
||||
self.setLayout(self.mainLayout)
|
||||
|
||||
try:
|
||||
self.samComboChanged(0)
|
||||
self.armorComboChanged(0)
|
||||
self.on_ewr_selection_changed(0)
|
||||
except:
|
||||
pass
|
||||
|
||||
@ -376,6 +402,12 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
+ "M]"
|
||||
)
|
||||
|
||||
def on_ewr_selection_changed(self, index):
|
||||
ewr = self.ewr_selector.itemData(index)
|
||||
self.buy_ewr_button.setText(
|
||||
f"Buy [${ewr.price()}M][-${self.current_group_value}M]"
|
||||
)
|
||||
|
||||
def armorComboChanged(self, index):
|
||||
self.buyArmorButton.setText(
|
||||
"Buy [$"
|
||||
@ -441,6 +473,24 @@ class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
self.changed.emit()
|
||||
self.close()
|
||||
|
||||
def buy_ewr(self):
|
||||
ewr_generator = self.ewr_selector.itemData(self.ewr_selector.currentIndex())
|
||||
price = ewr_generator.price() - self.current_group_value
|
||||
if price > self.game.budget:
|
||||
self.error_money()
|
||||
return
|
||||
else:
|
||||
self.game.budget -= price
|
||||
|
||||
generator = ewr_generator(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
self.ground_object.groups = [generator.vg]
|
||||
|
||||
GameUpdateSignal.get_instance().updateBudget(self.game)
|
||||
|
||||
self.changed.emit()
|
||||
self.close()
|
||||
|
||||
def error_money(self):
|
||||
msg = QMessageBox()
|
||||
msg.setIcon(QMessageBox.Information)
|
||||
|
||||
@ -221,6 +221,19 @@ class QNewPackageDialog(QPackageDialog):
|
||||
)
|
||||
self.ato_model = model
|
||||
|
||||
# In the *new* package dialog, a package has been created and may have aircraft
|
||||
# assigned to it, but it is not a part of the ATO until the user saves it.
|
||||
#
|
||||
# Other actions (modifying settings, closing some other dialogs like the base
|
||||
# menu) can cause a Game update which will forcibly close this window without
|
||||
# either accepting or rejecting it, so we neither save the package nor release
|
||||
# any allocated units.
|
||||
#
|
||||
# While it would be preferable to be able to update this dialog as needed in the
|
||||
# event of game updates, the quick fix is to just not allow interaction with
|
||||
# other UI elements until the new package has either been finalized or canceled.
|
||||
self.setModal(True)
|
||||
|
||||
self.save_button = QPushButton("Save")
|
||||
self.save_button.setProperty("style", "start-button")
|
||||
self.save_button.clicked.connect(self.accept)
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from PySide2.QtCore import QItemSelectionModel, QSize
|
||||
from PySide2.QtGui import QStandardItemModel
|
||||
from PySide2.QtWidgets import QAbstractItemView, QListView
|
||||
@ -5,6 +7,7 @@ from PySide2.QtWidgets import QAbstractItemView, QListView
|
||||
from qt_ui.models import GameModel
|
||||
from qt_ui.windows.mission.QFlightItem import QFlightItem
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
|
||||
class QPlannedFlightsView(QListView):
|
||||
@ -25,8 +28,11 @@ class QPlannedFlightsView(QListView):
|
||||
for flight in package.flights:
|
||||
if flight.from_cp == self.cp:
|
||||
item = QFlightItem(package.package, flight)
|
||||
self.model.appendRow(item)
|
||||
self.flight_items.append(item)
|
||||
|
||||
self.flight_items.sort(key=self.mission_start_for_flight)
|
||||
for item in self.flight_items:
|
||||
self.model.appendRow(item)
|
||||
self.set_selected_flight(0)
|
||||
|
||||
def set_selected_flight(self, row):
|
||||
@ -43,3 +49,7 @@ class QPlannedFlightsView(QListView):
|
||||
def set_flight_planner(self) -> None:
|
||||
self.clear_layout()
|
||||
self.setup_content()
|
||||
|
||||
@staticmethod
|
||||
def mission_start_for_flight(flight_item: QFlightItem) -> timedelta:
|
||||
return TotEstimator(flight_item.package).mission_start_time(flight_item.flight)
|
||||
|
||||
@ -10,6 +10,7 @@ from PySide2.QtWidgets import (
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QLineEdit,
|
||||
QHBoxLayout,
|
||||
)
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
@ -17,7 +18,7 @@ from game import Game
|
||||
from game.squadrons import Squadron
|
||||
from game.theater import ControlPoint, OffMapSpawn
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flight import Flight, FlightRoster
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
|
||||
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
|
||||
@ -26,6 +27,7 @@ from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelect
|
||||
from qt_ui.widgets.combos.QFlightTypeComboBox import QFlightTypeComboBox
|
||||
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
|
||||
from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector
|
||||
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import FlightRosterEditor
|
||||
|
||||
|
||||
class QFlightCreator(QDialog):
|
||||
@ -46,7 +48,7 @@ class QFlightCreator(QDialog):
|
||||
|
||||
self.task_selector = QFlightTypeComboBox(self.game.theater, package.target)
|
||||
self.task_selector.setCurrentIndex(0)
|
||||
self.task_selector.currentTextChanged.connect(self.on_task_changed)
|
||||
self.task_selector.currentIndexChanged.connect(self.on_task_changed)
|
||||
layout.addLayout(QLabeledWidget("Task:", self.task_selector))
|
||||
|
||||
self.aircraft_selector = QAircraftTypeSelector(
|
||||
@ -93,13 +95,20 @@ class QFlightCreator(QDialog):
|
||||
self.update_max_size(self.departure.available)
|
||||
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
|
||||
|
||||
self.client_slots_spinner = QFlightSizeSpinner(
|
||||
min_size=0, max_size=self.flight_size_spinner.value(), default_size=0
|
||||
)
|
||||
self.flight_size_spinner.valueChanged.connect(
|
||||
lambda v: self.client_slots_spinner.setMaximum(v)
|
||||
)
|
||||
layout.addLayout(QLabeledWidget("Client Slots:", self.client_slots_spinner))
|
||||
squadron = self.squadron_selector.currentData()
|
||||
if squadron is None:
|
||||
roster = None
|
||||
else:
|
||||
roster = FlightRoster(
|
||||
squadron, initial_size=self.flight_size_spinner.value()
|
||||
)
|
||||
self.roster_editor = FlightRosterEditor(roster)
|
||||
self.flight_size_spinner.valueChanged.connect(self.resize_roster)
|
||||
self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed)
|
||||
roster_layout = QHBoxLayout()
|
||||
layout.addLayout(roster_layout)
|
||||
roster_layout.addWidget(QLabel("Assigned pilots:"))
|
||||
roster_layout.addLayout(self.roster_editor)
|
||||
|
||||
# When an off-map spawn overrides the start type to in-flight, we save
|
||||
# the selected type into this value. If a non-off-map spawn is selected
|
||||
@ -142,6 +151,10 @@ class QFlightCreator(QDialog):
|
||||
def set_custom_name_text(self, text: str):
|
||||
self.custom_name_text = text
|
||||
|
||||
def resize_roster(self, new_size: int) -> None:
|
||||
self.roster_editor.roster.resize(new_size)
|
||||
self.roster_editor.resize(new_size)
|
||||
|
||||
def verify_form(self) -> Optional[str]:
|
||||
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
|
||||
squadron: Optional[Squadron] = self.squadron_selector.currentData()
|
||||
@ -181,7 +194,7 @@ class QFlightCreator(QDialog):
|
||||
origin = self.departure.currentData()
|
||||
arrival = self.arrival.currentData()
|
||||
divert = self.divert.currentData()
|
||||
size = self.flight_size_spinner.value()
|
||||
roster = self.roster_editor.roster
|
||||
|
||||
if arrival is None:
|
||||
arrival = origin
|
||||
@ -190,22 +203,17 @@ class QFlightCreator(QDialog):
|
||||
self.package,
|
||||
self.country,
|
||||
squadron,
|
||||
size,
|
||||
# A bit of a hack to work around the old API. Not actually relevant because
|
||||
# the roster is passed explicitly. Needs a refactor.
|
||||
roster.max_size,
|
||||
task,
|
||||
self.start_type.currentText(),
|
||||
origin,
|
||||
arrival,
|
||||
divert,
|
||||
custom_name=self.custom_name_text,
|
||||
roster=roster,
|
||||
)
|
||||
for pilot, idx in zip(flight.pilots, range(self.client_slots_spinner.value())):
|
||||
if pilot is None:
|
||||
logging.error(
|
||||
f"Cannot create client slot because {flight} has no pilot for "
|
||||
f"aircraft {idx}"
|
||||
)
|
||||
continue
|
||||
pilot.player = True
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
self.created.emit(flight)
|
||||
@ -234,14 +242,22 @@ class QFlightCreator(QDialog):
|
||||
self.start_type.setCurrentText(self.restore_start_type)
|
||||
self.restore_start_type = None
|
||||
|
||||
def on_task_changed(self) -> None:
|
||||
def on_task_changed(self, index: int) -> None:
|
||||
task = self.task_selector.itemData(index)
|
||||
self.aircraft_selector.update_items(
|
||||
self.task_selector.currentData(),
|
||||
self.game.aircraft_inventory.available_types_for_player,
|
||||
)
|
||||
self.squadron_selector.update_items(
|
||||
self.task_selector.currentData(), self.aircraft_selector.currentData()
|
||||
task, self.game.aircraft_inventory.available_types_for_player
|
||||
)
|
||||
self.squadron_selector.update_items(task, self.aircraft_selector.currentData())
|
||||
|
||||
def on_squadron_changed(self, index: int) -> None:
|
||||
squadron = self.squadron_selector.itemData(index)
|
||||
# Clear the roster first so we return the pilots to the pool. This way if we end
|
||||
# up repopulating from the same squadron we'll get the same pilots back.
|
||||
self.roster_editor.replace(None)
|
||||
if squadron is not None:
|
||||
self.roster_editor.replace(
|
||||
FlightRoster(squadron, self.flight_size_spinner.value())
|
||||
)
|
||||
|
||||
def update_max_size(self, available: int) -> None:
|
||||
self.flight_size_spinner.setMaximum(min(available, 4))
|
||||
|
||||
@ -28,7 +28,11 @@ class SquadronSelector(QComboBox):
|
||||
self, task: Optional[FlightType], aircraft: Optional[Type[FlyingType]]
|
||||
) -> None:
|
||||
current_squadron = self.currentData()
|
||||
self.clear()
|
||||
self.blockSignals(True)
|
||||
try:
|
||||
self.clear()
|
||||
finally:
|
||||
self.blockSignals(False)
|
||||
if task is None:
|
||||
self.addItem("No task selected", None)
|
||||
return
|
||||
|
||||
@ -13,7 +13,11 @@ class DcsLoadoutSelector(QComboBox):
|
||||
for loadout in Loadout.iter_for(flight):
|
||||
self.addItem(loadout.name, loadout)
|
||||
self.model().sort(0)
|
||||
self.setCurrentText(flight.loadout.name)
|
||||
self.setDisabled(flight.loadout.is_custom)
|
||||
if flight.loadout.is_custom:
|
||||
self.setCurrentText(Loadout.default_for(flight).name)
|
||||
else:
|
||||
self.setCurrentText(flight.loadout.name)
|
||||
|
||||
|
||||
class QFlightPayloadTab(QFrame):
|
||||
|
||||
@ -15,16 +15,16 @@ from PySide2.QtWidgets import (
|
||||
|
||||
from game import Game
|
||||
from game.squadrons import Pilot
|
||||
from gen.flights.flight import Flight
|
||||
from gen.flights.flight import Flight, FlightRoster
|
||||
from qt_ui.models import PackageModel
|
||||
|
||||
|
||||
class PilotSelector(QComboBox):
|
||||
available_pilots_changed = Signal()
|
||||
|
||||
def __init__(self, flight: Flight, idx: int) -> None:
|
||||
def __init__(self, roster: Optional[FlightRoster], idx: int) -> None:
|
||||
super().__init__()
|
||||
self.flight = flight
|
||||
self.roster = roster
|
||||
self.pilot_index = idx
|
||||
self.rebuild()
|
||||
|
||||
@ -34,15 +34,15 @@ class PilotSelector(QComboBox):
|
||||
|
||||
def _do_rebuild(self) -> None:
|
||||
self.clear()
|
||||
if self.pilot_index >= self.flight.count:
|
||||
if self.roster is None or self.pilot_index >= self.roster.max_size:
|
||||
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]
|
||||
choices = list(self.roster.squadron.available_pilots)
|
||||
current_pilot = self.roster.pilots[self.pilot_index]
|
||||
if current_pilot is not None:
|
||||
choices.append(current_pilot)
|
||||
# Put players first, otherwise alphabetically.
|
||||
@ -70,19 +70,23 @@ class PilotSelector(QComboBox):
|
||||
# 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]:
|
||||
if pilot == self.roster.pilots[self.pilot_index]:
|
||||
return
|
||||
self.flight.set_pilot(self.pilot_index, pilot)
|
||||
self.roster.set_pilot(self.pilot_index, pilot)
|
||||
self.available_pilots_changed.emit()
|
||||
|
||||
def replace(self, new_roster: Optional[FlightRoster]) -> None:
|
||||
self.roster = new_roster
|
||||
self.rebuild()
|
||||
|
||||
|
||||
class PilotControls(QHBoxLayout):
|
||||
def __init__(self, flight: Flight, idx: int) -> None:
|
||||
def __init__(self, roster: Optional[FlightRoster], idx: int) -> None:
|
||||
super().__init__()
|
||||
self.flight = flight
|
||||
self.roster = roster
|
||||
self.pilot_index = idx
|
||||
|
||||
self.selector = PilotSelector(flight, idx)
|
||||
self.selector = PilotSelector(roster, idx)
|
||||
self.selector.currentIndexChanged.connect(self.on_pilot_changed)
|
||||
self.addWidget(self.selector)
|
||||
|
||||
@ -95,9 +99,9 @@ class PilotControls(QHBoxLayout):
|
||||
|
||||
@property
|
||||
def pilot(self) -> Optional[Pilot]:
|
||||
if self.pilot_index >= self.flight.count:
|
||||
if self.roster is None or self.pilot_index >= self.roster.max_size:
|
||||
return None
|
||||
return self.flight.pilots[self.pilot_index]
|
||||
return self.roster.pilots[self.pilot_index]
|
||||
|
||||
def on_player_toggled(self, checked: bool) -> None:
|
||||
pilot = self.pilot
|
||||
@ -130,12 +134,21 @@ class PilotControls(QHBoxLayout):
|
||||
finally:
|
||||
self.player_checkbox.blockSignals(False)
|
||||
|
||||
def replace(self, new_roster: Optional[FlightRoster]) -> None:
|
||||
self.roster = new_roster
|
||||
if self.roster is None or self.pilot_index >= self.roster.max_size:
|
||||
self.disable_and_clear()
|
||||
else:
|
||||
self.enable_and_reset()
|
||||
self.selector.replace(new_roster)
|
||||
|
||||
|
||||
class FlightRosterEditor(QVBoxLayout):
|
||||
MAX_PILOTS = 4
|
||||
|
||||
def __init__(self, flight: Flight) -> None:
|
||||
def __init__(self, roster: Optional[FlightRoster]) -> None:
|
||||
super().__init__()
|
||||
self.roster = roster
|
||||
|
||||
self.pilot_controls = []
|
||||
for pilot_idx in range(self.MAX_PILOTS):
|
||||
@ -146,7 +159,7 @@ class FlightRosterEditor(QVBoxLayout):
|
||||
|
||||
return callback
|
||||
|
||||
controls = PilotControls(flight, pilot_idx)
|
||||
controls = PilotControls(roster, pilot_idx)
|
||||
controls.selector.available_pilots_changed.connect(
|
||||
make_reset_callback(pilot_idx)
|
||||
)
|
||||
@ -167,6 +180,13 @@ class FlightRosterEditor(QVBoxLayout):
|
||||
for controls in self.pilot_controls[new_size:]:
|
||||
controls.disable_and_clear()
|
||||
|
||||
def replace(self, new_roster: Optional[FlightRoster]) -> None:
|
||||
if self.roster is not None:
|
||||
self.roster.clear()
|
||||
self.roster = new_roster
|
||||
for controls in self.pilot_controls:
|
||||
controls.replace(new_roster)
|
||||
|
||||
|
||||
class QFlightSlotEditor(QGroupBox):
|
||||
def __init__(self, package_model: PackageModel, flight: Flight, game: Game):
|
||||
@ -196,14 +216,16 @@ class QFlightSlotEditor(QGroupBox):
|
||||
layout.addWidget(QLabel(str(self.flight.squadron)), 1, 1)
|
||||
|
||||
layout.addWidget(QLabel("Assigned pilots:"), 2, 0)
|
||||
self.roster_editor = FlightRosterEditor(flight)
|
||||
self.roster_editor = FlightRosterEditor(flight.roster)
|
||||
layout.addLayout(self.roster_editor, 2, 1)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def _changed_aircraft_count(self):
|
||||
self.game.aircraft_inventory.return_from_flight(self.flight)
|
||||
old_count = self.flight.count
|
||||
new_count = int(self.aircraft_count_spinner.value())
|
||||
self.game.aircraft_inventory.return_from_flight(self.flight)
|
||||
self.flight.resize(new_count)
|
||||
try:
|
||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
||||
except ValueError:
|
||||
@ -217,7 +239,6 @@ class QFlightSlotEditor(QGroupBox):
|
||||
f"{available} {self.flight.unit_type} remaining"
|
||||
)
|
||||
self.game.aircraft_inventory.claim_for_flight(self.flight)
|
||||
self.flight.resize(old_count)
|
||||
return
|
||||
|
||||
self.flight.resize(new_count)
|
||||
self.roster_editor.resize(new_count)
|
||||
|
||||
@ -8,7 +8,7 @@ from typing import Any, Dict, List, Union, Tuple
|
||||
|
||||
import packaging.version
|
||||
from PySide2 import QtGui
|
||||
from PySide2.QtCore import QItemSelectionModel
|
||||
from PySide2.QtCore import QItemSelectionModel, QModelIndex, Qt
|
||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||
from PySide2.QtWidgets import QAbstractItemView, QListView
|
||||
|
||||
@ -116,6 +116,7 @@ def load_campaigns() -> List[Campaign]:
|
||||
class QCampaignItem(QStandardItem):
|
||||
def __init__(self, campaign: Campaign) -> None:
|
||||
super(QCampaignItem, self).__init__()
|
||||
self.setData(campaign, QCampaignList.CampaignRole)
|
||||
self.setIcon(QtGui.QIcon(CONST.ICONS[campaign.icon_name]))
|
||||
self.setEditable(False)
|
||||
if campaign.is_compatible:
|
||||
@ -126,31 +127,33 @@ class QCampaignItem(QStandardItem):
|
||||
|
||||
|
||||
class QCampaignList(QListView):
|
||||
def __init__(self, campaigns: List[Campaign]) -> None:
|
||||
CampaignRole = Qt.UserRole
|
||||
|
||||
def __init__(self, campaigns: list[Campaign], show_incompatible: bool) -> None:
|
||||
super(QCampaignList, self).__init__()
|
||||
self.model = QStandardItemModel(self)
|
||||
self.setModel(self.model)
|
||||
self.campaign_model = QStandardItemModel(self)
|
||||
self.setModel(self.campaign_model)
|
||||
self.setMinimumWidth(250)
|
||||
self.setMinimumHeight(350)
|
||||
self.campaigns = []
|
||||
self.campaigns = campaigns
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectItems)
|
||||
self.setup_content(campaigns)
|
||||
self.setup_content(show_incompatible)
|
||||
|
||||
def setup_content(self, campaigns: List[Campaign]) -> None:
|
||||
for campaign in campaigns:
|
||||
self.campaigns.append(campaign)
|
||||
item = QCampaignItem(campaign)
|
||||
self.model.appendRow(item)
|
||||
self.setSelectedCampaign(0)
|
||||
self.repaint()
|
||||
@property
|
||||
def selected_campaign(self) -> Campaign:
|
||||
return self.currentIndex().data(QCampaignList.CampaignRole)
|
||||
|
||||
def setSelectedCampaign(self, row):
|
||||
self.selectionModel().clearSelection()
|
||||
index = self.model.index(row, 0)
|
||||
if not index.isValid():
|
||||
index = self.model.index(0, 0)
|
||||
self.selectionModel().setCurrentIndex(index, QItemSelectionModel.Select)
|
||||
self.repaint()
|
||||
def setup_content(self, show_incompatible: bool) -> None:
|
||||
self.selectionModel().blockSignals(True)
|
||||
try:
|
||||
self.campaign_model.clear()
|
||||
for campaign in self.campaigns:
|
||||
if show_incompatible or campaign.is_compatible:
|
||||
item = QCampaignItem(campaign)
|
||||
self.campaign_model.appendRow(item)
|
||||
finally:
|
||||
self.selectionModel().blockSignals(False)
|
||||
|
||||
def clear_layout(self):
|
||||
self.model.removeRows(0, self.model.rowCount())
|
||||
self.selectionModel().setCurrentIndex(
|
||||
self.campaign_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
|
||||
)
|
||||
|
||||
@ -6,7 +6,7 @@ from typing import List
|
||||
|
||||
from PySide2 import QtGui, QtWidgets
|
||||
from PySide2.QtCore import QItemSelectionModel, QPoint, Qt, QDate
|
||||
from PySide2.QtWidgets import QVBoxLayout, QTextEdit, QLabel
|
||||
from PySide2.QtWidgets import QVBoxLayout, QTextEdit, QLabel, QCheckBox
|
||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
|
||||
from game import db
|
||||
@ -319,7 +319,16 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
)
|
||||
|
||||
# List of campaigns
|
||||
campaignList = QCampaignList(campaigns)
|
||||
show_incompatible_campaigns_checkbox = QCheckBox(
|
||||
text="Show incompatible campaigns"
|
||||
)
|
||||
show_incompatible_campaigns_checkbox.setChecked(False)
|
||||
campaignList = QCampaignList(
|
||||
campaigns, show_incompatible_campaigns_checkbox.isChecked()
|
||||
)
|
||||
show_incompatible_campaigns_checkbox.toggled.connect(
|
||||
lambda checked: campaignList.setup_content(show_incompatible=checked)
|
||||
)
|
||||
self.registerField("selectedCampaign", campaignList)
|
||||
|
||||
# Faction description
|
||||
@ -380,8 +389,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
template_perf = jinja_env.get_template(
|
||||
"campaign_performance_template_EN.j2"
|
||||
)
|
||||
index = campaignList.selectionModel().currentIndex().row()
|
||||
campaign = campaignList.campaigns[index]
|
||||
campaign = campaignList.selected_campaign
|
||||
self.setField("selectedCampaign", campaign)
|
||||
self.campaignMapDescription.setText(template.render({"campaign": campaign}))
|
||||
self.faction_selection.setDefaultFactions(campaign)
|
||||
@ -396,9 +404,12 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
campaignList.selectionModel().selectionChanged.connect(on_campaign_selected)
|
||||
on_campaign_selected()
|
||||
|
||||
# Docs Link
|
||||
docsText = QtWidgets.QLabel(
|
||||
'<a href="https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Campaigns"><span style="color:#FFFFFF;">How to create your own theater</span></a>'
|
||||
"<p>Want more campaigns? You can "
|
||||
'<a href="https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance"><span style="color:#FFFFFF;">offer to help</span></a>, '
|
||||
'<a href="https://github.com/dcs-liberation/dcs_liberation/wiki/Community-campaigns"><span style="color:#FFFFFF;">play a community campaign</span></a>, '
|
||||
'or <a href="https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Campaigns"><span style="color:#FFFFFF;">create your own</span></a>.'
|
||||
"</p>"
|
||||
)
|
||||
docsText.setAlignment(Qt.AlignCenter)
|
||||
docsText.setOpenExternalLinks(True)
|
||||
@ -418,7 +429,8 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
|
||||
layout = QtWidgets.QGridLayout()
|
||||
layout.setColumnMinimumWidth(0, 20)
|
||||
layout.addWidget(campaignList, 0, 0, 5, 1)
|
||||
layout.addWidget(docsText, 5, 0, 1, 1)
|
||||
layout.addWidget(show_incompatible_campaigns_checkbox, 5, 0, 1, 1)
|
||||
layout.addWidget(docsText, 6, 0, 1, 1)
|
||||
layout.addWidget(self.campaignMapDescription, 0, 1, 1, 1)
|
||||
layout.addWidget(self.performanceText, 1, 1, 1, 1)
|
||||
layout.addWidget(mapSettingsGroup, 2, 1, 1, 1)
|
||||
|
||||
@ -473,20 +473,22 @@ class QSettingsWindow(QDialog):
|
||||
general_layout.addWidget(restrict_weapons, 0, 1, Qt.AlignRight)
|
||||
|
||||
def set_old_awec(value: bool) -> None:
|
||||
self.game.settings.disable_legacy_aewc = value
|
||||
self.game.settings.disable_legacy_aewc = not value
|
||||
|
||||
old_awac = QCheckBox()
|
||||
old_awac.setChecked(self.game.settings.disable_legacy_aewc)
|
||||
old_awac.setChecked(not self.game.settings.disable_legacy_aewc)
|
||||
old_awac.toggled.connect(set_old_awec)
|
||||
|
||||
old_awec_info = (
|
||||
"If checked, the invulnerable friendly AEW&C aircraft that begins "
|
||||
"the mission in the air will not be spawned. AEW&C missions must "
|
||||
"be planned in the ATO and will take time to arrive on-station."
|
||||
"If checked, an invulnerable friendly AEW&C aircraft that begins the "
|
||||
"mission on station will be be spawned. This behavior will be removed in a "
|
||||
"future release."
|
||||
)
|
||||
|
||||
old_awac.setToolTip(old_awec_info)
|
||||
old_awac_label = QLabel("Disable invulnerable, always-available AEW&C (WIP)")
|
||||
old_awac_label = QLabel(
|
||||
"Spawn invulnerable, always-available AEW&C aircraft (deprecated)"
|
||||
)
|
||||
old_awac_label.setToolTip(old_awec_info)
|
||||
|
||||
general_layout.addWidget(old_awac_label, 1, 0)
|
||||
|
||||
11
resources/campaigns/Battle_for_the_UAE.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Persian Gulf - Battle for the UAE",
|
||||
"theater": "Persian Gulf",
|
||||
"authors": "Mustang25",
|
||||
"recommended_player_faction": "Bluefor Modern",
|
||||
"recommended_enemy_faction": "Iran 2015",
|
||||
"description": "<p>Following the Battle of Abu Dhabi, Iran's invasion of the UAE has been halted approximately 20 miles Northeast of Liwa Airbase by coalition forces.</p><p>After weeks of stalemate, coalition forces have consolidated their position and are ready to launch their counterattack to push Iranian forces off the peninsula.</p>",
|
||||
"version": "6.0",
|
||||
"miz": "Battle_for_the_UAE_v3.0.2.miz",
|
||||
"performance": 2
|
||||
}
|
||||
BIN
resources/campaigns/Battle_for_the_UAE_v3.0.2.miz
Normal file
11
resources/campaigns/Operation_Mole_Cricket_2010.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Syria - Operation Mole Cricket 2010",
|
||||
"theater": "Syria",
|
||||
"authors": "Mustang25",
|
||||
"recommended_player_faction": "Bluefor Modern",
|
||||
"recommended_enemy_faction": "Syria 2011",
|
||||
"description": "<p>In a scenario reminescent of the First Lebanon War, hostile Syrian-backed forces have flooded into the Bekaa Valley.</p><p>The objective of this operation is twofold: drive the enemy out of the Bekaa Valley and push past the Golan Heights into Syrian territory to capture Tiyas Airbase.</p>",
|
||||
"version": "6.0",
|
||||
"miz": "Operation_Mole_Cricket_2010_v3.0.2.miz",
|
||||
"performance": 2
|
||||
}
|
||||
BIN
resources/campaigns/Operation_Mole_Cricket_2010_v3.0.2.miz
Normal file
@ -7,5 +7,5 @@
|
||||
"description": "<p>You have managed to establish a foothold at Khasab. Continue pushing south.</p>",
|
||||
"miz": "battle_of_abu_dhabi.miz",
|
||||
"performance": 2,
|
||||
"version": "5.0"
|
||||
"version": "6.0"
|
||||
}
|
||||
@ -4,5 +4,6 @@
|
||||
"authors": "Colonel Panic",
|
||||
"description": "<p>A medium sized theater with bases along the coast of the Black Sea.</p>",
|
||||
"miz": "black_sea.miz",
|
||||
"performance": 2
|
||||
"performance": 2,
|
||||
"version": "6.0"
|
||||
}
|
||||
@ -2,8 +2,10 @@
|
||||
"name": "Nevada - Exercise Vegas Nerve",
|
||||
"theater": "Nevada",
|
||||
"authors": "Starfire",
|
||||
"description": "<p>A Red Flag Exercise scenario for the NTTR comprising 4 control points.</p>",
|
||||
"version": 3,
|
||||
"recommended_player_faction": "Bluefor Modern",
|
||||
"recommended_enemy_faction": "Redfor (China) 2010",
|
||||
"description": "<p>This is an asymmetrical Red Flag Exercise scenario for the NTTR comprising 4 control points. You start off in control of the two Tonopah airports, and will push south to capture Groom Lake and Nellis AFBs. Taking down Nellis AFB's IADS and striking their resource sites ASAP once Groom Lake has been captured is recommended to offset their resource advantage.</p>",
|
||||
"version": "6.0",
|
||||
"miz": "exercise_vegas_nerve.miz",
|
||||
"performance": 0
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Syria - Battle for Golan Heights - Lite",
|
||||
"name": "Syria - Battle for Golan Heights",
|
||||
"theater": "Syria",
|
||||
"authors": "Khopa",
|
||||
"recommended_player_faction": "Israel 2000",
|
||||
@ -7,5 +7,5 @@
|
||||
"description": "<p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance friendly.</p>",
|
||||
"miz": "golan_heights_lite.miz",
|
||||
"performance": 1,
|
||||
"version": "5.0"
|
||||
"version": "6.0"
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"recommended_player_faction": "USA 2005",
|
||||
"recommended_enemy_faction": "Insurgents (Hard)",
|
||||
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
|
||||
"version": "5.0",
|
||||
"version": "6.0",
|
||||
"miz": "inherent_resolve.miz",
|
||||
"performance": 2
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
"recommended_player_faction": "Bluefor Modern",
|
||||
"recommended_enemy_faction": "Turkey 2005",
|
||||
"description": "<p>This is a semi-fictional what-if scenario for Operation Peace Spring, during which Turkish forces that crossed into Syria on an offensive against Kurdish militias were emboldened by early successes to continue pushing further southward. Attempts to broker a ceasefire have failed. Members of Operation Inherent Resolve have gathered at Ramat David Airbase in Israel to launch a counter-offensive. Campaign inversion is available if you wish to play as Turkey.</p>",
|
||||
"version": 3,
|
||||
"version": "6.0",
|
||||
"miz": "operation_peace_spring.miz",
|
||||
"performance": 1
|
||||
}
|
||||
11
resources/campaigns/operation_vectrons_claw.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Caucasus - Operation Vectron's Claw",
|
||||
"theater": "Caucasus",
|
||||
"authors": "Starfire",
|
||||
"recommended_player_faction": "USA 2005",
|
||||
"recommended_enemy_faction": "Russia 1990",
|
||||
"description": "<p>United Nations Observer Mission in Georgia (UNOMIG) observers stationed in Georgia to monitor the ceasefire between Georgia and Abkhazia have been cut off from friendly forces by Russian troops backing the separatist state. The UNOMIG HQ at Sukhumi has been taken, and a small contingent of observers and troops at the Zugdidi Sector HQ will have to make a run for the coast, supported by offshore US naval aircraft. The contingent is aware that their best shot at survival is to swiftly retake Sukhumi before Russian forces have a chance to dig in, so that friendly ground forces can land and reinforce them.<br/></p><p><strong>Note:</strong> Ground unit purchase will not be available past Turn 0 until Sukhumi is retaken, so it is imperative you reach Sukhumi with at least one surviving ground unit to capture it. The player can either play the first leg of the scenario as an evacuation with a couple of light vehicles (e.g. Humvees) set on breakthrough (modifying waypoints in the mission editor so they are not charging head-on into enemy ground forces is suggested), or purchase heavier ground units if they wish to experience a more traditional ground war.</p>",
|
||||
"version": "6.0",
|
||||
"miz": "operation_vectrons_claw.miz",
|
||||
"performance": 1
|
||||
}
|
||||
BIN
resources/campaigns/operation_vectrons_claw.miz
Normal file
@ -7,5 +7,5 @@
|
||||
"description": "<p>A small theater in Russia, progress from Mozdok to Maykop.</p><p>This scenario is pretty simple, it is ideal if you want to run a short campaign. If your PC is not powerful, this is also the less performance heavy scenario.</p>",
|
||||
"miz": "russia_small.miz",
|
||||
"performance": 0,
|
||||
"version": 3
|
||||
"version": "6.0"
|
||||
}
|
||||
|
||||
11
resources/campaigns/syria_full_map.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Syria - Full Map",
|
||||
"theater": "Syria",
|
||||
"authors": "Plob",
|
||||
"recommended_player_faction": "Bluefor Modern",
|
||||
"recommended_enemy_faction": "Syria 2011",
|
||||
"description": "<p>Syria Full map, designed for groups of 4-12 players.</p>",
|
||||
"miz": "syria_full_map.miz",
|
||||
"performance": 3,
|
||||
"version": "6.0"
|
||||
}
|
||||
BIN
resources/campaigns/syria_full_map.miz
Normal file
@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "Syria - Full Map",
|
||||
"theater": "Syria",
|
||||
"authors": "Hawkmoon",
|
||||
"description": "<p>Full map of Syria</p><p><strong>Note : </strong></p><p>For a better early game experience, it is suggested to give the AI an high amount of starting money.</p>",
|
||||
"miz": "syria_full_map_remastered.miz",
|
||||
"performance": 3
|
||||
}
|
||||
61
resources/factions/jordan_2010.json
Normal file
@ -0,0 +1,61 @@
|
||||
{
|
||||
"country": "Jordan",
|
||||
"name": "Jordan 2010",
|
||||
"authors": "Starfire",
|
||||
"description": "<p>Royal Jordanian Armed Forces early 21st century</p>",
|
||||
"aircrafts": [
|
||||
"F_5E_3",
|
||||
"C_101CC",
|
||||
"SA342M",
|
||||
"SA342L"
|
||||
],
|
||||
"frontline_units": [
|
||||
"MBT_Challenger_II",
|
||||
"MBT_M60A3_Patton",
|
||||
"IFV_Marder",
|
||||
"IFV_BMP_2",
|
||||
"APC_M113",
|
||||
"APC_M1043_HMMWV_Armament",
|
||||
"ATGM_M1045_HMMWV_TOW"
|
||||
],
|
||||
"artillery_units": [
|
||||
"MLRS_M270",
|
||||
"SPH_M109_Paladin"
|
||||
],
|
||||
"logistics_units": [
|
||||
"Transport_M818"
|
||||
],
|
||||
"infantry_units": [
|
||||
"Infantry_M4",
|
||||
"Soldier_M249",
|
||||
"SAM_SA_18_Igla_S_MANPADS"
|
||||
],
|
||||
"air_defenses": [
|
||||
"SA8Generator",
|
||||
"SA13Generator",
|
||||
"VulcanGenerator",
|
||||
"ZU23Generator",
|
||||
"HawkGenerator"
|
||||
],
|
||||
"ewrs": [
|
||||
"HawkEwrGenerator"
|
||||
],
|
||||
"aircraft_carrier": [
|
||||
],
|
||||
"helicopter_carrier": [
|
||||
],
|
||||
"destroyers": [
|
||||
],
|
||||
"cruisers": [
|
||||
],
|
||||
"requirements": {
|
||||
},
|
||||
"carrier_names": [
|
||||
],
|
||||
"helicopter_carrier_names": [
|
||||
],
|
||||
"navy_generators": [
|
||||
],
|
||||
"has_jtac": false,
|
||||
"doctrine": "coldwar"
|
||||
}
|
||||
@ -498,7 +498,6 @@ QHeaderView::section {
|
||||
background: #4B5B74;
|
||||
padding: 4px;
|
||||
border-style: none;
|
||||
border-bottom: 1px solid #1D2731;
|
||||
}
|
||||
|
||||
QHeaderView::section:horizontal
|
||||
@ -515,11 +514,6 @@ QHeaderView::section:vertical
|
||||
background: #4B5B74;
|
||||
}
|
||||
|
||||
QTableWidget {
|
||||
gridline-color: red;
|
||||
background: #4B5B74;
|
||||
}
|
||||
|
||||
QTableView QTableCornerButton::section {
|
||||
background: #4B5B74;
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="158" height="118" viewBox="21 36 158 118"><path d="M25,50 l150,0 0,100 -150,0 z" stroke-width="4" stroke="black" fill="rgb(128,224,255)" fill-opacity="1" ></path><path d="m 75,85 50,30 m -50,0 50,-30" stroke-width="4" stroke="black" fill="none" ></path><path d="M85,48 85,40 115,40 115,48 100,46 Z" stroke-width="4" stroke="black" fill="black" ></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="158" height="118" viewBox="21 36 158 118"><path d="M25,50 l150,0 0,100 -150,0 z" stroke-width="4" stroke="black" fill="rgb(0,107,140)" fill-opacity="1" ></path><path d="m 75,85 50,30 m -50,0 50,-30" stroke-width="4" stroke="black" fill="none" ></path><path d="M85,48 85,40 115,40 115,48 100,46 Z" stroke-width="4" stroke="black" fill="black" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 438 B After Width: | Height: | Size: 436 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="158" height="148" viewBox="21 36 158 148"><path d="M25,50 l150,0 0,100 -150,0 z" stroke-width="4" stroke="black" fill="rgb(128,224,255)" fill-opacity="1" ></path><path d="m 75,85 50,30 m -50,0 50,-30" stroke-width="4" stroke="black" fill="none" ></path><path d="M85,48 85,40 115,40 115,48 100,46 Z" stroke-width="4" stroke="black" fill="black" ></path><path d="M25,155 l150,0 0,25 -150,0 z" stroke-width="4" stroke="black" fill="rgb(255,255,0)" ></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="158" height="148" viewBox="21 36 158 148"><path d="M25,50 l150,0 0,100 -150,0 z" stroke-width="4" stroke="black" fill="rgb(0,107,140)" fill-opacity="1" ></path><path d="m 75,85 50,30 m -50,0 50,-30" stroke-width="4" stroke="black" fill="none" ></path><path d="M85,48 85,40 115,40 115,48 100,46 Z" stroke-width="4" stroke="black" fill="black" ></path><path d="M25,155 l150,0 0,25 -150,0 z" stroke-width="4" stroke="black" fill="rgb(255,255,0)" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 537 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="158" height="148" viewBox="21 36 158 148"><path d="M25,50 l150,0 0,100 -150,0 z" stroke-width="4" stroke="black" fill="rgb(128,224,255)" fill-opacity="1" ></path><path d="m 75,85 50,30 m -50,0 50,-30" stroke-width="4" stroke="black" fill="none" ></path><path d="M85,48 85,40 115,40 115,48 100,46 Z" stroke-width="4" stroke="black" fill="black" ></path><path d="M25,155 l150,0 0,25 -150,0 z" stroke-width="4" stroke="black" fill="rgb(255,0,0)" ></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="158" height="148" viewBox="21 36 158 148"><path d="M25,50 l150,0 0,100 -150,0 z" stroke-width="4" stroke="black" fill="rgb(0,107,140)" fill-opacity="1" ></path><path d="m 75,85 50,30 m -50,0 50,-30" stroke-width="4" stroke="black" fill="none" ></path><path d="M85,48 85,40 115,40 115,48 100,46 Z" stroke-width="4" stroke="black" fill="black" ></path><path d="M25,155 l150,0 0,25 -150,0 z" stroke-width="4" stroke="black" fill="rgb(255,0,0)" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 537 B After Width: | Height: | Size: 535 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="152" height="162" viewBox="24 14 152 162"><path d="M 100,28 L172,100 100,172 28,100 100,28 Z" stroke-width="4" stroke="black" fill="rgb(255,128,128)" fill-opacity="1" ></path><path d="m 75,85 50,30 m -50,0 50,-30" stroke-width="4" stroke="black" fill="none" ></path><path d="M85,40 85,18 115,18 115,40 100,24 Z" stroke-width="4" stroke="black" fill="black" ></path></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" baseProfile="tiny" width="152" height="162" viewBox="24 14 152 162"><path d="M 100,28 L172,100 100,172 28,100 100,28 Z" stroke-width="4" stroke="black" fill="rgb(200,0,0)" fill-opacity="1" ></path><path d="m 75,85 50,30 m -50,0 50,-30" stroke-width="4" stroke="black" fill="none" ></path><path d="M85,40 85,18 115,18 115,40 100,24 Z" stroke-width="4" stroke="black" fill="black" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 451 B After Width: | Height: | Size: 447 B |