Merge branch 'develop' into helipads

# Conflicts:
#	game/theater/conflicttheater.py
#	gen/flights/flightplan.py
This commit is contained in:
Khopa 2021-06-06 15:46:30 +02:00
commit 73b1be36a2
103 changed files with 1992 additions and 3914 deletions

View 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):

View File

@ -1,3 +1,11 @@
# 4.0.0
Saves from 3.x are not compatible with 4.0.
## Features/Improvements
## Fixes
# 3.0.0 # 3.0.0
Saves from 2.5 are not compatible with 3.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]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn.
* **[Campaign]** Non-control point FOBs will no longer spawn. * **[Campaign]** Non-control point FOBs will no longer spawn.
* **[Campaign]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information. * **[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 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 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]** 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]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
* **[Flight Planner]** Flight plans now include bullseye waypoints. * **[Flight Planner]** Flight plans now include bullseye waypoints.
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route. * **[Flight Planner]** 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. * **[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. * **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets.
* **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows. * **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows.
* **[UI]** Added new web based map UI. This is mostly functional but many of the old display options are a WIP. Revert to the old map with --old-map. * **[UI]** 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]** 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]** 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 factories to spawn.
* **[Modding]** Campaigns now choose locations for ammunition depots to spawn.
* **[Modding]** Campaigns now use map structures as strike targets. * **[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]** Campaigns may now place AAA objectives.
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory. * **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
* **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default. * **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default.
@ -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]** Auto planner will no longer attempt to plan missions for which the faction has no compatible aircraft.
* **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used. * **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used.
* **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives. * **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives.
* **[Campaign]** EWR sites are now purchasable.
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups. * **[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]** 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 # 2.5.1

0
game/data/__init__.py Normal file
View File

View File

@ -11,7 +11,7 @@ DEFAULT_AVAILABLE_BUILDINGS = [
"derrick", "derrick",
] ]
WW2_FREE = ["fuel", "ware", "fob"] WW2_FREE = ["fuel", "ware"]
WW2_GERMANY_BUILDINGS = [ WW2_GERMANY_BUILDINGS = [
"fuel", "fuel",
"ww2bunker", "ww2bunker",

View File

@ -1,7 +1,20 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from dcs.task import Reconnaissance
from game.utils import Distance, feet, nautical_miles 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) @dataclass(frozen=True)
@ -50,6 +63,8 @@ class Doctrine:
sweep_distance: Distance sweep_distance: Distance
ground_unit_procurement_ratios: GroundUnitProcurementRatios
MODERN_DOCTRINE = Doctrine( MODERN_DOCTRINE = Doctrine(
cap=True, cap=True,
@ -76,6 +91,17 @@ MODERN_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(50), cap_engagement_range=nautical_miles(50),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(60), 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( COLDWAR_DOCTRINE = Doctrine(
@ -103,6 +129,17 @@ COLDWAR_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(35), cap_engagement_range=nautical_miles(35),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(40), 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( WWII_DOCTRINE = Doctrine(
@ -130,4 +167,14 @@ WWII_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(20), cap_engagement_range=nautical_miles(20),
cas_duration=timedelta(minutes=30), cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(10), 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,
}
),
) )

View 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

View File

@ -1251,6 +1251,7 @@ REWARDS = {
"ammo": 2, "ammo": 2,
"farp": 1, "farp": 1,
# TODO: Should generate no cash once they generate units. # TODO: Should generate no cash once they generate units.
# https://github.com/dcs-liberation/dcs_liberation/issues/1036
"factory": 10, "factory": 10,
"comms": 10, "comms": 10,
"oil": 10, "oil": 10,

View File

@ -24,7 +24,7 @@ from game import db
from game.theater import Airfield, ControlPoint from game.theater import Airfield, ControlPoint
from game.transfers import CargoShip from game.transfers import CargoShip
from game.unitmap import ( from game.unitmap import (
AirliftUnit, AirliftUnits,
Building, Building,
ConvoyUnit, ConvoyUnit,
FrontLineUnit, FrontLineUnit,
@ -75,8 +75,8 @@ class GroundLosses:
player_cargo_ships: List[CargoShip] = field(default_factory=list) player_cargo_ships: List[CargoShip] = field(default_factory=list)
enemy_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) player_airlifts: List[AirliftUnits] = field(default_factory=list)
enemy_airlifts: List[AirliftUnit] = field(default_factory=list) enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list) player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
enemy_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 yield from self.ground_losses.enemy_cargo_ships
@property @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.player_airlifts
yield from self.ground_losses.enemy_airlifts yield from self.ground_losses.enemy_airlifts
@ -220,7 +220,8 @@ class Debriefing:
else: else:
losses = self.ground_losses.enemy_airlifts losses = self.ground_losses.enemy_airlifts
for loss in losses: 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 return losses_by_type
def building_losses_by_type(self, player: bool) -> Dict[str, int]: def building_losses_by_type(self, player: bool) -> Dict[str, int]:

View File

@ -144,7 +144,7 @@ class Event:
def _commit_pilot_experience(ato: AirTaskingOrder) -> None: def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
for idx, pilot in enumerate(flight.pilots): for idx, pilot in enumerate(flight.roster.pilots):
if pilot is None: if pilot is None:
logging.error( logging.error(
f"Cannot award experience to pilot #{idx} of {flight} " f"Cannot award experience to pilot #{idx} of {flight} "
@ -202,19 +202,17 @@ class Event:
@staticmethod @staticmethod
def commit_airlift_losses(debriefing: Debriefing) -> None: def commit_airlift_losses(debriefing: Debriefing) -> None:
for loss in debriefing.airlift_losses: for loss in debriefing.airlift_losses:
unit_type = loss.unit_type
transfer = loss.transfer transfer = loss.transfer
available = loss.transfer.units.get(unit_type, 0)
airlift_name = f"airlift from {transfer.origin} to {transfer.destination}" airlift_name = f"airlift from {transfer.origin} to {transfer.destination}"
if available <= 0: for unit_type in loss.cargo:
logging.error( try:
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) 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 @staticmethod
def commit_ground_object_losses(debriefing: Debriefing) -> None: def commit_ground_object_losses(debriefing: Debriefing) -> None:

View File

@ -1,4 +1,5 @@
from __future__ import annotations from __future__ import annotations
from game.data.groundunitclass import GroundUnitClass
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
@ -133,6 +134,16 @@ class Faction:
#: both will use it. #: both will use it.
unrestricted_satnav: bool = False 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 @classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
faction = Faction(locales=json.get("locales")) faction = Faction(locales=json.get("locales"))

View File

@ -113,8 +113,6 @@ class Game:
self.informations.append(Information("Game Start", "-" * 40, 0)) 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. # Culling Zones are for areas around points of interest that contain things we may not wish to cull.
self.__culling_zones: List[Point] = [] 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.__destroyed_units: List[str] = []
self.savepath = "" self.savepath = ""
self.budget = player_budget self.budget = player_budget
@ -124,8 +122,8 @@ class Game:
self.conditions = self.generate_conditions() self.conditions = self.generate_conditions()
self.blue_transit_network = self.compute_transit_network_for(player=True) self.blue_transit_network = TransitNetwork()
self.red_transit_network = self.compute_transit_network_for(player=False) self.red_transit_network = TransitNetwork()
self.blue_procurement_requests: List[AircraftProcurementRequest] = [] self.blue_procurement_requests: List[AircraftProcurementRequest] = []
self.red_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.blue_air_wing = AirWing(self, player=True)
self.red_air_wing = AirWing(self, player=False) self.red_air_wing = AirWing(self, player=False)
self.on_load() self.on_load(game_still_initializing=True)
def __getstate__(self) -> Dict[str, Any]: def __getstate__(self) -> Dict[str, Any]:
state = self.__dict__.copy() state = self.__dict__.copy()
@ -301,10 +299,11 @@ class Game:
else: else:
raise RuntimeError(f"{event} was passed when an Event type was expected") 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) LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater) ObjectiveDistanceCache.set_theater(self.theater)
self.compute_conflicts_position() self.compute_conflicts_position()
if not game_still_initializing:
self.compute_threat_zones() self.compute_threat_zones()
self.blue_faker = Faker(self.faction_for(player=True).locales) self.blue_faker = Faker(self.faction_for(player=True).locales)
self.red_faker = Faker(self.faction_for(player=False).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 # gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default # repairs) is split evenly between air and ground. For the default
# starting budget of 2000 this gives 600 to ground forces and 1400 to # starting budget of 2000 this gives 600 to ground forces and 1400 to
# aircraft. # aircraft. After that the budget will be spend proportionally based on how much is already invested
ground_portion = 0.3 if self.turn == 0 else 0.5
self.budget = ProcurementAi( self.budget = ProcurementAi(
self, self,
for_player=True, for_player=True,
@ -448,7 +447,6 @@ class Game:
manage_runways=self.settings.automate_runway_repair, manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements, manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements, manage_aircraft=self.settings.automate_aircraft_reinforcements,
front_line_budget_share=ground_portion,
).spend_budget(self.budget) ).spend_budget(self.budget)
self.enemy_budget = ProcurementAi( self.enemy_budget = ProcurementAi(
@ -458,7 +456,6 @@ class Game:
manage_runways=True, manage_runways=True,
manage_front_line=True, manage_front_line=True,
manage_aircraft=True, manage_aircraft=True,
front_line_budget_share=ground_portion,
).spend_budget(self.enemy_budget) ).spend_budget(self.enemy_budget)
def message(self, text: str) -> None: def message(self, text: str) -> None:
@ -519,7 +516,6 @@ class Game:
:return: List of points of interests :return: List of points of interests
""" """
zones = [] zones = []
points = []
# By default, use the existing frontline conflict position # By default, use the existing frontline conflict position
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts():
@ -529,11 +525,6 @@ class Game:
zones.append(front_line.red_cp.position) zones.append(front_line.red_cp.position)
for cp in self.theater.controlpoints: 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 do_not_cull_carrier is enabled, add carriers as culling point
if self.settings.perf_do_not_cull_carrier: if self.settings.perf_do_not_cull_carrier:
if cp.is_carrier or cp.is_lha: if cp.is_carrier or cp.is_lha:
@ -577,7 +568,6 @@ class Game:
zones.append(Point(0, 0)) zones.append(Point(0, 0))
self.__culling_zones = zones self.__culling_zones = zones
self.__culling_points = points
def add_destroyed_units(self, data): def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"]) pos = Point(data["x"], data["z"])
@ -593,17 +583,10 @@ class Game:
:param pos: Position you are tryng to spawn stuff at :param pos: Position you are tryng to spawn stuff at
:return: True if units can not be added at given position :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 return False
else:
for z in self.__culling_zones: for z in self.__culling_zones:
if ( if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
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 False
return True return True
@ -614,13 +597,6 @@ class Game:
""" """
return self.__culling_zones 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 # 1 = red, 2 = blue
def get_player_coalition_id(self): def get_player_coalition_id(self):
return 2 return 2

View File

@ -8,17 +8,19 @@ from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple, Type
from dcs.unittype import FlyingType, VehicleType from dcs.unittype import FlyingType, VehicleType
from game import db from game import db
from game.data.groundunitclass import GroundUnitClass
from game.factions.faction import Faction from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint, MissionTarget
from game.utils import Distance from game.utils import Distance
from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
FRONTLINE_RESERVES_FACTOR = 1.3
@dataclass(frozen=True) @dataclass(frozen=True)
class AircraftProcurementRequest: class AircraftProcurementRequest:
@ -43,10 +45,7 @@ class ProcurementAi:
manage_runways: bool, manage_runways: bool,
manage_front_line: bool, manage_front_line: bool,
manage_aircraft: bool, manage_aircraft: bool,
front_line_budget_share: float,
) -> None: ) -> None:
if front_line_budget_share > 1.0:
raise ValueError
self.game = game self.game = game
self.is_player = for_player self.is_player = for_player
@ -55,14 +54,34 @@ class ProcurementAi:
self.manage_runways = manage_runways self.manage_runways = manage_runways
self.manage_front_line = manage_front_line self.manage_front_line = manage_front_line
self.manage_aircraft = manage_aircraft 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) 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: def spend_budget(self, budget: float) -> float:
if self.manage_runways: if self.manage_runways:
budget = self.repair_runways(budget) budget = self.repair_runways(budget)
if self.manage_front_line: if self.manage_front_line:
armor_budget = math.ceil(budget * self.front_line_budget_share) armor_budget = budget * self.calculate_ground_unit_budget_share()
budget -= armor_budget budget -= armor_budget
budget += self.reinforce_front_line(armor_budget) budget += self.reinforce_front_line(armor_budget)
@ -114,28 +133,14 @@ class ProcurementAi:
) )
return budget return budget
def random_affordable_ground_unit( def affordable_ground_unit_of_class(
self, budget: float, cp: ControlPoint self, budget: float, unit_class: GroundUnitClass
) -> Optional[Type[VehicleType]]: ) -> Optional[Type[VehicleType]]:
affordable_units = [ faction_units = set(self.faction.frontline_units) | set(
u self.faction.artillery_units
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
) )
total_non_aa = ( of_class = set(unit_class.unit_list) & faction_units
cp.base.total_armor + cp.pending_deliveries_count - total_number_aa affordable_units = [u for u in of_class if db.PRICES[u] <= budget]
)
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)
if not affordable_units: if not affordable_units:
return None return None
return random.choice(affordable_units) return random.choice(affordable_units)
@ -147,12 +152,12 @@ class ProcurementAi:
# TODO: Attempt to transfer from reserves. # TODO: Attempt to transfer from reserves.
while budget > 0: while budget > 0:
candidates = self.front_line_candidates() cp = self.ground_reinforcement_candidate()
if not candidates: if cp is None:
break break
cp = random.choice(candidates) most_needed_type = self.most_needed_unit_class(cp)
unit = self.random_affordable_ground_unit(budget, cp) unit = self.affordable_ground_unit_of_class(budget, most_needed_type)
if unit is None: if unit is None:
# Can't afford any more units. # Can't afford any more units.
break break
@ -162,6 +167,31 @@ class ProcurementAi:
return budget 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( def _affordable_aircraft_for_task(
self, self,
task: FlightType, task: FlightType,
@ -179,7 +209,7 @@ class ProcurementAi:
continue continue
for squadron in self.air_wing.squadrons_for(unit): for squadron in self.air_wing.squadrons_for(unit):
if task in squadron.mission_types: if task in squadron.auto_assignable_mission_types:
break break
else: else:
continue continue
@ -244,11 +274,9 @@ class ProcurementAi:
) -> Iterator[ControlPoint]: ) -> Iterator[ControlPoint]:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near) distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
threatened = [] 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): if not cp.is_friendly(self.is_player):
continue continue
if not cp.runway_is_operational():
continue
if cp.unclaimed_parking(self.game) < request.number: if cp.unclaimed_parking(self.game) < request.number:
continue continue
if self.threat_zones.threatened(cp.position): if self.threat_zones.threatened(cp.position):
@ -256,33 +284,33 @@ class ProcurementAi:
yield cp yield cp
yield from threatened yield from threatened
def front_line_candidates(self) -> List[ControlPoint]: def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
candidates = [] worst_supply = math.inf
understaffed: Optional[ControlPoint] = None
# Prefer to buy front line units at active front lines that are not # Prefer to buy front line units at active front lines that are not
# already overloaded. # already overloaded.
for cp in self.owned_points: for cp in self.owned_points:
if not cp.has_active_frontline:
total_ground_units_allocated_to_this_control_point = ( continue
self.total_ground_units_allocated_to(cp)
)
if not cp.has_ground_unit_source(self.game): if not cp.has_ground_unit_source(self.game):
# No source of ground units, so can't buy anything.
continue continue
if ( purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
total_ground_units_allocated_to_this_control_point >= 50 allocated = cp.allocated_ground_units(self.game.transfers)
or total_ground_units_allocated_to_this_control_point if allocated.total >= purchase_target:
>= cp.frontline_unit_count_limit
):
# Control point is already sufficiently defended. # Control point is already sufficiently defended.
continue continue
for connected in cp.connected_points: if allocated.total < worst_supply:
if not connected.is_friendly(to_player=self.is_player): worst_supply = allocated.total
candidates.append(cp) understaffed = cp
if not candidates: if understaffed is not None:
# Otherwise buy reserves, but don't exceed 10 reserve units per CP. return understaffed
# 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 # These units do not exist in the world until the CP becomes
# connected to an active front line, at which point all these units # connected to an active front line, at which point all these units
# will suddenly appear at the gates of the newly captured CP. # will suddenly appear at the gates of the newly captured CP.
@ -293,19 +321,32 @@ class ProcurementAi:
# Also, do not bother buying units at bases that will never connect # Also, do not bother buying units at bases that will never connect
# to a front line. # to a front line.
for cp in self.owned_points: 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: if cp.is_global:
continue continue
candidates.append(cp) if not cp.can_recruit_ground_units(self.game):
continue
return candidates allocated = cp.allocated_ground_units(self.game.transfers)
if allocated.total >= self.game.settings.reserves_procurement_target:
continue
def total_ground_units_allocated_to(self, control_point: ControlPoint) -> int: if allocated.total < worst_supply:
total = control_point.expected_ground_units_next_turn.total worst_supply = allocated.total
for transfer in self.game.transfers: understaffed = cp
if transfer.destination == control_point:
total += sum(transfer.units.values()) return understaffed
return total
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

View File

@ -43,11 +43,11 @@ class Settings:
automate_front_line_reinforcements: bool = False automate_front_line_reinforcements: bool = False
automate_aircraft_reinforcements: bool = False automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = False disable_legacy_aewc: bool = True
generate_dark_kneeboard: bool = False generate_dark_kneeboard: bool = False
invulnerable_player_pilots: bool = True invulnerable_player_pilots: bool = True
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
auto_ato_player_missions_asap: bool = False auto_ato_player_missions_asap: bool = True
# Performance oriented # Performance oriented
perf_red_alert_state: bool = True perf_red_alert_state: bool = True
@ -57,6 +57,7 @@ class Settings:
perf_moving_units: bool = True perf_moving_units: bool = True
perf_infantry: bool = True perf_infantry: bool = True
perf_destroyed_units: bool = True perf_destroyed_units: bool = True
reserves_procurement_target: int = 10
# Performance culling # Performance culling
perf_culling: bool = False perf_culling: bool = False

View File

@ -10,10 +10,8 @@ from pathlib import Path
from typing import ( from typing import (
Type, Type,
Tuple, Tuple,
List,
TYPE_CHECKING, TYPE_CHECKING,
Optional, Optional,
Iterable,
Iterator, Iterator,
Sequence, Sequence,
) )
@ -83,9 +81,12 @@ class Squadron:
role: str role: str
aircraft: Type[FlyingType] aircraft: Type[FlyingType]
livery: Optional[str] livery: Optional[str]
mission_types: Tuple[FlightType, ...] mission_types: tuple[FlightType, ...]
pilots: List[Pilot] pilots: list[Pilot]
available_pilots: List[Pilot] = field(init=False, hash=False, compare=False) 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 # 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 # 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: def __post_init__(self) -> None:
self.available_pilots = list(self.active_pilots) self.available_pilots = list(self.active_pilots)
self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str: def __str__(self) -> str:
return f'{self.name} "{self.nickname}"' return f'{self.name} "{self.nickname}"'
@ -142,8 +144,12 @@ class Squadron:
def return_pilot(self, pilot: Pilot) -> None: def return_pilot(self, pilot: Pilot) -> None:
self.available_pilots.append(pilot) self.available_pilots.append(pilot)
def return_pilots(self, pilots: Iterable[Pilot]) -> None: def return_pilots(self, pilots: Sequence[Pilot]) -> None:
self.available_pilots.extend(pilots) # 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: def enlist_new_pilots(self, count: int) -> None:
new_pilots = [Pilot(self.faker.name()) for _ in range(count)] 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]: def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.pilots if p.status == status] 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 @property
def active_pilots(self) -> list[Pilot]: def active_pilots(self) -> list[Pilot]:
return self._pilots_with_status(PilotStatus.Active) return self._pilots_with_status(PilotStatus.Active)
@ -169,8 +178,12 @@ class Squadron:
return self._pilots_with_status(PilotStatus.OnLeave) return self._pilots_with_status(PilotStatus.OnLeave)
@property @property
def size(self) -> int: def number_of_pilots_including_dead(self) -> int:
return len(self.active_pilots) + len(self.pilots_on_leave) 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: def pilot_at_index(self, index: int) -> Pilot:
return self.pilots[index] return self.pilots[index]
@ -213,6 +226,12 @@ class Squadron:
player=player, 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: class SquadronLoader:
def __init__(self, game: Game, player: bool) -> None: def __init__(self, game: Game, player: bool) -> None:

View File

@ -10,7 +10,6 @@ from dcs.vehicles import AirDefence, Armor
from game import db from game import db
from game.db import PRICES from game.db import PRICES
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
STRENGTH_AA_ASSEMBLE_MIN = 0.2 STRENGTH_AA_ASSEMBLE_MIN = 0.2
PLANES_SCRAMBLE_MIN_BASE = 2 PLANES_SCRAMBLE_MIN_BASE = 2
@ -25,6 +24,7 @@ class Base:
def __init__(self): def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {} self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {} self.armor: Dict[Type[VehicleType], int] = {}
# TODO: Appears unused.
self.aa: Dict[AirDefence, int] = {} self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {} self.commision_points: Dict[Type, float] = {}
self.strength = 1 self.strength = 1
@ -47,10 +47,6 @@ class Base:
logging.exception(f"No price found for {unit_type.id}") logging.exception(f"No price found for {unit_type.id}")
return total 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 @property
def total_aa(self) -> int: def total_aa(self) -> int:
return sum(self.aa.values()) return sum(self.aa.values())

View File

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
import logging
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
@ -40,10 +39,6 @@ from dcs.unitgroup import (
VehicleGroup, VehicleGroup,
) )
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed 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 pyproj import CRS, Transformer
from shapely import geometry, ops from shapely import geometry, ops
@ -58,10 +53,12 @@ from .controlpoint import (
) )
from .frontline import FrontLine from .frontline import FrontLine
from .landmap import Landmap, load_landmap, poly_contains from .landmap import Landmap, load_landmap, poly_contains
from .latlon import LatLon
from .projections import TransverseMercator from .projections import TransverseMercator
from ..point_with_heading import PointWithHeading from ..point_with_heading import PointWithHeading
from ..profiling import logged_duration 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_TINY = 150
SIZE_SMALL = 600 SIZE_SMALL = 600
@ -88,42 +85,39 @@ class MizCampaignLoader:
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id
FARP_HELIPAD = "SINGLE_HELIPAD" 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 OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id
# Multiple options for the required SAMs so campaign designers can more # Multiple options for air defenses so campaign designers can more accurately see
# accurately see the coverage of their IADS for the expected type. # the coverage of their IADS for the expected type.
REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = { LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Patriot_LN.id, AirDefence.SAM_Patriot_LN.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id, AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.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_Hawk_LN_M192.id,
AirDefence.SAM_SA_2_S_75_Guideline_LN.id, AirDefence.SAM_SA_2_S_75_Guideline_LN.id,
AirDefence.SAM_SA_3_S_125_Goa_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_Avenger__Stinger.id,
AirDefence.SAM_Rapier_LN.id, AirDefence.SAM_Rapier_LN.id,
AirDefence.SAM_SA_19_Tunguska_Grison.id, AirDefence.SAM_SA_19_Tunguska_Grison.id,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.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.AAA_8_8cm_Flak_18.id,
AirDefence.SPAAA_Vulcan_M163.id, AirDefence.SPAAA_Vulcan_M163.id,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.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 ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
@ -131,9 +125,7 @@ class MizCampaignLoader:
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
REQUIRED_STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
BASE_DEFENSE_RADIUS = nautical_miles(2)
def __init__(self, miz: Path, theater: ConflictTheater) -> None: def __init__(self, miz: Path, theater: ConflictTheater) -> None:
self.theater = theater self.theater = theater
@ -211,98 +203,56 @@ class MizCampaignLoader:
@property @property
def ships(self) -> Iterator[ShipGroup]: 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: for group in self.red.ship_group:
if group.units[0].type == self.SHIP_UNIT_TYPE: if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group 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 @property
def offshore_strike_targets(self) -> Iterator[StaticGroup]: 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: for group in self.red.static_group:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE: if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group yield group
@property @property
def missile_sites(self) -> Iterator[VehicleGroup]: 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: for group in self.red.vehicle_group:
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE: if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
yield group yield group
@property @property
def coastal_defenses(self) -> Iterator[VehicleGroup]: 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: for group in self.red.vehicle_group:
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE: if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
yield group yield group
@property @property
def required_long_range_sams(self) -> Iterator[VehicleGroup]: def long_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: 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 yield group
@property @property
def required_medium_range_sams(self) -> Iterator[VehicleGroup]: def medium_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: 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 yield group
@property @property
def required_short_range_sams(self) -> Iterator[VehicleGroup]: def short_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: 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 yield group
@property @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): 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 yield group
@property @property
def required_ewrs(self) -> Iterator[VehicleGroup]: def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: 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 yield group
@property @property
@ -330,9 +280,9 @@ class MizCampaignLoader:
yield group yield group
@property @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): 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 yield group
@property @property
@ -441,112 +391,57 @@ class MizCampaignLoader:
return closest, distance return closest, distance
def add_preset_locations(self) -> None: 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: for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append( closest.preset_locations.offshore_strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading) 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: for group in self.ships:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ships.append( closest.preset_locations.ships.append(
PointWithHeading.from_point(group.position, group.units[0].heading) 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: for group in self.missile_sites:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append( closest.preset_locations.missile_sites.append(
PointWithHeading.from_point(group.position, group.units[0].heading) 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: for group in self.coastal_defenses:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append( closest.preset_locations.coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading) 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, 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) 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, 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) 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, 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) 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, 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) 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, distance = self.objective_info(group)
closest.preset_locations.required_aaa.append( closest.preset_locations.ewrs.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(
PointWithHeading.from_point(group.position, group.units[0].heading) PointWithHeading.from_point(group.position, group.units[0].heading)
) )
@ -574,9 +469,9 @@ class MizCampaignLoader:
PointWithHeading.from_point(group.position, group.units[0].heading) 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, 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) PointWithHeading.from_point(group.position, group.units[0].heading)
) )

View File

@ -3,11 +3,11 @@ from __future__ import annotations
import heapq import heapq
import itertools import itertools
import logging import logging
import random
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum, unique, auto, IntEnum
from functools import total_ordering from functools import total_ordering, cached_property
from typing import ( from typing import (
Any, Any,
Dict, Dict,
@ -33,24 +33,19 @@ from dcs.ships import (
) )
from dcs.terrain.terrain import Airport, ParkingSlot from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unittype import FlyingType from dcs.unittype import FlyingType, VehicleType
from game import db from game import db
from game.point_with_heading import PointWithHeading from game.point_with_heading import PointWithHeading
from game.scenery_group import SceneryGroup from game.scenery_group import SceneryGroup
from gen.flights.closestairfields import ObjectiveDistanceCache 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.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData from gen.runways import RunwayAssigner, RunwayData
from .base import Base from .base import Base
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import ( from .theatergroundobject import (
BaseDefenseGroundObject,
EwrGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
VehicleGroupGroundObject,
) )
from ..db import PRICES from ..db import PRICES
from ..helipad import Helipad from ..helipad import Helipad
@ -60,6 +55,7 @@ from ..weather import Conditions
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from ..transfers import PendingTransfers
FREE_FRONTLINE_UNIT_SUPPLY: int = 15 FREE_FRONTLINE_UNIT_SUPPLY: int = 15
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12 AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
@ -79,149 +75,133 @@ class ControlPointType(Enum):
OFF_MAP = 6 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 @dataclass
class PresetLocations: class PresetLocations:
"""Defines the preset locations loaded from the campaign mission file.""" """Defines the preset locations loaded from the campaign mission file."""
#: Locations used for spawning ground defenses for bases. #: Locations used by non-carrier ships that will be spawned unless the faction has
base_garrisons: List[PointWithHeading] = field(default_factory=list) #: no navy or the player has disabled ship generation for the owning side.
#: 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.
ships: List[PointWithHeading] = field(default_factory=list) ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by non-carrier ships that will be spawned unless the faction has #: Locations used by coastal defenses that are generated if the faction is capable.
#: 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.
coastal_defenses: List[PointWithHeading] = field(default_factory=list) 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. #: Locations used by ground based strike objectives.
strike_locations: List[PointWithHeading] = field(default_factory=list) 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. #: Locations used by offshore strike objectives.
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list) offshore_strike_locations: List[PointWithHeading] = field(default_factory=list)
#: Locations used by offshore strike objectives that will always be spawned. #: Locations used by missile sites like scuds and V-2s that are generated if the
required_offshore_strike_locations: List[PointWithHeading] = field( #: faction is capable.
default_factory=list
)
#: Locations used by missile sites like scuds and V-2s.
missile_sites: List[PointWithHeading] = field(default_factory=list) missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations used by missile sites like scuds and V-2s that are always generated if #: Locations of long range SAMs.
#: the faction is capable. long_range_sams: List[PointWithHeading] = field(default_factory=list)
required_missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations of long range SAMs which should always be spawned. #: Locations of medium range SAMs.
required_long_range_sams: List[PointWithHeading] = field(default_factory=list) medium_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of medium range SAMs which should always be spawned. #: Locations of short range SAMs.
required_medium_range_sams: List[PointWithHeading] = field(default_factory=list) short_range_sams: List[PointWithHeading] = field(default_factory=list)
#: Locations of short range SAMs which should always be spawned. #: Locations of AAA groups.
required_short_range_sams: List[PointWithHeading] = field(default_factory=list) aaa: List[PointWithHeading] = field(default_factory=list)
#: Locations of AAA groups which should always be spawned. #: Locations of EWRs.
required_aaa: List[PointWithHeading] = field(default_factory=list) ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations of EWRs which should always be spawned.
required_ewrs: List[PointWithHeading] = field(default_factory=list)
#: Locations of map scenery to create zones for. #: Locations of map scenery to create zones for.
scenery: List[SceneryGroup] = field(default_factory=list) 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) 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) 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) 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) @dataclass(frozen=True)
class PendingOccupancy: class AircraftAllocations:
present: int present: dict[Type[FlyingType], int]
ordered: int ordered: dict[Type[FlyingType], int]
transferring: 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 @property
def total(self) -> int: 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 @dataclass
@ -285,6 +265,13 @@ class GroundUnitDestination:
return self.total_value < other.total_value return self.total_value < other.total_value
@unique
class ControlPointStatus(IntEnum):
Functional = auto()
Damaged = auto()
Destroyed = auto()
class ControlPoint(MissionTarget, ABC): class ControlPoint(MissionTarget, ABC):
position = None # type: Point position = None # type: Point
@ -315,7 +302,6 @@ class ControlPoint(MissionTarget, ABC):
self.full_name = name self.full_name = name
self.at = at self.at = at
self.connected_objectives: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
self.helipads: List[Helipad] = [] self.helipads: List[Helipad] = []
@ -344,7 +330,7 @@ class ControlPoint(MissionTarget, ABC):
@property @property
def ground_objects(self) -> List[TheaterGroundObject]: def ground_objects(self) -> List[TheaterGroundObject]:
return list(itertools.chain(self.connected_objectives, self.base_defenses)) return list(self.connected_objectives)
@property @property
@abstractmethod @abstractmethod
@ -553,24 +539,6 @@ class ControlPoint(MissionTarget, ABC):
def is_friendly_to(self, control_point: ControlPoint) -> bool: def is_friendly_to(self, control_point: ControlPoint) -> bool:
return control_point.is_friendly(self.captured) 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: def capture_equipment(self, game: Game) -> None:
total = self.base.total_armor_value total = self.base.total_armor_value
self.base.armor.clear() self.base.armor.clear()
@ -625,7 +593,7 @@ class ControlPoint(MissionTarget, ABC):
max_retreat_distance = nautical_miles(200) max_retreat_distance = nautical_miles(200)
# Skip the first airbase because that's the airbase we're retreating # Skip the first airbase because that's the airbase we're retreating
# from. # from.
airfields = list(closest.airfields_within(max_retreat_distance))[1:] airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:]
for airbase in airfields: for airbase in airfields:
if not airbase.can_operate(airframe): if not airbase.can_operate(airframe):
continue continue
@ -655,11 +623,17 @@ class ControlPoint(MissionTarget, ABC):
airframe, count = self.base.aircraft.popitem() airframe, count = self.base.aircraft.popitem()
self._retreat_air_units(game, airframe, count) 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. # TODO: Should be Airbase specific.
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
self.pending_unit_deliveries.refund_all(game) self.pending_unit_deliveries.refund_all(game)
self.retreat_ground_units(game) self.retreat_ground_units(game)
self.retreat_air_units(game) self.retreat_air_units(game)
self.depopulate_uncapturable_tgos()
if for_player: if for_player:
self.captured = True self.captured = True
@ -668,46 +642,29 @@ class ControlPoint(MissionTarget, ABC):
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate()
@abstractmethod @abstractmethod
def can_operate(self, aircraft: Type[FlyingType]) -> bool: 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: if self.captured:
ato = game.blue_ato ato = game.blue_ato
else: else:
ato = game.red_ato ato = game.red_ato
total = 0 transferring: defaultdict[Type[FlyingType], int] = defaultdict(int)
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
if flight.departure == flight.arrival: if flight.departure == flight.arrival:
continue continue
if flight.departure == self: if flight.departure == self:
total -= flight.count transferring[flight.unit_type] -= flight.count
elif flight.arrival == self: elif flight.arrival == self:
total += flight.count transferring[flight.unit_type] += flight.count
return total return transferring
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)
)
def unclaimed_parking(self, game: Game) -> int: def unclaimed_parking(self, game: Game) -> int:
return ( return self.total_aircraft_parking - self.allocated_aircraft(game).total
self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
)
@abstractmethod @abstractmethod
def active_runway( def active_runway(
@ -757,47 +714,34 @@ class ControlPoint(MissionTarget, ABC):
u.position.x = u.position.x + delta.x u.position.x = u.position.x + delta.x
u.position.y = u.position.y + delta.y u.position.y = u.position.y + delta.y
@property def allocated_aircraft(self, game: Game) -> AircraftAllocations:
def pending_frontline_aa_deliveries_count(self): on_order = {}
""" for unit_bought, count in self.pending_unit_deliveries.units.items():
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:
if issubclass(unit_bought, FlyingType): if issubclass(unit_bought, FlyingType):
continue on_order[unit_bought] = count
if unit_bought in TYPE_SHORAD:
continue
on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy( return AircraftAllocations(
self.base.total_armor, 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, on_order,
# Ground unit transfers not yet implemented. transferring,
transferring=0,
) )
@property @property
@ -816,18 +760,27 @@ class ControlPoint(MissionTarget, ABC):
@property @property
def frontline_unit_count_limit(self) -> int: 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 ( return (
FREE_FRONTLINE_UNIT_SUPPLY 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 @property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]: def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return [] return []
@ -837,6 +790,11 @@ class ControlPoint(MissionTarget, ABC):
def category(self) -> str: def category(self) -> str:
... ...
@property
@abstractmethod
def status(self) -> ControlPointStatus:
...
class Airfield(ControlPoint): class Airfield(ControlPoint):
def __init__( def __init__(
@ -921,6 +879,15 @@ class Airfield(ControlPoint):
def category(self) -> str: def category(self) -> str:
return "airfield" 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): class NavalControlPoint(ControlPoint, ABC):
@property @property
@ -945,12 +912,16 @@ class NavalControlPoint(ControlPoint, ABC):
def heading(self) -> int: def heading(self) -> int:
return 0 # TODO compute heading 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: def runway_is_operational(self) -> bool:
# Necessary because it's possible for the carrier itself to have sunk # Necessary because it's possible for the carrier itself to have sunk
# while its escorts are still alive. # while its escorts are still alive.
for g in self.ground_objects: for group in self.find_main_tgo().groups:
if g.dcs_identifier in ["CARRIER", "LHA"]:
for group in g.groups:
for u in group.units: for u in group.units:
if db.unit_type_from_name(u.type) in [ if db.unit_type_from_name(u.type) in [
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
@ -984,6 +955,14 @@ class NavalControlPoint(ControlPoint, ABC):
def can_deploy_ground_units(self) -> bool: def can_deploy_ground_units(self) -> bool:
return False 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): class Carrier(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
@ -1113,6 +1092,10 @@ class OffMapSpawn(ControlPoint):
def category(self) -> str: def category(self) -> str:
return "offmap" return "offmap"
@property
def status(self) -> ControlPointStatus:
return ControlPointStatus.Functional
class Fob(ControlPoint): class Fob(ControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
@ -1176,3 +1159,7 @@ class Fob(ControlPoint):
@property @property
def category(self) -> str: def category(self) -> str:
return "fob" return "fob"
@property
def status(self) -> ControlPointStatus:
return ControlPointStatus.Functional

View File

@ -1,12 +1,11 @@
from __future__ import annotations from __future__ import annotations
from game.scenery_group import SceneryGroup
import logging import logging
import pickle import pickle
import random import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime 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.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike from dcs.task import CAP, CAS, PinpointStrike
@ -14,7 +13,8 @@ from dcs.vehicles import AirDefence
from game import Game, db from game import Game, db
from game.factions.faction import Faction 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 ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
CarrierGroundObject, CarrierGroundObject,
@ -39,8 +39,8 @@ from gen.fleet.ship_group_generator import (
) )
from gen.missiles.missiles_group_generator import generate_missile_group from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.airdefensegroupgenerator import AirDefenseRange 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.ewr_group_generator import generate_ewr_group
from gen.sam.sam_group_generator import generate_anti_air_group
from . import ( from . import (
ConflictTheater, ConflictTheater,
ControlPoint, ControlPoint,
@ -145,24 +145,6 @@ class GameGenerator:
cp.captured = True 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: class ControlPointGroundObjectGenerator:
def __init__( def __init__(
self, self,
@ -173,7 +155,6 @@ class ControlPointGroundObjectGenerator:
self.game = game self.game = game
self.generator_settings = generator_settings self.generator_settings = generator_settings
self.control_point = control_point self.control_point = control_point
self.location_finder = LocationFinder(control_point)
@property @property
def faction_name(self) -> str: def faction_name(self) -> str:
@ -203,19 +184,9 @@ class ControlPointGroundObjectGenerator:
if not self.control_point.captured and skip_enemy_navy: if not self.control_point.captured and skip_enemy_navy:
return return
self.generate_required_ships() for position in self.control_point.preset_locations.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:
self.generate_ship_at(position) 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: def generate_ship_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -289,159 +260,6 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
return True 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): class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__( def __init__(
self, self,
@ -457,16 +275,14 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not super().generate(): if not super().generate():
return False return False
BaseDefenseGenerator(self.game, self.control_point).generate()
self.generate_ground_points() self.generate_ground_points()
return True return True
def generate_ground_points(self) -> None: def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point.""" """Generate ground objects and AA sites for the control point."""
self.generate_armor_groups() self.generate_armor_groups()
skip_sams = self.generate_required_aa() self.generate_aa()
skip_ewrs = self.generate_required_ewr() self.generate_ewrs()
self.generate_scenery_sites() self.generate_scenery_sites()
self.generate_strike_targets() self.generate_strike_targets()
self.generate_offshore_strike_targets() self.generate_offshore_strike_targets()
@ -475,35 +291,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if self.faction.missiles: if self.faction.missiles:
self.generate_missile_sites() self.generate_missile_sites()
self.generate_required_missile_sites()
if self.faction.coastal_defenses: if self.faction.coastal_defenses:
self.generate_coastal_sites() 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: def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups: for position in self.control_point.preset_locations.armor_groups:
@ -517,7 +307,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id, group_id,
position, position,
self.control_point, self.control_point,
for_airbase=False,
) )
group = generate_armor_group(self.faction_name, self.game, g) group = generate_armor_group(self.faction_name, self.game, g)
@ -531,14 +320,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
g.groups = [group] g.groups = [group]
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
def generate_required_aa(self) -> int: def generate_aa(self) -> None:
"""Generates the AA sites that are required by the campaign.
Returns:
The number of AA sites that were generated.
"""
presets = self.control_point.preset_locations 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( self.generate_aa_at(
position, position,
ranges=[ ranges=[
@ -548,7 +332,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
{AirDefenseRange.AAA}, {AirDefenseRange.AAA},
], ],
) )
for position in presets.required_medium_range_sams: for position in presets.medium_range_sams:
self.generate_aa_at( self.generate_aa_at(
position, position,
ranges=[ ranges=[
@ -557,52 +341,21 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
{AirDefenseRange.AAA}, {AirDefenseRange.AAA},
], ],
) )
for position in presets.required_short_range_sams: for position in presets.short_range_sams:
self.generate_aa_at( self.generate_aa_at(
position, position,
ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}], ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}],
) )
for position in presets.required_aaa: for position in presets.aaa:
self.generate_aa_at( self.generate_aa_at(
position, position,
ranges=[{AirDefenseRange.AAA}], 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: def generate_ewrs(self) -> None:
"""Generates the EWR sites that are required by the campaign.
Returns:
The number of EWR sites that were generated.
"""
presets = self.control_point.preset_locations presets = self.control_point.preset_locations
for position in presets.required_ewrs: for position in presets.ewrs:
self.generate_ewr_at(position) 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: 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) self.generate_strike_target_at(category="ammo", position=position)
def generate_factories(self) -> None: def generate_factories(self) -> None:
"""Generates the factories that are required by the campaign."""
for position in self.control_point.preset_locations.factories: for position in self.control_point.preset_locations.factories:
self.generate_factory_at(position) self.generate_factory_at(position)
@ -653,19 +405,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g) 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( def generate_aa_at(
self, position: Point, ranges: Iterable[Set[AirDefenseRange]] self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
) -> None: ) -> None:
@ -676,7 +415,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id, group_id,
position, position,
self.control_point, self.control_point,
for_airbase=False,
) )
groups = generate_anti_air_group(self.game, g, self.faction, ranges) groups = generate_anti_air_group(self.game, g, self.faction, ranges)
if not groups: if not groups:
@ -689,12 +427,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
g.groups = groups g.groups = groups
self.control_point.connected_objectives.append(g) 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: def generate_ewr_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -703,7 +435,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id, group_id,
position, position,
self.control_point, self.control_point,
for_airbase=False,
) )
group = generate_ewr_group(self.game, g, self.faction) group = generate_ewr_group(self.game, g, self.faction)
if group is None: if group is None:
@ -750,18 +481,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return 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: def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count): for position in self.control_point.preset_locations.missile_sites:
self.generate_missile_site() self.generate_missile_site_at(position)
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)
def generate_missile_site_at(self, position: PointWithHeading) -> None: def generate_missile_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id() group_id = self.game.next_group_id()
@ -776,17 +498,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)
return 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: def generate_coastal_sites(self) -> None:
for i in range(self.faction.coastal_group_count): for position in self.control_point.preset_locations.coastal_defenses:
self.generate_coastal_site()
def generate_coastal_site(self) -> None:
position = self.location_finder.location_for(LocationType.Coastal)
if position is not None:
self.generate_coastal_site_at(position) self.generate_coastal_site_at(position)
def generate_coastal_site_at(self, position: PointWithHeading) -> None: def generate_coastal_site_at(self, position: PointWithHeading) -> None:
@ -807,46 +520,39 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return return
def generate_strike_targets(self) -> None: 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"}) building_set = list(set(self.faction.building_set) - {"oil"})
if not building_set: if not building_set:
logging.error("Faction has no buildings defined") logging.error("Faction has no buildings defined")
return 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) category = random.choice(building_set)
self.generate_strike_target_at(category, position) self.generate_strike_target_at(category, position)
def generate_offshore_strike_targets(self) -> None: 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: if "oil" not in self.faction.building_set:
logging.error("Faction does not support offshore strike targets") logging.error("Faction does not support offshore strike targets")
return return
for ( for position in self.control_point.preset_locations.offshore_strike_locations:
position
) in self.control_point.preset_locations.required_offshore_strike_locations:
self.generate_strike_target_at("oil", position) self.generate_strike_target_at("oil", position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator): class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool: def generate(self) -> bool:
self.generate_fob() self.generate_fob()
FobDefenseGenerator(self.game, self.control_point).generate()
self.generate_armor_groups() self.generate_armor_groups()
self.generate_factories() self.generate_factories()
self.generate_ammunition_depots() self.generate_ammunition_depots()
self.generate_required_aa() self.generate_aa()
self.generate_required_ewr() self.generate_ewrs()
self.generate_scenery_sites() self.generate_scenery_sites()
self.generate_strike_targets() self.generate_strike_targets()
self.generate_offshore_strike_targets() self.generate_offshore_strike_targets()
if self.faction.missiles: if self.faction.missiles:
self.generate_missile_sites() self.generate_missile_sites()
self.generate_required_missile_sites()
if self.faction.coastal_defenses: if self.faction.coastal_defenses:
self.generate_coastal_sites() self.generate_coastal_sites()
self.generate_required_coastal_sites()
return True return True
@ -873,7 +579,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
unit["heading"], unit["heading"],
self.control_point, self.control_point,
unit["type"], unit["type"],
airbase_group=True, is_fob_structure=True,
) )
self.control_point.connected_objectives.append(g) self.control_point.connected_objectives.append(g)

View File

@ -12,7 +12,6 @@ from dcs.unittype import VehicleType
from .. import db from .. import db
from ..data.radar_db import ( from ..data.radar_db import (
UNITS_WITH_RADAR,
TRACK_RADARS, TRACK_RADARS,
TELARS, TELARS,
LAUNCHER_TRACKER_PAIRS, LAUNCHER_TRACKER_PAIRS,
@ -58,7 +57,6 @@ class TheaterGroundObject(MissionTarget):
heading: int, heading: int,
control_point: ControlPoint, control_point: ControlPoint,
dcs_identifier: str, dcs_identifier: str,
airbase_group: bool,
sea_object: bool, sea_object: bool,
) -> None: ) -> None:
super().__init__(name, position) super().__init__(name, position)
@ -67,7 +65,6 @@ class TheaterGroundObject(MissionTarget):
self.heading = heading self.heading = heading
self.control_point = control_point self.control_point = control_point
self.dcs_identifier = dcs_identifier self.dcs_identifier = dcs_identifier
self.airbase_group = airbase_group
self.sea_object = sea_object self.sea_object = sea_object
self.groups: List[Group] = [] self.groups: List[Group] = []
@ -193,6 +190,21 @@ class TheaterGroundObject(MissionTarget):
def strike_targets(self) -> List[Union[MissionTarget, Unit]]: def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return self.units 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): class BuildingGroundObject(TheaterGroundObject):
def __init__( def __init__(
@ -205,7 +217,7 @@ class BuildingGroundObject(TheaterGroundObject):
heading: int, heading: int,
control_point: ControlPoint, control_point: ControlPoint,
dcs_identifier: str, dcs_identifier: str,
airbase_group=False, is_fob_structure=False,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
@ -215,9 +227,9 @@ class BuildingGroundObject(TheaterGroundObject):
heading=heading, heading=heading,
control_point=control_point, control_point=control_point,
dcs_identifier=dcs_identifier, dcs_identifier=dcs_identifier,
airbase_group=airbase_group,
sea_object=False, sea_object=False,
) )
self.is_fob_structure = is_fob_structure
self.object_id = object_id self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but # Other TGOs track deadness based on the number of alive units, but
# buildings don't have groups assigned to the TGO. # buildings don't have groups assigned to the TGO.
@ -250,6 +262,23 @@ class BuildingGroundObject(TheaterGroundObject):
def strike_targets(self) -> List[Union[MissionTarget, Unit]]: def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return list(self.iter_building_group()) 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): class SceneryGroundObject(BuildingGroundObject):
def __init__( def __init__(
@ -272,7 +301,7 @@ class SceneryGroundObject(BuildingGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier=dcs_identifier, dcs_identifier=dcs_identifier,
airbase_group=False, is_fob_structure=False,
) )
self.zone = zone self.zone = zone
try: try:
@ -305,7 +334,7 @@ class FactoryGroundObject(BuildingGroundObject):
heading=heading, heading=heading,
control_point=control_point, control_point=control_point,
dcs_identifier="Workshop A", dcs_identifier="Workshop A",
airbase_group=False, is_fob_structure=False,
) )
@ -321,6 +350,14 @@ class NavalGroundObject(TheaterGroundObject):
def might_have_aa(self) -> bool: def might_have_aa(self) -> bool:
return True return True
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return False
class GenericCarrierGroundObject(NavalGroundObject): class GenericCarrierGroundObject(NavalGroundObject):
@property @property
@ -339,7 +376,6 @@ class CarrierGroundObject(GenericCarrierGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier="CARRIER", dcs_identifier="CARRIER",
airbase_group=True,
sea_object=True, sea_object=True,
) )
@ -361,7 +397,6 @@ class LhaGroundObject(GenericCarrierGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier="LHA", dcs_identifier="LHA",
airbase_group=True,
sea_object=True, sea_object=True,
) )
@ -384,10 +419,17 @@ class MissileSiteGroundObject(TheaterGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False,
sea_object=False, sea_object=False,
) )
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return False
class CoastalSiteGroundObject(TheaterGroundObject): class CoastalSiteGroundObject(TheaterGroundObject):
def __init__( def __init__(
@ -406,26 +448,28 @@ class CoastalSiteGroundObject(TheaterGroundObject):
heading=heading, heading=heading,
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False,
sea_object=False, sea_object=False,
) )
@property
def capturable(self) -> bool:
return False
class BaseDefenseGroundObject(TheaterGroundObject): @property
"""Base type for all base defenses.""" def purchasable(self) -> bool:
return False
# TODO: Differentiate types. # TODO: Differentiate types.
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each # This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# be split into their own types. # be split into their own types.
class SamGroundObject(BaseDefenseGroundObject): class SamGroundObject(TheaterGroundObject):
def __init__( def __init__(
self, self,
name: str, name: str,
group_id: int, group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
for_airbase: bool,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
@ -435,7 +479,6 @@ class SamGroundObject(BaseDefenseGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False, sea_object=False,
) )
# Set by the SAM unit generator if the generated group is compatible # Set by the SAM unit generator if the generated group is compatible
@ -495,15 +538,22 @@ class SamGroundObject(BaseDefenseGroundObject):
else: else:
return max(max_tel_range, max_telar_range, max_non_radar) 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__( def __init__(
self, self,
name: str, name: str,
group_id: int, group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
for_airbase: bool,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
@ -513,19 +563,25 @@ class VehicleGroupGroundObject(BaseDefenseGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False, 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__( def __init__(
self, self,
name: str, name: str,
group_id: int, group_id: int,
position: Point, position: Point,
control_point: ControlPoint, control_point: ControlPoint,
for_airbase: bool,
) -> None: ) -> None:
super().__init__( super().__init__(
name=name, name=name,
@ -535,7 +591,6 @@ class EwrGroundObject(BaseDefenseGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier="EWR", dcs_identifier="EWR",
airbase_group=for_airbase,
sea_object=False, sea_object=False,
) )
@ -555,6 +610,14 @@ class EwrGroundObject(BaseDefenseGroundObject):
def might_have_aa(self) -> bool: def might_have_aa(self) -> bool:
return True return True
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return True
class ShipGroundObject(NavalGroundObject): class ShipGroundObject(NavalGroundObject):
def __init__( def __init__(
@ -568,7 +631,6 @@ class ShipGroundObject(NavalGroundObject):
heading=0, heading=0,
control_point=control_point, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False,
sea_object=True, sea_object=True,
) )

View File

@ -40,6 +40,10 @@ class ThreatZones:
) )
return DcsPoint(boundary.x, boundary.y) 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 @singledispatchmethod
def threatened(self, position) -> bool: def threatened(self, position) -> bool:
raise NotImplementedError raise NotImplementedError
@ -124,7 +128,7 @@ class ThreatZones:
cls, location: ControlPoint, max_distance: Distance cls, location: ControlPoint, max_distance: Distance
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
airfields = ObjectiveDistanceCache.get_closest_airfields(location) 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: if airfield.captured != location.captured:
return airfield return airfield
return None return None

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import math
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import singledispatchmethod from functools import singledispatchmethod
@ -89,10 +90,9 @@ class TransferOrder:
self.units.clear() self.units.clear()
def kill_unit(self, unit_type: Type[VehicleType]) -> None: def kill_unit(self, unit_type: Type[VehicleType]) -> None:
if unit_type in self.units: 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 self.units[unit_type] -= 1
return
raise KeyError
@property @property
def size(self) -> int: def size(self) -> int:
@ -238,7 +238,7 @@ class AirliftPlanner:
for s in self.game.air_wing_for(self.for_player).squadrons_for( for s in self.game.air_wing_for(self.for_player).squadrons_for(
unit_type unit_type
) )
if FlightType.TRANSPORT in s.mission_types if FlightType.TRANSPORT in s.auto_assignable_mission_types
] ]
if not squadrons: if not squadrons:
continue continue
@ -254,11 +254,13 @@ class AirliftPlanner:
self, squadron: Squadron, inventory: ControlPointAircraftInventory self, squadron: Squadron, inventory: ControlPointAircraftInventory
) -> int: ) -> int:
available = inventory.available(squadron.aircraft) available = inventory.available(squadron.aircraft)
# 4 is the max flight size in DCS. capacity_each = 1 if squadron.aircraft.helicopter else 2
flight_size = min(self.transfer.size, available, 4) 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: if capacity < self.transfer.size:
transfer = self.game.transfers.split_transfer(self.transfer, flight_size) transfer = self.game.transfers.split_transfer(self.transfer, capacity)
else: else:
transfer = self.transfer transfer = self.transfer
@ -530,33 +532,35 @@ class PendingTransfers:
return new_transfer return new_transfer
@singledispatchmethod @singledispatchmethod
def cancel_transport(self, transfer: TransferOrder, transport) -> None: def cancel_transport(self, transport, transfer: TransferOrder) -> None:
pass pass
@cancel_transport.register @cancel_transport.register
def _cancel_transport_air( def _cancel_transport_air(
self, _transfer: TransferOrder, transport: Airlift self, transport: Airlift, _transfer: TransferOrder
) -> None: ) -> None:
flight = transport.flight flight = transport.flight
flight.package.remove_flight(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) self.game.aircraft_inventory.return_from_flight(flight)
flight.clear_roster() flight.clear_roster()
@cancel_transport.register @cancel_transport.register
def _cancel_transport_convoy( def _cancel_transport_convoy(
self, transfer: TransferOrder, transport: Convoy self, transport: Convoy, transfer: TransferOrder
) -> None: ) -> None:
self.convoys.remove(transport, transfer) self.convoys.remove(transport, transfer)
@cancel_transport.register @cancel_transport.register
def _cancel_transport_cargo_ship( def _cancel_transport_cargo_ship(
self, transfer: TransferOrder, transport: CargoShip self, transport: CargoShip, transfer: TransferOrder
) -> None: ) -> None:
self.cargo_ships.remove(transport, transfer) self.cargo_ships.remove(transport, transfer)
def cancel_transfer(self, transfer: TransferOrder) -> None: def cancel_transfer(self, transfer: TransferOrder) -> None:
if transfer.transport is not None: if transfer.transport is not None:
self.cancel_transport(transfer, transfer.transport) self.cancel_transport(transfer.transport, transfer)
self.pending_transfers.remove(transfer) self.pending_transfers.remove(transfer)
transfer.origin.base.commision_units(transfer.units) transfer.origin.base.commision_units(transfer.units)

View File

@ -1,4 +1,6 @@
"""Maps generated units back to their Liberation types.""" """Maps generated units back to their Liberation types."""
import itertools
import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Optional, Type from typing import Dict, Optional, Type
@ -40,8 +42,8 @@ class ConvoyUnit:
@dataclass(frozen=True) @dataclass(frozen=True)
class AirliftUnit: class AirliftUnits:
unit_type: Type[VehicleType] cargo: tuple[Type[VehicleType], ...]
transfer: TransferOrder transfer: TransferOrder
@ -59,10 +61,10 @@ class UnitMap:
self.buildings: Dict[str, Building] = {} self.buildings: Dict[str, Building] = {}
self.convoys: Dict[str, ConvoyUnit] = {} self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {} 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: 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 # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
name = str(unit.name) name = str(unit.name)
@ -177,15 +179,26 @@ class UnitMap:
return self.cargo_ships.get(name, None) return self.cargo_ships.get(name, None)
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> 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 # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
name = str(transport.name) name = str(transport.name)
if name in self.airlifts: if name in self.airlifts:
raise RuntimeError(f"Duplicate airlift unit: {name}") 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) return self.airlifts.get(name, None)
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None: def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:

View File

@ -2,7 +2,7 @@ from pathlib import Path
def _build_version_string() -> str: def _build_version_string() -> str:
components = ["3.0"] components = ["4.0"]
build_number_path = Path("resources/buildnumber") build_number_path = Path("resources/buildnumber")
if build_number_path.exists(): if build_number_path.exists():
with build_number_path.open("r") as build_number_file: 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, #: * SPAAA_ZSU_23_4_Shilka_Gun_Dish,
#: #:
#: Version 5.0 #: Version 5.0
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition Depot" #: * Ammunition Depots objective locations are now predetermined using the "Ammunition
#: Warehouse object, and through trigger zone based scenery objects. # Depot" Warehouse object, and through trigger zone based scenery objects.
#: * The number of alive Ammunition Depot objective buildings connected to a control point #: * The number of alive Ammunition Depot objective buildings connected to a control
#: directly influences how many ground units can be supported on the front line. #: point directly influences how many ground units can be supported on the front
#: * The number of supported ground units at any control point is artificially capped at 50, #: line.
#: even if the number of alive Ammunition Depot objectives can support more. #: * The number of supported ground units at any control point is artificially
CAMPAIGN_FORMAT_VERSION = (5, 0) #: 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)

View File

@ -803,7 +803,7 @@ class AircraftConflictGenerator:
self._setup_payload(flight, group) self._setup_payload(flight, group)
self._setup_livery(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 player = pilot is not None and pilot.player
self.set_skill(unit, pilot, blue=flight.departure.captured) self.set_skill(unit, pilot, blue=flight.departure.captured)
# Do not generate player group with late activation. # Do not generate player group with late activation.

View File

@ -162,7 +162,7 @@ class AircraftAllocator:
self, flight: ProposedFlight, task: FlightType self, flight: ProposedFlight, task: FlightType
) -> Optional[Tuple[ControlPoint, Squadron]]: ) -> Optional[Tuple[ControlPoint, Squadron]]:
types = aircraft_for_task(task) 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 flight.max_distance
) )
@ -180,7 +180,7 @@ class AircraftAllocator:
# Valid location with enough aircraft available. Find a squadron to fit # Valid location with enough aircraft available. Find a squadron to fit
# the role. # the role.
for squadron in self.air_wing.squadrons_for(aircraft): 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 continue
if len(squadron.available_pilots) >= flight.num_aircraft: if len(squadron.available_pilots) >= flight.num_aircraft:
inventory.remove_aircraft(aircraft, flight.num_aircraft) inventory.remove_aircraft(aircraft, flight.num_aircraft)
@ -258,7 +258,9 @@ class PackageBuilder:
self, aircraft: Type[FlyingType], arrival: ControlPoint self, aircraft: Type[FlyingType], arrival: ControlPoint
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150) 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: if airfield.captured != self.is_player:
continue continue
if airfield == arrival: if airfield == arrival:
@ -433,7 +435,7 @@ class ObjectiveFinder:
is_building = isinstance(ground_object, BuildingGroundObject) is_building = isinstance(ground_object, BuildingGroundObject)
is_fob = isinstance(enemy_cp, Fob) 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 # This is the FOB structure itself. Can't be repaired or
# targeted by the player, so shouldn't be targetable by the # targeted by the player, so shouldn't be targetable by the
# AI. # AI.
@ -467,9 +469,11 @@ class ObjectiveFinder:
# Off-map spawn locations don't need protection. # Off-map spawn locations don't need protection.
continue continue
airfields_in_proximity = self.closest_airfields_to(cp) airfields_in_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = airfields_in_proximity.airfields_within( airfields_in_threat_range = (
airfields_in_proximity.operational_airfields_within(
self.AIRFIELD_THREAT_RANGE self.AIRFIELD_THREAT_RANGE
) )
)
for airfield in airfields_in_threat_range: for airfield in airfields_in_threat_range:
if not airfield.is_friendly(self.is_player): if not airfield.is_friendly(self.is_player):
yield cp yield cp
@ -502,31 +506,23 @@ class ObjectiveFinder:
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player) c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
) )
def farthest_friendly_control_point(self) -> Optional[ControlPoint]: def farthest_friendly_control_point(self) -> ControlPoint:
""" """Finds the friendly control point that is farthest from any threats."""
Iterates over all friendly control points and find the one farthest away from the frontline threat_zones = self.game.threat_zone_for(not self.is_player)
BUT! prefer Cvs. Everybody likes CVs!
"""
from_frontline = 0
cp = None
first_friendly_cp = None
for c in self.game.theater.controlpoints: farthest = None
if c.is_friendly(self.is_player): max_distance = meters(0)
if first_friendly_cp is None: for cp in self.friendly_control_points():
first_friendly_cp = c if isinstance(cp, OffMapSpawn):
if c.is_carrier: continue
return c distance = threat_zones.distance_to_threat(cp.position)
if c.has_active_frontline: if distance > max_distance:
if c.distance_to(self.front_lines().__next__()) > from_frontline: farthest = cp
from_frontline = c.distance_to(self.front_lines().__next__()) max_distance = distance
cp = c
# If no frontlines on the map, return the first friendly cp if farthest is None:
if cp is None: raise RuntimeError("Found no friendly control points. You probably lost.")
return first_friendly_cp return farthest
else:
return cp
def enemy_control_points(self) -> Iterator[ControlPoint]: def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points.""" """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(): for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
if ( if (
squadron.aircraft in all_compatible squadron.aircraft in all_compatible
and mission_type in squadron.mission_types and mission_type in squadron.auto_assignable_mission_types
): ):
return True return True
return False return False
@ -624,11 +620,9 @@ class CoalitionMissionPlanner:
eliminated this turn. eliminated this turn.
""" """
# Find farthest, friendly CP for AEWC # Find farthest, friendly CP for AEWC.
cp = self.objective_finder.farthest_friendly_control_point()
if cp is not None:
yield ProposedMission( yield ProposedMission(
cp, self.objective_finder.farthest_friendly_control_point(),
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)], [ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
# Supports all the early CAP flights, so should be in the air ASAP. # Supports all the early CAP flights, so should be in the air ASAP.
asap=True, asap=True,
@ -1012,7 +1006,7 @@ class CoalitionMissionPlanner:
interval = (latest - earliest) // count interval = (latest - earliest) // count
for time in range(earliest, latest, interval): for time in range(earliest, latest, interval):
error = random.randint(-margin, margin) error = random.randint(-margin, margin)
yield timedelta(minutes=max(0, time + error)) yield timedelta(seconds=max(0, time + error))
dca_types = { dca_types = {
FlightType.BARCAP, FlightType.BARCAP,
@ -1026,11 +1020,11 @@ class CoalitionMissionPlanner:
start_time = start_time_generator( start_time = start_time_generator(
count=len(non_dca_packages), count=len(non_dca_packages),
earliest=5, earliest=5 * 60,
latest=int( 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: for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot() tot = TotEstimator(package).earliest_tot()

View File

@ -31,17 +31,35 @@ class ClosestAirfields:
if c.runway_is_operational() or c.has_helipads 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. """Iterates over all airfields within the given range of the target.
Note that this iterates over *all* airfields, not just friendly Note that this iterates over *all* airfields, not just friendly
airfields. airfields.
""" """
for cp in self.closest_airfields: return self._airfields_within(distance, operational=True)
if cp.distance_to(self.target) < distance.meters:
yield cp def all_airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
else: """Iterates over all airfields within the given range of the target.
break
Note that this iterates over *all* airfields, not just friendly
airfields.
"""
return self._airfields_within(distance, operational=False)
class ObjectiveDistanceCache: class ObjectiveDistanceCache:

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Type, Union from typing import List, Optional, TYPE_CHECKING, Type, Union
@ -202,6 +203,49 @@ class FlightWaypoint:
return waypoint 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: class Flight:
def __init__( def __init__(
self, self,
@ -216,11 +260,15 @@ class Flight:
divert: Optional[ControlPoint], divert: Optional[ControlPoint],
custom_name: Optional[str] = None, custom_name: Optional[str] = None,
cargo: Optional[TransferOrder] = None, cargo: Optional[TransferOrder] = None,
roster: Optional[FlightRoster] = None,
) -> None: ) -> None:
self.package = package self.package = package
self.country = country self.country = country
self.squadron = squadron 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.departure = departure
self.arrival = arrival self.arrival = arrival
self.divert = divert self.divert = divert
@ -246,11 +294,11 @@ class Flight:
@property @property
def count(self) -> int: def count(self) -> int:
return len(self.pilots) return self.roster.max_size
@property @property
def client_count(self) -> int: 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 @property
def unit_type(self) -> Type[FlyingType]: def unit_type(self) -> Type[FlyingType]:
@ -265,32 +313,17 @@ class Flight:
return self.flight_plan.waypoints[1:] return self.flight_plan.waypoints[1:]
def resize(self, new_size: int) -> None: def resize(self, new_size: int) -> None:
if self.count > new_size: self.roster.resize(new_size)
self.squadron.return_pilots(
p for p in self.pilots[new_size:] if p is not None
)
self.pilots = self.pilots[:new_size]
return
self.pilots.extend(
[
self.squadron.claim_available_pilot()
for _ in range(new_size - self.count)
]
)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None: def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
if pilot is not None: self.roster.set_pilot(index, pilot)
self.squadron.claim_pilot(pilot)
if (current_pilot := self.pilots[index]) is not None:
self.squadron.return_pilot(current_pilot)
self.pilots[index] = pilot
@property @property
def missing_pilots(self) -> int: 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: 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): def __repr__(self):
name = db.unit_type_name(self.unit_type) name = db.unit_type_name(self.unit_type)

View File

@ -1057,7 +1057,7 @@ class FlightPlanBuilder:
""" """
location = self.package.target 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. # As high as possible to maximize detection and on-station time.
if flight.unit_type == E_2C: if flight.unit_type == E_2C:
@ -1072,22 +1072,22 @@ class FlightPlanBuilder:
patrol_alt = feet(25000) patrol_alt = feet(25000)
builder = WaypointBuilder(flight, self.game, self.is_player) builder = WaypointBuilder(flight, self.game, self.is_player)
start = builder.orbit(start, patrol_alt) orbit_location = builder.orbit(orbit_location, patrol_alt)
return AwacsFlightPlan( return AwacsFlightPlan(
package=self.package, package=self.package,
flight=flight, flight=flight,
takeoff=builder.takeoff(flight.departure), takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path( 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( 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), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
bullseye=builder.bullseye(), bullseye=builder.bullseye(),
hold=start, hold=orbit_location,
hold_duration=timedelta(hours=4), hold_duration=timedelta(hours=4),
) )
@ -1339,20 +1339,24 @@ class FlightPlanBuilder:
return start, end return start, end
def aewc_orbit(self, location: MissionTarget) -> Point: def aewc_orbit(self, location: MissionTarget) -> Point:
# in threat zone
if self.threat_zones.threatened(location.position):
# Borderpoint
closest_boundary = self.threat_zones.closest_boundary(location.position) closest_boundary = self.threat_zones.closest_boundary(location.position)
heading_to_threat_boundary = location.position.heading_between_point(
# Heading + Distance to border point closest_boundary
heading = location.position.heading_between_point(closest_boundary) )
distance = location.position.distance_to_point(closest_boundary) distance_to_threat = meters(
location.position.distance_to_point(closest_boundary)
return location.position.point_from_heading(heading, distance) )
orbit_heading = heading_to_threat_boundary
# this Part is fine. No threat zone, just use our point # Station 100nm outside the threat zone.
threat_buffer = nautical_miles(100)
if self.threat_zones.threatened(location.position):
orbit_distance = distance_to_threat + threat_buffer
else: 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( def racetrack_for_frontline(
self, origin: Point, front_line: 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 # We'll always have a package, but if this is being planned via the UI
# it could be the first flight in the package. # it could be the first flight in the package.
if not self.package.flights: if not self.package.flights:
raise RuntimeError( raise PlanningError(
"Cannot determine source airfield for package with no flights" "Cannot determine source airfield for package with no flights"
) )
@ -1819,5 +1823,4 @@ class FlightPlanBuilder:
for flight in self.package.flights: for flight in self.package.flights:
if flight.departure == airfield: if flight.departure == airfield:
return airfield return airfield
raise PlanningError("Could not find any airfield assigned to this package")
raise RuntimeError("Could not find any airfield assigned to this package")

View File

@ -1,3 +1,4 @@
import logging
import random import random
from enum import Enum from enum import Enum
from typing import Dict, List from typing import Dict, List
@ -5,7 +6,8 @@ from typing import Dict, List
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from game.theater import ControlPoint 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 from gen.ground_forces.combat_stance import CombatStance
MAX_COMBAT_GROUP_PER_CP = 10 MAX_COMBAT_GROUP_PER_CP = 10
@ -91,37 +93,35 @@ class GroundPlanner:
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE] group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
# Create combat groups and assign them randomly to each enemy CP # Create combat groups and assign them randomly to each enemy CP
for key in self.cp.base.armor.keys(): for unit_type in self.cp.base.armor:
if unit_type in GroundUnitClass.Tank:
role = None
collection = None
if key in TYPE_TANKS:
collection = self.tank_groups collection = self.tank_groups
role = CombatGroupRole.TANK role = CombatGroupRole.TANK
elif key in TYPE_APC: elif unit_type in GroundUnitClass.Apc:
collection = self.apc_group collection = self.apc_group
role = CombatGroupRole.APC role = CombatGroupRole.APC
elif key in TYPE_ARTILLERY: elif unit_type in GroundUnitClass.Artillery:
collection = self.art_group collection = self.art_group
role = CombatGroupRole.ARTILLERY role = CombatGroupRole.ARTILLERY
elif key in TYPE_IFV: elif unit_type in GroundUnitClass.Ifv:
collection = self.ifv_group collection = self.ifv_group
role = CombatGroupRole.IFV role = CombatGroupRole.IFV
elif key in TYPE_LOGI: elif unit_type in GroundUnitClass.Logistics:
collection = self.logi_groups collection = self.logi_groups
role = CombatGroupRole.LOGI role = CombatGroupRole.LOGI
elif key in TYPE_ATGM: elif unit_type in GroundUnitClass.Atgm:
collection = self.atgm_group collection = self.atgm_group
role = CombatGroupRole.ATGM role = CombatGroupRole.ATGM
elif key in TYPE_SHORAD: elif unit_type in GroundUnitClass.Shorads:
collection = self.shorad_groups collection = self.shorad_groups
role = CombatGroupRole.SHORAD role = CombatGroupRole.SHORAD
else: else:
print("Warning unit type not handled by ground generator") logging.warning(
print(key) f"Unused front line vehicle at base {unit_type}: unknown unit class"
)
continue continue
available = self.cp.base.armor[key] available = self.cp.base.armor[unit_type]
if available > remaining_available_frontline_units: if available > remaining_available_frontline_units:
available = remaining_available_frontline_units available = remaining_available_frontline_units
@ -151,7 +151,7 @@ class GroundPlanner:
group.assigned_enemy_cp = "__reserve__" group.assigned_enemy_cp = "__reserve__"
for i in range(n): for i in range(n):
group.units.append(key) group.units.append(unit_type)
collection.append(group) collection.append(group)
if remaining_available_frontline_units == 0: if remaining_available_frontline_units == 0:
@ -161,7 +161,7 @@ class GroundPlanner:
print("Ground Planner : ") print("Ground Planner : ")
print(self.cp.name) print(self.cp.name)
print("------------------") print("------------------")
for key in self.units_per_cp.keys(): for unit_type in self.units_per_cp.keys():
print("For : #" + str(key)) print("For : #" + str(unit_type))
for group in self.units_per_cp[key]: for group in self.units_per_cp[unit_type]:
print(str(group)) print(str(group))

View File

@ -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,
]

View File

@ -15,7 +15,7 @@ from dcs import Mission, Point, unitgroup
from dcs.action import SceneryDestructionZone from dcs.action import SceneryDestructionZone
from dcs.country import Country from dcs.country import Country
from dcs.point import StaticPoint 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 ( from dcs.task import (
ActivateBeaconCommand, ActivateBeaconCommand,
ActivateICLSCommand, ActivateICLSCommand,
@ -24,7 +24,7 @@ from dcs.task import (
FireAtPoint, FireAtPoint,
) )
from dcs.triggers import TriggerStart, TriggerZone 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.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType from dcs.unittype import StaticType, UnitType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
@ -76,8 +76,12 @@ class GenericGroundObjectGenerator:
self.m = mission self.m = mission
self.unit_map = unit_map self.unit_map = unit_map
@property
def culled(self) -> bool:
return self.game.position_culled(self.ground_object.position)
def generate(self) -> None: def generate(self) -> None:
if self.game.position_culled(self.ground_object.position): if self.culled:
return return
for group in self.ground_object.groups: for group in self.ground_object.groups:
@ -130,6 +134,12 @@ class GenericGroundObjectGenerator:
class MissileSiteGenerator(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: def generate(self) -> None:
super(MissileSiteGenerator, self).generate() super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now) # Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)

View File

@ -312,10 +312,6 @@ class NameGenerator:
db.unit_type_name(unit_type), db.unit_type_name(unit_type),
) )
@staticmethod
def next_basedefense_name():
return "basedefense_aa|0|0|"
@classmethod @classmethod
def next_awacs_name(cls, country: Country): def next_awacs_name(cls, country: Country):
cls.number += 1 cls.number += 1
@ -352,7 +348,7 @@ class NameGenerator:
for _ in range(10): for _ in range(10):
alpha = random.choice(ALPHA_MILITARY).upper() alpha = random.choice(ALPHA_MILITARY).upper()
number = str(random.randint(0, 100)) number = random.randint(0, 100)
alpha_mil_name = f"{alpha} #{number:02}" alpha_mil_name = f"{alpha} #{number:02}"
if alpha_mil_name not in cls.existing_alphas: if alpha_mil_name not in cls.existing_alphas:
cls.existing_alphas.append(alpha_mil_name) cls.existing_alphas.append(alpha_mil_name)

View File

@ -21,6 +21,8 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
This is the base for all SAM group generators This is the base for all SAM group generators
""" """
price: int
def __init__(self, game: Game, ground_object: SamGroundObject) -> None: def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
ground_object.skynet_capable = True ground_object.skynet_capable = True
super().__init__(game, ground_object) super().__init__(game, ground_object)

View File

@ -17,8 +17,8 @@ from gen.sam.ewrs import (
SnowDriftGenerator, SnowDriftGenerator,
StraightFlushGenerator, StraightFlushGenerator,
TallRackGenerator, TallRackGenerator,
EwrGenerator,
) )
from gen.sam.group_generator import GroupGenerator
EWR_MAP = { EWR_MAP = {
"BoxSpringGenerator": BoxSpringGenerator, "BoxSpringGenerator": BoxSpringGenerator,
@ -36,7 +36,7 @@ EWR_MAP = {
def get_faction_possible_ewrs_generator( def get_faction_possible_ewrs_generator(
faction: Faction, faction: Faction,
) -> List[Type[GroupGenerator]]: ) -> List[Type[EwrGenerator]]:
""" """
Return the list of possible EWR generators for the given faction Return the list of possible EWR generators for the given faction
:param faction: Faction name to search units for :param faction: Faction name to search units for

View File

@ -5,9 +5,16 @@ from gen.sam.group_generator import GroupGenerator
class EwrGenerator(GroupGenerator): class EwrGenerator(GroupGenerator):
@property unit_type: VehicleType
def unit_type(self) -> VehicleType:
raise NotImplementedError @classmethod
def name(cls) -> str:
return cls.unit_type.name
@staticmethod
def price() -> int:
# TODO: Differentiate sites.
return 20
def generate(self) -> None: def generate(self) -> None:
self.add_unit( self.add_unit(

View File

@ -10,14 +10,12 @@ from dcs.condition import (
FlagIsFalse, FlagIsFalse,
FlagIsTrue, FlagIsTrue,
) )
from dcs.unitgroup import FlyingGroup
from dcs.mission import Mission from dcs.mission import Mission
from dcs.task import Option from dcs.task import Option
from dcs.translation import String from dcs.translation import String
from dcs.triggers import ( from dcs.triggers import (
Event, Event,
TriggerOnce, TriggerOnce,
TriggerZone,
TriggerCondition, TriggerCondition,
) )
from dcs.unit import Skill from dcs.unit import Skill
@ -25,7 +23,6 @@ from dcs.unit import Skill
from game.theater import Airfield from game.theater import Airfield
from game.theater.controlpoint import Fob from game.theater.controlpoint import Fob
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@ -115,19 +112,22 @@ class TriggersGenerator:
mark_trigger.add_condition(TimeAfter(1)) mark_trigger.add_condition(TimeAfter(1))
v = 10 v = 10
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
added = [] seen = set()
for ground_object in cp.ground_objects: 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( 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: if cp.captured:
name = ground_object.obj_name + " [ALLY]" name = ground_object.obj_name + " [ALLY]"
else: else:
name = ground_object.obj_name + " [ENEMY]" name = ground_object.obj_name + " [ENEMY]"
mark_trigger.add_action(MarkToAll(v, zone.id, String(name))) mark_trigger.add_action(MarkToAll(v, zone.id, String(name)))
v = v + 1 v += 1
added.append(ground_object.obj_name)
self.mission.triggerrules.triggers.append(mark_trigger) self.mission.triggerrules.triggers.append(mark_trigger)
def _generate_capture_triggers( def _generate_capture_triggers(

View File

@ -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

View File

@ -57,7 +57,7 @@ def inject_custom_payloads(user_path: Path) -> None:
PayloadDirectories.set_preferred(user_path / "MissionEditor" / "UnitPayloads") 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 os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
app = QApplication(sys.argv) app = QApplication(sys.argv)
@ -111,7 +111,7 @@ def run_ui(game: Optional[Game], new_map: bool) -> None:
GameUpdateSignal() GameUpdateSignal()
# Start window # Start window
window = QLiberationWindow(game, new_map) window = QLiberationWindow(game)
window.showMaximized() window.showMaximized()
splash.finish(window) splash.finish(window)
qt_execution_code = app.exec_() 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.", help="Emits a warning for weapons without date or fallback information.",
) )
parser.add_argument( parser.add_argument("--new-map", help="Deprecated. Does nothing.")
"--new-map", parser.add_argument("--old-map", help="Deprecated. Does nothing.")
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."
)
new_game = subparsers.add_parser("new-game") new_game = subparsers.add_parser("new-game")
@ -267,7 +259,7 @@ def main():
args.cheats, args.cheats,
) )
run_ui(game, args.new_map) run_ui(game)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -18,7 +18,7 @@ from game.squadrons import Squadron, Pilot
from game.theater.missiontarget import MissionTarget from game.theater.missiontarget import MissionTarget
from game.transfers import TransferOrder from game.transfers import TransferOrder
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.flights.flight import Flight from gen.flights.flight import Flight, FlightType
from gen.flights.traveltime import TotEstimator from gen.flights.traveltime import TotEstimator
from qt_ui.uiconstants import AIRCRAFT_ICONS from qt_ui.uiconstants import AIRCRAFT_ICONS
@ -424,7 +424,7 @@ class SquadronModel(QAbstractListModel):
self.squadron = squadron self.squadron = squadron
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: 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: def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if not index.isValid(): if not index.isValid():
@ -467,6 +467,15 @@ class SquadronModel(QAbstractListModel):
pilot.send_on_leave() pilot.send_on_leave()
self.endResetModel() 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: class GameModel:
"""A model for the Game object. """A model for the Game object.

View File

@ -1,7 +1,7 @@
import os import os
from typing import Dict 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 game.theater.theatergroundobject import NAME_BY_CATEGORY
from .liberation_theme import get_theme_icons from .liberation_theme import get_theme_icons
@ -16,51 +16,6 @@ URLS: Dict[str, str] = {
LABELS_OPTIONS = ["Full", "Abbreviated", "Dot Only", "Off"] LABELS_OPTIONS = ["Full", "Abbreviated", "Dot Only", "Off"]
SKILL_OPTIONS = ["Average", "Good", "High", "Excellent"] 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_BANNERS: Dict[str, QPixmap] = {}
AIRCRAFT_ICONS: Dict[str, QPixmap] = {} AIRCRAFT_ICONS: Dict[str, QPixmap] = {}
VEHICLE_BANNERS: Dict[str, QPixmap] = {} VEHICLE_BANNERS: Dict[str, QPixmap] = {}
@ -138,17 +93,6 @@ def load_icons():
"./resources/ui/misc/" + get_theme_icons() + "/ordnance_icon.png" "./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( ICONS["Generator"] = QPixmap(
"./resources/ui/misc/" + get_theme_icons() + "/generator.png" "./resources/ui/misc/" + get_theme_icons() + "/generator.png"
) )

View File

@ -1,8 +1,6 @@
from PySide2 import QtCore, QtGui, QtWidgets from PySide2 import QtCore, QtGui
from PySide2.QtWidgets import QCalendarWidget from PySide2.QtWidgets import QCalendarWidget
from qt_ui.uiconstants import COLORS
class QLiberationCalendar(QCalendarWidget): class QLiberationCalendar(QCalendarWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
@ -29,7 +27,7 @@ class QLiberationCalendar(QCalendarWidget):
painter.save() painter.save()
painter.fillRect(rect, QtGui.QColor("#D3D3D3")) painter.fillRect(rect, QtGui.QColor("#D3D3D3"))
painter.setPen(QtCore.Qt.NoPen) painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(QtGui.QColor(COLORS["sea_blue"])) painter.setBrush(QtGui.QColor(52, 68, 85))
r = QtCore.QRect( r = QtCore.QRect(
QtCore.QPoint(), min(rect.width(), rect.height()) * QtCore.QSize(1, 1) QtCore.QPoint(), min(rect.width(), rect.height()) * QtCore.QSize(1, 1)
) )

View File

@ -25,8 +25,6 @@ from qt_ui.windows.AirWingDialog import AirWingDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QTopPanel(QFrame): class QTopPanel(QFrame):
@ -74,26 +72,12 @@ class QTopPanel(QFrame):
self.transfers.setProperty("style", "btn-primary") self.transfers.setProperty("style", "btn-primary")
self.transfers.clicked.connect(self.open_transfers) 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.intel_box = QIntelBox(self.game)
self.buttonBox = QGroupBox("Misc") self.buttonBox = QGroupBox("Misc")
self.buttonBoxLayout = QHBoxLayout() self.buttonBoxLayout = QHBoxLayout()
self.buttonBoxLayout.addWidget(self.air_wing) self.buttonBoxLayout.addWidget(self.air_wing)
self.buttonBoxLayout.addWidget(self.transfers) self.buttonBoxLayout.addWidget(self.transfers)
self.buttonBoxLayout.addWidget(self.settings)
self.buttonBoxLayout.addWidget(self.statistics)
self.buttonBox.setLayout(self.buttonBoxLayout) self.buttonBox.setLayout(self.buttonBoxLayout)
self.proceedBox = QGroupBox("Proceed") self.proceedBox = QGroupBox("Proceed")
@ -123,8 +107,6 @@ class QTopPanel(QFrame):
self.air_wing.setEnabled(True) self.air_wing.setEnabled(True)
self.transfers.setEnabled(True) self.transfers.setEnabled(True)
self.settings.setEnabled(True)
self.statistics.setEnabled(True)
self.conditionsWidget.setCurrentTurn(game.turn, game.conditions) self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
self.intel_box.set_game(game) self.intel_box.set_game(game)
@ -146,14 +128,6 @@ class QTopPanel(QFrame):
self.dialog = PendingTransfersDialog(self.game_model) self.dialog = PendingTransfersDialog(self.game_model)
self.dialog.show() 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): def passTurn(self):
with logged_duration("Skipping turn"): with logged_duration("Skipping turn"):
self.game.pass_turn(no_action=True) self.game.pass_turn(no_action=True)

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -2,7 +2,7 @@ from __future__ import annotations
import logging import logging
from datetime import timedelta 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 PySide2.QtCore import Property, QObject, Signal, Slot
from dcs import Point from dcs import Point
@ -20,6 +20,7 @@ from game.theater import (
TheaterGroundObject, TheaterGroundObject,
FrontLine, FrontLine,
LatLon, LatLon,
ControlPointStatus,
) )
from game.threatzones import ThreatZones from game.threatzones import ThreatZones
from game.transfers import MultiGroupTransport, TransportMap from game.transfers import MultiGroupTransport, TransportMap
@ -36,6 +37,8 @@ from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
LeafletLatLon = list[float] LeafletLatLon = list[float]
LeafletPoly = list[LeafletLatLon] LeafletPoly = list[LeafletLatLon]
MAX_SHIP_DISTANCE = nautical_miles(80)
# **EVERY PROPERTY NEEDS A NOTIFY SIGNAL** # **EVERY PROPERTY NEEDS A NOTIFY SIGNAL**
# #
# https://bugreports.qt.io/browse/PYSIDE-1426 # 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] 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): class ControlPointJs(QObject):
nameChanged = Signal() nameChanged = Signal()
blueChanged = Signal() blueChanged = Signal()
@ -67,6 +80,7 @@ class ControlPointJs(QObject):
mobileChanged = Signal() mobileChanged = Signal()
destinationChanged = Signal(list) destinationChanged = Signal(list)
categoryChanged = Signal() categoryChanged = Signal()
statusChanged = Signal()
def __init__( def __init__(
self, self,
@ -92,6 +106,17 @@ class ControlPointJs(QObject):
def category(self) -> str: def category(self) -> str:
return self.control_point.category 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) @Property(list, notify=positionChanged)
def position(self) -> LeafletLatLon: def position(self) -> LeafletLatLon:
ll = self.theater.point_to_ll(self.control_point.position) ll = self.theater.point_to_ll(self.control_point.position)
@ -109,8 +134,6 @@ class ControlPointJs(QObject):
return self.theater.point_to_ll(self.control_point.target_position).as_list() return self.theater.point_to_ll(self.control_point.target_position).as_list()
def destination_in_range(self, destination: Point) -> bool: def destination_in_range(self, destination: Point) -> bool:
from qt_ui.widgets.map.QLiberationMap import MAX_SHIP_DISTANCE
move_distance = meters( move_distance = meters(
destination.distance_to_point(self.control_point.position) destination.distance_to_point(self.control_point.position)
) )
@ -122,8 +145,6 @@ class ControlPointJs(QObject):
@Slot(list, result=str) @Slot(list, result=str)
def setDestination(self, destination: LeafletLatLon) -> str: def setDestination(self, destination: LeafletLatLon) -> str:
from qt_ui.widgets.map.QLiberationMap import MAX_SHIP_DISTANCE
if not self.control_point.moveable: if not self.control_point.moveable:
return f"{self.control_point} is not mobile" return f"{self.control_point} is not mobile"
if not self.control_point.captured: if not self.control_point.captured:
@ -581,23 +602,13 @@ class ThreatZonesJs(QObject):
def radarSams(self) -> list[LeafletPoly]: def radarSams(self) -> list[LeafletPoly]:
return self._radar_sams 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 @classmethod
def from_zones(cls, zones: ThreatZones, theater: ConflictTheater) -> ThreatZonesJs: def from_zones(cls, zones: ThreatZones, theater: ConflictTheater) -> ThreatZonesJs:
return ThreatZonesJs( return ThreatZonesJs(
cls.polys_to_leaflet(zones.all, theater), shapely_to_leaflet_polys(zones.all, theater),
cls.polys_to_leaflet(zones.airbases, theater), shapely_to_leaflet_polys(zones.airbases, theater),
cls.polys_to_leaflet(zones.air_defenses, theater), shapely_to_leaflet_polys(zones.air_defenses, theater),
cls.polys_to_leaflet(zones.radar_sam_threats, theater), shapely_to_leaflet_polys(zones.radar_sam_threats, theater),
) )
@classmethod @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): class MapModel(QObject):
cleared = Signal() cleared = Signal()
@ -669,6 +744,8 @@ class MapModel(QObject):
frontLinesChanged = Signal() frontLinesChanged = Signal()
threatZonesChanged = Signal() threatZonesChanged = Signal()
navmeshesChanged = Signal() navmeshesChanged = Signal()
mapZonesChanged = Signal()
unculledZonesChanged = Signal()
def __init__(self, game_model: GameModel) -> None: def __init__(self, game_model: GameModel) -> None:
super().__init__() super().__init__()
@ -683,6 +760,8 @@ class MapModel(QObject):
ThreatZonesJs.empty(), ThreatZonesJs.empty() ThreatZonesJs.empty(), ThreatZonesJs.empty()
) )
self._navmeshes = NavMeshJs([], []) self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
self._selected_flight_index: Optional[Tuple[int, int]] = None self._selected_flight_index: Optional[Tuple[int, int]] = None
GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load) GameUpdateSignal.get_instance().game_loaded.connect(self.on_game_load)
GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos) GameUpdateSignal.get_instance().flight_paths_changed.connect(self.reset_atos)
@ -704,6 +783,8 @@ class MapModel(QObject):
ThreatZonesJs.empty(), ThreatZonesJs.empty() ThreatZonesJs.empty(), ThreatZonesJs.empty()
) )
self._navmeshes = NavMeshJs([], []) self._navmeshes = NavMeshJs([], [])
self._map_zones = MapZonesJs([], [], [])
self._unculled_zones = []
self.cleared.emit() self.cleared.emit()
def set_package_selection(self, index: int) -> None: def set_package_selection(self, index: int) -> None:
@ -749,6 +830,8 @@ class MapModel(QObject):
self.reset_front_lines() self.reset_front_lines()
self.reset_threat_zones() self.reset_threat_zones()
self.reset_navmeshes() self.reset_navmeshes()
self.reset_map_zones()
self.reset_unculled_zones()
def on_game_load(self, game: Optional[Game]) -> None: def on_game_load(self, game: Optional[Game]) -> None:
if game is not None: if game is not None:
@ -895,6 +978,22 @@ class MapModel(QObject):
def navmeshes(self) -> NavMeshJs: def navmeshes(self) -> NavMeshJs:
return self._navmeshes 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 @property
def game(self) -> Game: def game(self) -> Game:
if self.game_model.game is None: if self.game_model.game is None:

View File

@ -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 ( from PySide2.QtCore import (
QItemSelectionModel, QItemSelectionModel,
@ -8,13 +11,21 @@ from PySide2.QtCore import (
) )
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QCheckBox,
QDialog, QDialog,
QListView, QListView,
QVBoxLayout, QVBoxLayout,
QTabWidget,
QTableWidget,
QTableWidgetItem,
QWidget,
) )
from dcs.unittype import FlyingType
from game import db from game import db
from game.inventory import ControlPointAircraftInventory
from game.squadrons import Squadron from game.squadrons import Squadron
from gen.flights.flight import Flight
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.models import GameModel, AirWingModel, SquadronModel from qt_ui.models import GameModel, AirWingModel, SquadronModel
from qt_ui.windows.SquadronDialog import SquadronDialog from qt_ui.windows.SquadronDialog import SquadronDialog
@ -41,9 +52,10 @@ class SquadronDelegate(TwoColumnRowDelegate):
return self.squadron(index).nickname return self.squadron(index).nickname
elif (row, column) == (1, 1): elif (row, column) == (1, 1):
squadron = self.squadron(index) squadron = self.squadron(index)
alive = squadron.number_of_living_pilots
active = len(squadron.active_pilots) active = len(squadron.active_pilots)
available = len(squadron.available_pilots) available = len(squadron.available_pilots)
return f"{squadron.size} pilots, {active} active, {available} unassigned" return f"{alive} pilots, {active} active, {available} unassigned"
return "" return ""
@ -75,6 +87,138 @@ class SquadronList(QListView):
self.dialog.show() 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): class AirWingDialog(QDialog):
"""Dialog window showing the player's air wing.""" """Dialog window showing the player's air wing."""
@ -89,4 +233,4 @@ class AirWingDialog(QDialog):
layout = QVBoxLayout() layout = QVBoxLayout()
self.setLayout(layout) self.setLayout(layout)
layout.addWidget(SquadronList(self.air_wing_model)) layout.addWidget(AirWingTabs(game_model))

View File

@ -22,12 +22,11 @@ from game import Game, VERSION, persistency
from game.debriefing import Debriefing from game.debriefing import Debriefing
from qt_ui import liberation_install from qt_ui import liberation_install
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.uiconstants import URLS from qt_ui.uiconstants import URLS
from qt_ui.widgets.QTopPanel import QTopPanel from qt_ui.widgets.QTopPanel import QTopPanel
from qt_ui.widgets.ato import QAirTaskingOrderPanel 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.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
from qt_ui.windows.infos.QInfoPanel import QInfoPanel 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 ( from qt_ui.windows.preferences.QLiberationPreferencesWindow import (
QLiberationPreferencesWindow, QLiberationPreferencesWindow,
) )
from qt_ui.windows.settings.QSettingsWindow import QSettingsWindow
from qt_ui.windows.stats.QStatsWindow import QStatsWindow
class QLiberationWindow(QMainWindow): class QLiberationWindow(QMainWindow):
def __init__(self, game: Optional[Game], new_map: bool) -> None: def __init__(self, game: Optional[Game]) -> None:
super(QLiberationWindow, self).__init__() super(QLiberationWindow, self).__init__()
self.game = game self.game = game
@ -46,7 +47,7 @@ class QLiberationWindow(QMainWindow):
Dialog.set_game(self.game_model) Dialog.set_game(self.game_model)
self.ato_panel = QAirTaskingOrderPanel(self.game_model) self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.info_panel = QInfoPanel(self.game) 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.setGeometry(300, 100, 270, 100)
self.setWindowTitle(f"DCS Liberation - v{VERSION}") 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): def initToolbar(self):
self.tool_bar = self.addToolBar("File") self.tool_bar = self.addToolBar("File")
self.tool_bar.addAction(self.newGameAction) self.tool_bar.addAction(self.newGameAction)
@ -158,7 +167,9 @@ class QLiberationWindow(QMainWindow):
self.links_bar.addAction(self.openDiscordAction) self.links_bar.addAction(self.openDiscordAction)
self.links_bar.addAction(self.openGithubAction) 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): def initMenuBar(self):
self.menu = self.menuBar() self.menu = self.menuBar()
@ -174,30 +185,6 @@ class QLiberationWindow(QMainWindow):
file_menu.addSeparator() file_menu.addSeparator()
file_menu.addAction("E&xit", self.close) 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 = self.menu.addMenu("&Help")
help_menu.addAction(self.openDiscordAction) help_menu.addAction(self.openDiscordAction)
help_menu.addAction(self.openGithubAction) help_menu.addAction(self.openGithubAction)
@ -284,11 +271,6 @@ class QLiberationWindow(QMainWindow):
self.game = game self.game = game
GameUpdateSignal.get_instance().game_loaded.emit(self.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]): def setGame(self, game: Optional[Game]):
try: try:
self.game = game self.game = game
@ -337,6 +319,14 @@ class QLiberationWindow(QMainWindow):
self.subwindow = QLiberationPreferencesWindow() self.subwindow = QLiberationPreferencesWindow()
self.subwindow.show() 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): def onDebriefing(self, debrief: Debriefing):
logging.info("On Debriefing") logging.info("On Debriefing")
self.debriefing = QDebriefingWindow(debrief) self.debriefing = QDebriefingWindow(debrief)

View File

@ -133,10 +133,10 @@ class QWaitingForMissionResultWindow(QDialog):
self.setLayout(self.layout) self.setLayout(self.layout)
@staticmethod @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() row = layout.rowCount()
layout.addWidget(QLabel(f"<b>{description}</b>"), row, 0) 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: def updateLayout(self, debriefing: Debriefing) -> None:
updateBox = QGroupBox("Mission status") updateBox = QGroupBox("Mission status")
@ -145,34 +145,36 @@ class QWaitingForMissionResultWindow(QDialog):
self.debriefing = debriefing self.debriefing = debriefing
self.add_update_row( 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( self.add_update_row(
"Front line units destroyed", "Front line units destroyed",
list(debriefing.front_line_losses), len(list(debriefing.front_line_losses)),
update_layout, update_layout,
) )
self.add_update_row( 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( self.add_update_row(
"Shipping cargo destroyed", "Shipping cargo destroyed",
list(debriefing.cargo_ship_losses), len(list(debriefing.cargo_ship_losses)),
update_layout, update_layout,
) )
self.add_update_row( 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( self.add_update_row(
"Ground units lost at objective areas", "Ground units lost at objective areas",
list(debriefing.ground_object_losses), len(list(debriefing.ground_object_losses)),
update_layout, update_layout,
) )
self.add_update_row( 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( 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 # Clear previous content of the window

View File

@ -1,4 +1,5 @@
import logging import logging
from typing import Callable
from PySide2.QtCore import ( from PySide2.QtCore import (
QItemSelectionModel, QItemSelectionModel,
@ -13,9 +14,13 @@ from PySide2.QtWidgets import (
QVBoxLayout, QVBoxLayout,
QPushButton, QPushButton,
QHBoxLayout, QHBoxLayout,
QGridLayout,
QLabel,
QCheckBox,
) )
from game.squadrons import Pilot from game.squadrons import Pilot
from gen.flights.flight import FlightType
from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.delegates import TwoColumnRowDelegate
from qt_ui.models import SquadronModel from qt_ui.models import SquadronModel
@ -61,6 +66,31 @@ class PilotList(QListView):
self.setSelectionBehavior(QAbstractItemView.SelectItems) 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): class SquadronDialog(QDialog):
"""Dialog window showing a squadron.""" """Dialog window showing a squadron."""
@ -75,11 +105,17 @@ class SquadronDialog(QDialog):
layout = QVBoxLayout() layout = QVBoxLayout()
self.setLayout(layout) 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 = PilotList(squadron_model)
self.pilot_list.selectionModel().selectionChanged.connect( self.pilot_list.selectionModel().selectionChanged.connect(
self.on_selection_changed self.on_selection_changed
) )
layout.addWidget(self.pilot_list) columns.addWidget(self.pilot_list)
button_panel = QHBoxLayout() button_panel = QHBoxLayout()
button_panel.addStretch() button_panel.addStretch()

View File

@ -11,7 +11,12 @@ from PySide2.QtWidgets import (
) )
from game import Game, db 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 gen.flights.flight import FlightType
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel from qt_ui.models import GameModel
@ -44,8 +49,8 @@ class QBaseMenu2(QDialog):
self.setWindowFlags(Qt.WindowStaysOnTopHint) self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.setMinimumSize(300, 200) self.setMinimumSize(300, 200)
self.setMinimumWidth(800) self.setMinimumWidth(1024)
self.setMaximumWidth(800) self.setMaximumWidth(1024)
self.setModal(True) self.setModal(True)
self.setWindowTitle(self.cp.name) self.setWindowTitle(self.cp.name)
@ -62,6 +67,7 @@ class QBaseMenu2(QDialog):
title.setAlignment(Qt.AlignLeft | Qt.AlignTop) title.setAlignment(Qt.AlignLeft | Qt.AlignTop)
title.setProperty("style", "base-title") title.setProperty("style", "base-title")
self.intel_summary = QLabel() self.intel_summary = QLabel()
self.intel_summary.setToolTip(self.generate_intel_tooltip())
self.update_intel_summary() self.update_intel_summary()
top_layout.addWidget(title) top_layout.addWidget(title)
top_layout.addWidget(self.intel_summary) top_layout.addWidget(self.intel_summary)
@ -195,16 +201,49 @@ class QBaseMenu2(QDialog):
def update_intel_summary(self) -> None: def update_intel_summary(self) -> None:
aircraft = self.cp.base.total_aircraft aircraft = self.cp.base.total_aircraft
parking = self.cp.total_aircraft_parking 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( self.intel_summary.setText(
"\n".join( "\n".join(
[ [
f"{aircraft}/{parking} aircraft", 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), 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): def closeEvent(self, close_event: QCloseEvent):
GameUpdateSignal.get_instance().updateGame(self.game_model.game) GameUpdateSignal.get_instance().updateGame(self.game_model.game)

View File

@ -4,7 +4,6 @@ from game.theater import ControlPoint, OffMapSpawn, Fob
from qt_ui.models import GameModel from qt_ui.models import GameModel
from qt_ui.windows.basemenu.DepartingConvoysMenu import DepartingConvoysMenu from qt_ui.windows.basemenu.DepartingConvoysMenu import DepartingConvoysMenu
from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand 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.ground_forces.QGroundForcesHQ import QGroundForcesHQ
from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo from qt_ui.windows.basemenu.intel.QIntelInfo import QIntelInfo
@ -14,9 +13,6 @@ class QBaseMenuTabs(QTabWidget):
super(QBaseMenuTabs, self).__init__() super(QBaseMenuTabs, self).__init__()
if not cp.captured: 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.intel = QIntelInfo(cp, game_model.game)
self.addTab(self.intel, "Intel") self.addTab(self.intel, "Intel")
@ -30,17 +26,9 @@ class QBaseMenuTabs(QTabWidget):
if cp.helipads: if cp.helipads:
self.airfield_command = QAirfieldCommand(cp, game_model) self.airfield_command = QAirfieldCommand(cp, game_model)
self.addTab(self.airfield_command, "Heliport") self.addTab(self.airfield_command, "Heliport")
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses")
else: else:
self.airfield_command = QAirfieldCommand(cp, game_model) self.airfield_command = QAirfieldCommand(cp, game_model)
self.addTab(self.airfield_command, "Airfield Command") self.addTab(self.airfield_command, "Airfield Command")
if not isinstance(cp, OffMapSpawn):
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):
self.ground_forces_hq = QGroundForcesHQ(cp, game_model) self.ground_forces_hq = QGroundForcesHQ(cp, game_model)
self.addTab(self.ground_forces_hq, "Ground Forces HQ") 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")

View File

@ -164,16 +164,16 @@ class QHangarStatus(QHBoxLayout):
self.setAlignment(Qt.AlignLeft) self.setAlignment(Qt.AlignLeft)
def update_label(self) -> None: 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 max_amount = self.control_point.total_aircraft_parking
components = [f"{next_turn.present} present"] components = [f"{next_turn.present} present"]
if next_turn.ordered > 0: if next_turn.total_ordered > 0:
components.append(f"{next_turn.ordered} purchased") components.append(f"{next_turn.total_ordered} purchased")
elif next_turn.ordered < 0: elif next_turn.total_ordered < 0:
components.append(f"{-next_turn.ordered} sold") components.append(f"{-next_turn.total_ordered} sold")
transferring = next_turn.transferring transferring = next_turn.total_transferring
if transferring > 0: if transferring > 0:
components.append(f"{transferring} transferring in") components.append(f"{transferring} transferring in")
if transferring < 0: if transferring < 0:

View File

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

View File

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

View File

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

View File

@ -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 import Game
from game.theater import ControlPoint from game.theater import ControlPoint
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import ( from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import (
QGroundForcesStrategySelector, QGroundForcesStrategySelector,
) )
@ -15,10 +18,44 @@ class QGroundForcesStrategy(QGroupBox):
self.init_ui() self.init_ui()
def init_ui(self): 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() layout = QVBoxLayout()
for enemy_cp in self.cp.connected_points: for enemy_cp in self.cp.connected_points:
if not enemy_cp.captured: if not enemy_cp.captured:
layout.addWidget(QLabel(enemy_cp.name)) layout.addWidget(QLabel(enemy_cp.name))
layout.addWidget(QGroundForcesStrategySelector(self.cp, enemy_cp)) 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() layout.addStretch()
self.setLayout(layout) 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)

View File

@ -21,8 +21,15 @@ from game import Game, db
from game.data.building_data import FORTIFICATION_BUILDINGS from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of from game.db import PRICES, PinpointStrike, REWARDS, unit_type_of
from game.theater import ControlPoint, TheaterGroundObject 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.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 gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox from qt_ui.widgets.QBudgetBox import QBudgetBox
@ -32,9 +39,6 @@ from dcs import vehicles
class QGroundObjectMenu(QDialog): class QGroundObjectMenu(QDialog):
changed = QtCore.Signal()
def __init__( def __init__(
self, self,
parent, parent,
@ -70,12 +74,12 @@ class QGroundObjectMenu(QDialog):
self.doLayout() self.doLayout()
if self.ground_object.dcs_identifier == "AA": if isinstance(self.ground_object, BuildingGroundObject):
self.mainLayout.addWidget(self.intelBox)
else:
self.mainLayout.addWidget(self.buildingBox) self.mainLayout.addWidget(self.buildingBox)
if self.cp.captured: if self.cp.captured:
self.mainLayout.addWidget(self.financesBox) self.mainLayout.addWidget(self.financesBox)
else:
self.mainLayout.addWidget(self.intelBox)
self.actionLayout = QHBoxLayout() self.actionLayout = QHBoxLayout()
@ -87,12 +91,12 @@ class QGroundObjectMenu(QDialog):
self.buy_replace.clicked.connect(self.buy_group) self.buy_replace.clicked.connect(self.buy_group)
self.buy_replace.setProperty("style", "btn-success") self.buy_replace.setProperty("style", "btn-success")
if not isinstance(self.ground_object, NavalGroundObject): if self.ground_object.purchasable:
if self.total_value > 0: if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button) self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace) 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.mainLayout.addLayout(self.actionLayout)
self.setLayout(self.mainLayout) self.setLayout(self.mainLayout)
@ -196,23 +200,21 @@ class QGroundObjectMenu(QDialog):
self.actionLayout.setParent(None) self.actionLayout.setParent(None)
self.doLayout() self.doLayout()
if self.ground_object.dcs_identifier == "AA": if isinstance(self.ground_object, BuildingGroundObject):
self.mainLayout.addWidget(self.intelBox)
else:
self.mainLayout.addWidget(self.buildingBox) self.mainLayout.addWidget(self.buildingBox)
else:
self.mainLayout.addWidget(self.intelBox)
self.actionLayout = QHBoxLayout() self.actionLayout = QHBoxLayout()
if self.total_value > 0: if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button) self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace) 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.mainLayout.addLayout(self.actionLayout)
except Exception as e: except Exception as e:
print(e) logging.exception(e)
self.update_total_value() self.update_total_value()
self.changed.emit()
def update_total_value(self): def update_total_value(self):
total_value = 0 total_value = 0
@ -244,7 +246,6 @@ class QGroundObjectMenu(QDialog):
logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type)) logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type))
self.do_refresh_layout() self.do_refresh_layout()
self.changed.emit()
def sell_all(self): def sell_all(self):
self.update_total_value() self.update_total_value()
@ -294,9 +295,6 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.buySamBox = QGroupBox("Buy SAM site :") self.buySamBox = QGroupBox("Buy SAM site :")
self.buyArmorBox = QGroupBox("Buy defensive position :") self.buyArmorBox = QGroupBox("Buy defensive position :")
self.init_ui()
def init_ui(self):
faction = self.game.player_faction faction = self.game.player_faction
# Sams # Sams
@ -317,6 +315,30 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.buySamButton.clicked.connect(self.buySam) 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
armored_units = db.find_unittype( armored_units = db.find_unittype(
@ -354,16 +376,20 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.buyArmorBox.setLayout(self.buyArmorLayout) self.buyArmorBox.setLayout(self.buyArmorLayout)
self.mainLayout = QHBoxLayout() 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) self.mainLayout.addWidget(self.buyArmorBox)
elif isinstance(self.ground_object, EwrGroundObject):
self.mainLayout.addWidget(buy_ewr_box)
self.setLayout(self.mainLayout) self.setLayout(self.mainLayout)
try: try:
self.samComboChanged(0) self.samComboChanged(0)
self.armorComboChanged(0) self.armorComboChanged(0)
self.on_ewr_selection_changed(0)
except: except:
pass pass
@ -376,6 +402,12 @@ class QBuyGroupForGroundObjectDialog(QDialog):
+ "M]" + "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): def armorComboChanged(self, index):
self.buyArmorButton.setText( self.buyArmorButton.setText(
"Buy [$" "Buy [$"
@ -441,6 +473,24 @@ class QBuyGroupForGroundObjectDialog(QDialog):
self.changed.emit() self.changed.emit()
self.close() 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): def error_money(self):
msg = QMessageBox() msg = QMessageBox()
msg.setIcon(QMessageBox.Information) msg.setIcon(QMessageBox.Information)

View File

@ -221,6 +221,19 @@ class QNewPackageDialog(QPackageDialog):
) )
self.ato_model = model 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 = QPushButton("Save")
self.save_button.setProperty("style", "start-button") self.save_button.setProperty("style", "start-button")
self.save_button.clicked.connect(self.accept) self.save_button.clicked.connect(self.accept)

View File

@ -1,3 +1,5 @@
from datetime import timedelta
from PySide2.QtCore import QItemSelectionModel, QSize from PySide2.QtCore import QItemSelectionModel, QSize
from PySide2.QtGui import QStandardItemModel from PySide2.QtGui import QStandardItemModel
from PySide2.QtWidgets import QAbstractItemView, QListView 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.models import GameModel
from qt_ui.windows.mission.QFlightItem import QFlightItem from qt_ui.windows.mission.QFlightItem import QFlightItem
from game.theater.controlpoint import ControlPoint from game.theater.controlpoint import ControlPoint
from gen.flights.traveltime import TotEstimator
class QPlannedFlightsView(QListView): class QPlannedFlightsView(QListView):
@ -25,8 +28,11 @@ class QPlannedFlightsView(QListView):
for flight in package.flights: for flight in package.flights:
if flight.from_cp == self.cp: if flight.from_cp == self.cp:
item = QFlightItem(package.package, flight) item = QFlightItem(package.package, flight)
self.model.appendRow(item)
self.flight_items.append(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) self.set_selected_flight(0)
def set_selected_flight(self, row): def set_selected_flight(self, row):
@ -43,3 +49,7 @@ class QPlannedFlightsView(QListView):
def set_flight_planner(self) -> None: def set_flight_planner(self) -> None:
self.clear_layout() self.clear_layout()
self.setup_content() self.setup_content()
@staticmethod
def mission_start_for_flight(flight_item: QFlightItem) -> timedelta:
return TotEstimator(flight_item.package).mission_start_time(flight_item.flight)

View File

@ -10,6 +10,7 @@ from PySide2.QtWidgets import (
QPushButton, QPushButton,
QVBoxLayout, QVBoxLayout,
QLineEdit, QLineEdit,
QHBoxLayout,
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
@ -17,7 +18,7 @@ from game import Game
from game.squadrons import Squadron from game.squadrons import Squadron
from game.theater import ControlPoint, OffMapSpawn from game.theater import ControlPoint, OffMapSpawn
from gen.ato import Package from gen.ato import Package
from gen.flights.flight import Flight from gen.flights.flight import Flight, FlightRoster
from qt_ui.uiconstants import EVENT_ICONS from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner from qt_ui.widgets.QFlightSizeSpinner import QFlightSizeSpinner
from qt_ui.widgets.QLabeledWidget import QLabeledWidget 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.QFlightTypeComboBox import QFlightTypeComboBox
from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector from qt_ui.widgets.combos.QOriginAirfieldSelector import QOriginAirfieldSelector
from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector from qt_ui.windows.mission.flight.SquadronSelector import SquadronSelector
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import FlightRosterEditor
class QFlightCreator(QDialog): class QFlightCreator(QDialog):
@ -46,7 +48,7 @@ class QFlightCreator(QDialog):
self.task_selector = QFlightTypeComboBox(self.game.theater, package.target) self.task_selector = QFlightTypeComboBox(self.game.theater, package.target)
self.task_selector.setCurrentIndex(0) 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)) layout.addLayout(QLabeledWidget("Task:", self.task_selector))
self.aircraft_selector = QAircraftTypeSelector( self.aircraft_selector = QAircraftTypeSelector(
@ -93,13 +95,20 @@ class QFlightCreator(QDialog):
self.update_max_size(self.departure.available) self.update_max_size(self.departure.available)
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner)) layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
self.client_slots_spinner = QFlightSizeSpinner( squadron = self.squadron_selector.currentData()
min_size=0, max_size=self.flight_size_spinner.value(), default_size=0 if squadron is None:
roster = None
else:
roster = FlightRoster(
squadron, initial_size=self.flight_size_spinner.value()
) )
self.flight_size_spinner.valueChanged.connect( self.roster_editor = FlightRosterEditor(roster)
lambda v: self.client_slots_spinner.setMaximum(v) self.flight_size_spinner.valueChanged.connect(self.resize_roster)
) self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed)
layout.addLayout(QLabeledWidget("Client Slots:", self.client_slots_spinner)) 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 # 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 # 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): def set_custom_name_text(self, text: str):
self.custom_name_text = text 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]: def verify_form(self) -> Optional[str]:
aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData() aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData()
squadron: Optional[Squadron] = self.squadron_selector.currentData() squadron: Optional[Squadron] = self.squadron_selector.currentData()
@ -181,7 +194,7 @@ class QFlightCreator(QDialog):
origin = self.departure.currentData() origin = self.departure.currentData()
arrival = self.arrival.currentData() arrival = self.arrival.currentData()
divert = self.divert.currentData() divert = self.divert.currentData()
size = self.flight_size_spinner.value() roster = self.roster_editor.roster
if arrival is None: if arrival is None:
arrival = origin arrival = origin
@ -190,22 +203,17 @@ class QFlightCreator(QDialog):
self.package, self.package,
self.country, self.country,
squadron, 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, task,
self.start_type.currentText(), self.start_type.currentText(),
origin, origin,
arrival, arrival,
divert, divert,
custom_name=self.custom_name_text, 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 # noinspection PyUnresolvedReferences
self.created.emit(flight) self.created.emit(flight)
@ -234,13 +242,21 @@ class QFlightCreator(QDialog):
self.start_type.setCurrentText(self.restore_start_type) self.start_type.setCurrentText(self.restore_start_type)
self.restore_start_type = None 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.aircraft_selector.update_items(
self.task_selector.currentData(), task, self.game.aircraft_inventory.available_types_for_player
self.game.aircraft_inventory.available_types_for_player,
) )
self.squadron_selector.update_items( self.squadron_selector.update_items(task, self.aircraft_selector.currentData())
self.task_selector.currentData(), 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: def update_max_size(self, available: int) -> None:

View File

@ -28,7 +28,11 @@ class SquadronSelector(QComboBox):
self, task: Optional[FlightType], aircraft: Optional[Type[FlyingType]] self, task: Optional[FlightType], aircraft: Optional[Type[FlyingType]]
) -> None: ) -> None:
current_squadron = self.currentData() current_squadron = self.currentData()
self.blockSignals(True)
try:
self.clear() self.clear()
finally:
self.blockSignals(False)
if task is None: if task is None:
self.addItem("No task selected", None) self.addItem("No task selected", None)
return return

View File

@ -13,6 +13,10 @@ class DcsLoadoutSelector(QComboBox):
for loadout in Loadout.iter_for(flight): for loadout in Loadout.iter_for(flight):
self.addItem(loadout.name, loadout) self.addItem(loadout.name, loadout)
self.model().sort(0) self.model().sort(0)
self.setDisabled(flight.loadout.is_custom)
if flight.loadout.is_custom:
self.setCurrentText(Loadout.default_for(flight).name)
else:
self.setCurrentText(flight.loadout.name) self.setCurrentText(flight.loadout.name)

View File

@ -15,16 +15,16 @@ from PySide2.QtWidgets import (
from game import Game from game import Game
from game.squadrons import Pilot from game.squadrons import Pilot
from gen.flights.flight import Flight from gen.flights.flight import Flight, FlightRoster
from qt_ui.models import PackageModel from qt_ui.models import PackageModel
class PilotSelector(QComboBox): class PilotSelector(QComboBox):
available_pilots_changed = Signal() available_pilots_changed = Signal()
def __init__(self, flight: Flight, idx: int) -> None: def __init__(self, roster: Optional[FlightRoster], idx: int) -> None:
super().__init__() super().__init__()
self.flight = flight self.roster = roster
self.pilot_index = idx self.pilot_index = idx
self.rebuild() self.rebuild()
@ -34,15 +34,15 @@ class PilotSelector(QComboBox):
def _do_rebuild(self) -> None: def _do_rebuild(self) -> None:
self.clear() 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.addItem("No aircraft", None)
self.setDisabled(True) self.setDisabled(True)
return return
self.setEnabled(True) self.setEnabled(True)
self.addItem("Unassigned", None) self.addItem("Unassigned", None)
choices = list(self.flight.squadron.available_pilots) choices = list(self.roster.squadron.available_pilots)
current_pilot = self.flight.pilots[self.pilot_index] current_pilot = self.roster.pilots[self.pilot_index]
if current_pilot is not None: if current_pilot is not None:
choices.append(current_pilot) choices.append(current_pilot)
# Put players first, otherwise alphabetically. # 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. # The roster resize is handled separately, so we have no pilots to remove.
return return
pilot = self.itemData(index) pilot = self.itemData(index)
if pilot == self.flight.pilots[self.pilot_index]: if pilot == self.roster.pilots[self.pilot_index]:
return return
self.flight.set_pilot(self.pilot_index, pilot) self.roster.set_pilot(self.pilot_index, pilot)
self.available_pilots_changed.emit() self.available_pilots_changed.emit()
def replace(self, new_roster: Optional[FlightRoster]) -> None:
self.roster = new_roster
self.rebuild()
class PilotControls(QHBoxLayout): class PilotControls(QHBoxLayout):
def __init__(self, flight: Flight, idx: int) -> None: def __init__(self, roster: Optional[FlightRoster], idx: int) -> None:
super().__init__() super().__init__()
self.flight = flight self.roster = roster
self.pilot_index = idx self.pilot_index = idx
self.selector = PilotSelector(flight, idx) self.selector = PilotSelector(roster, idx)
self.selector.currentIndexChanged.connect(self.on_pilot_changed) self.selector.currentIndexChanged.connect(self.on_pilot_changed)
self.addWidget(self.selector) self.addWidget(self.selector)
@ -95,9 +99,9 @@ class PilotControls(QHBoxLayout):
@property @property
def pilot(self) -> Optional[Pilot]: 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 None
return self.flight.pilots[self.pilot_index] return self.roster.pilots[self.pilot_index]
def on_player_toggled(self, checked: bool) -> None: def on_player_toggled(self, checked: bool) -> None:
pilot = self.pilot pilot = self.pilot
@ -130,12 +134,21 @@ class PilotControls(QHBoxLayout):
finally: finally:
self.player_checkbox.blockSignals(False) 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): class FlightRosterEditor(QVBoxLayout):
MAX_PILOTS = 4 MAX_PILOTS = 4
def __init__(self, flight: Flight) -> None: def __init__(self, roster: Optional[FlightRoster]) -> None:
super().__init__() super().__init__()
self.roster = roster
self.pilot_controls = [] self.pilot_controls = []
for pilot_idx in range(self.MAX_PILOTS): for pilot_idx in range(self.MAX_PILOTS):
@ -146,7 +159,7 @@ class FlightRosterEditor(QVBoxLayout):
return callback return callback
controls = PilotControls(flight, pilot_idx) controls = PilotControls(roster, pilot_idx)
controls.selector.available_pilots_changed.connect( controls.selector.available_pilots_changed.connect(
make_reset_callback(pilot_idx) make_reset_callback(pilot_idx)
) )
@ -167,6 +180,13 @@ class FlightRosterEditor(QVBoxLayout):
for controls in self.pilot_controls[new_size:]: for controls in self.pilot_controls[new_size:]:
controls.disable_and_clear() 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): class QFlightSlotEditor(QGroupBox):
def __init__(self, package_model: PackageModel, flight: Flight, game: Game): 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(str(self.flight.squadron)), 1, 1)
layout.addWidget(QLabel("Assigned pilots:"), 2, 0) 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) layout.addLayout(self.roster_editor, 2, 1)
self.setLayout(layout) self.setLayout(layout)
def _changed_aircraft_count(self): def _changed_aircraft_count(self):
self.game.aircraft_inventory.return_from_flight(self.flight) old_count = self.flight.count
new_count = int(self.aircraft_count_spinner.value()) new_count = int(self.aircraft_count_spinner.value())
self.game.aircraft_inventory.return_from_flight(self.flight)
self.flight.resize(new_count)
try: try:
self.game.aircraft_inventory.claim_for_flight(self.flight) self.game.aircraft_inventory.claim_for_flight(self.flight)
except ValueError: except ValueError:
@ -217,7 +239,6 @@ class QFlightSlotEditor(QGroupBox):
f"{available} {self.flight.unit_type} remaining" f"{available} {self.flight.unit_type} remaining"
) )
self.game.aircraft_inventory.claim_for_flight(self.flight) self.game.aircraft_inventory.claim_for_flight(self.flight)
self.flight.resize(old_count)
return return
self.flight.resize(new_count)
self.roster_editor.resize(new_count) self.roster_editor.resize(new_count)

View File

@ -8,7 +8,7 @@ from typing import Any, Dict, List, Union, Tuple
import packaging.version import packaging.version
from PySide2 import QtGui from PySide2 import QtGui
from PySide2.QtCore import QItemSelectionModel from PySide2.QtCore import QItemSelectionModel, QModelIndex, Qt
from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QAbstractItemView, QListView from PySide2.QtWidgets import QAbstractItemView, QListView
@ -116,6 +116,7 @@ def load_campaigns() -> List[Campaign]:
class QCampaignItem(QStandardItem): class QCampaignItem(QStandardItem):
def __init__(self, campaign: Campaign) -> None: def __init__(self, campaign: Campaign) -> None:
super(QCampaignItem, self).__init__() super(QCampaignItem, self).__init__()
self.setData(campaign, QCampaignList.CampaignRole)
self.setIcon(QtGui.QIcon(CONST.ICONS[campaign.icon_name])) self.setIcon(QtGui.QIcon(CONST.ICONS[campaign.icon_name]))
self.setEditable(False) self.setEditable(False)
if campaign.is_compatible: if campaign.is_compatible:
@ -126,31 +127,33 @@ class QCampaignItem(QStandardItem):
class QCampaignList(QListView): 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__() super(QCampaignList, self).__init__()
self.model = QStandardItemModel(self) self.campaign_model = QStandardItemModel(self)
self.setModel(self.model) self.setModel(self.campaign_model)
self.setMinimumWidth(250) self.setMinimumWidth(250)
self.setMinimumHeight(350) self.setMinimumHeight(350)
self.campaigns = [] self.campaigns = campaigns
self.setSelectionBehavior(QAbstractItemView.SelectItems) self.setSelectionBehavior(QAbstractItemView.SelectItems)
self.setup_content(campaigns) self.setup_content(show_incompatible)
def setup_content(self, campaigns: List[Campaign]) -> None: @property
for campaign in campaigns: def selected_campaign(self) -> Campaign:
self.campaigns.append(campaign) return self.currentIndex().data(QCampaignList.CampaignRole)
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) item = QCampaignItem(campaign)
self.model.appendRow(item) self.campaign_model.appendRow(item)
self.setSelectedCampaign(0) finally:
self.repaint() self.selectionModel().blockSignals(False)
def setSelectedCampaign(self, row): self.selectionModel().setCurrentIndex(
self.selectionModel().clearSelection() self.campaign_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select
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 clear_layout(self):
self.model.removeRows(0, self.model.rowCount())

View File

@ -6,7 +6,7 @@ from typing import List
from PySide2 import QtGui, QtWidgets from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QItemSelectionModel, QPoint, Qt, QDate 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 jinja2 import Environment, FileSystemLoader, select_autoescape
from game import db from game import db
@ -319,7 +319,16 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
) )
# List of campaigns # 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) self.registerField("selectedCampaign", campaignList)
# Faction description # Faction description
@ -380,8 +389,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
template_perf = jinja_env.get_template( template_perf = jinja_env.get_template(
"campaign_performance_template_EN.j2" "campaign_performance_template_EN.j2"
) )
index = campaignList.selectionModel().currentIndex().row() campaign = campaignList.selected_campaign
campaign = campaignList.campaigns[index]
self.setField("selectedCampaign", campaign) self.setField("selectedCampaign", campaign)
self.campaignMapDescription.setText(template.render({"campaign": campaign})) self.campaignMapDescription.setText(template.render({"campaign": campaign}))
self.faction_selection.setDefaultFactions(campaign) self.faction_selection.setDefaultFactions(campaign)
@ -396,9 +404,12 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
campaignList.selectionModel().selectionChanged.connect(on_campaign_selected) campaignList.selectionModel().selectionChanged.connect(on_campaign_selected)
on_campaign_selected() on_campaign_selected()
# Docs Link
docsText = QtWidgets.QLabel( 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.setAlignment(Qt.AlignCenter)
docsText.setOpenExternalLinks(True) docsText.setOpenExternalLinks(True)
@ -418,7 +429,8 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
layout = QtWidgets.QGridLayout() layout = QtWidgets.QGridLayout()
layout.setColumnMinimumWidth(0, 20) layout.setColumnMinimumWidth(0, 20)
layout.addWidget(campaignList, 0, 0, 5, 1) 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.campaignMapDescription, 0, 1, 1, 1)
layout.addWidget(self.performanceText, 1, 1, 1, 1) layout.addWidget(self.performanceText, 1, 1, 1, 1)
layout.addWidget(mapSettingsGroup, 2, 1, 1, 1) layout.addWidget(mapSettingsGroup, 2, 1, 1, 1)

View File

@ -473,20 +473,22 @@ class QSettingsWindow(QDialog):
general_layout.addWidget(restrict_weapons, 0, 1, Qt.AlignRight) general_layout.addWidget(restrict_weapons, 0, 1, Qt.AlignRight)
def set_old_awec(value: bool) -> None: 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 = 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_awac.toggled.connect(set_old_awec)
old_awec_info = ( old_awec_info = (
"If checked, the invulnerable friendly AEW&C aircraft that begins " "If checked, an invulnerable friendly AEW&C aircraft that begins the "
"the mission in the air will not be spawned. AEW&C missions must " "mission on station will be be spawned. This behavior will be removed in a "
"be planned in the ATO and will take time to arrive on-station." "future release."
) )
old_awac.setToolTip(old_awec_info) 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) old_awac_label.setToolTip(old_awec_info)
general_layout.addWidget(old_awac_label, 1, 0) general_layout.addWidget(old_awac_label, 1, 0)

View 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
}

Binary file not shown.

View 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
}

View File

@ -7,5 +7,5 @@
"description": "<p>You have managed to establish a foothold at Khasab. Continue pushing south.</p>", "description": "<p>You have managed to establish a foothold at Khasab. Continue pushing south.</p>",
"miz": "battle_of_abu_dhabi.miz", "miz": "battle_of_abu_dhabi.miz",
"performance": 2, "performance": 2,
"version": "5.0" "version": "6.0"
} }

View File

@ -4,5 +4,6 @@
"authors": "Colonel Panic", "authors": "Colonel Panic",
"description": "<p>A medium sized theater with bases along the coast of the Black Sea.</p>", "description": "<p>A medium sized theater with bases along the coast of the Black Sea.</p>",
"miz": "black_sea.miz", "miz": "black_sea.miz",
"performance": 2 "performance": 2,
"version": "6.0"
} }

Binary file not shown.

View File

@ -2,8 +2,10 @@
"name": "Nevada - Exercise Vegas Nerve", "name": "Nevada - Exercise Vegas Nerve",
"theater": "Nevada", "theater": "Nevada",
"authors": "Starfire", "authors": "Starfire",
"description": "<p>A Red Flag Exercise scenario for the NTTR comprising 4 control points.</p>", "recommended_player_faction": "Bluefor Modern",
"version": 3, "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", "miz": "exercise_vegas_nerve.miz",
"performance": 0 "performance": 0
} }

View File

@ -1,5 +1,5 @@
{ {
"name": "Syria - Battle for Golan Heights - Lite", "name": "Syria - Battle for Golan Heights",
"theater": "Syria", "theater": "Syria",
"authors": "Khopa", "authors": "Khopa",
"recommended_player_faction": "Israel 2000", "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>", "description": "<p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance friendly.</p>",
"miz": "golan_heights_lite.miz", "miz": "golan_heights_lite.miz",
"performance": 1, "performance": 1,
"version": "5.0" "version": "6.0"
} }

View File

@ -5,7 +5,7 @@
"recommended_player_faction": "USA 2005", "recommended_player_faction": "USA 2005",
"recommended_enemy_faction": "Insurgents (Hard)", "recommended_enemy_faction": "Insurgents (Hard)",
"description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>", "description": "<p>In this scenario, you start from Jordan, and have to fight your way through eastern Syria.</p>",
"version": "5.0", "version": "6.0",
"miz": "inherent_resolve.miz", "miz": "inherent_resolve.miz",
"performance": 2 "performance": 2
} }

View File

@ -5,7 +5,7 @@
"recommended_player_faction": "Bluefor Modern", "recommended_player_faction": "Bluefor Modern",
"recommended_enemy_faction": "Turkey 2005", "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>", "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", "miz": "operation_peace_spring.miz",
"performance": 1 "performance": 1
} }

View 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
}

Binary file not shown.

View 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>", "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", "miz": "russia_small.miz",
"performance": 0, "performance": 0,
"version": 3 "version": "6.0"
} }

Binary file not shown.

View 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"
}

Binary file not shown.

View File

@ -1,8 +0,0 @@
{
"name": "Syria - Full Map",
"theater": "Syria",
"authors": "Hawkmoon",
"description": "<p>Full map of Syria</p><p><strong>Note :&nbsp;</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
}

View 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"
}

View File

@ -498,7 +498,6 @@ QHeaderView::section {
background: #4B5B74; background: #4B5B74;
padding: 4px; padding: 4px;
border-style: none; border-style: none;
border-bottom: 1px solid #1D2731;
} }
QHeaderView::section:horizontal QHeaderView::section:horizontal
@ -515,11 +514,6 @@ QHeaderView::section:vertical
background: #4B5B74; background: #4B5B74;
} }
QTableWidget {
gridline-color: red;
background: #4B5B74;
}
QTableView QTableCornerButton::section { QTableView QTableCornerButton::section {
background: #4B5B74; background: #4B5B74;
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More