mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
171 Commits
8.0.0
...
develop-4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40b147148b | ||
|
|
2f56bae3e5 | ||
|
|
6febf546a8 | ||
|
|
7754e5fd4d | ||
|
|
81058d9e25 | ||
|
|
a6c544a6e6 | ||
|
|
46755240d2 | ||
|
|
2141ee8a0a | ||
|
|
a71a053d05 | ||
|
|
6ddea481e4 | ||
|
|
ab9fd69493 | ||
|
|
a54f0e792d | ||
|
|
f0476fcbb3 | ||
|
|
263c1cc012 | ||
|
|
c06550ca64 | ||
|
|
03471c6c13 | ||
|
|
d6c1456108 | ||
|
|
11389f50c8 | ||
|
|
70ed11bf69 | ||
|
|
26cfa4e30a | ||
|
|
8282a95569 | ||
|
|
283fc26385 | ||
|
|
5729cc5f42 | ||
|
|
3fadaa9a5a | ||
|
|
3de4feec7e | ||
|
|
570108e2a7 | ||
|
|
adc7a41941 | ||
|
|
fcd8d6c76b | ||
|
|
3dc71a558d | ||
|
|
ab1c682f9b | ||
|
|
4e489fe75d | ||
|
|
5d8e0b3b1e | ||
|
|
28fbd448b0 | ||
|
|
93d85830a9 | ||
|
|
7de399090b | ||
|
|
8a2a21bf11 | ||
|
|
62716f7b86 | ||
|
|
5febba9640 | ||
|
|
7dea51321f | ||
|
|
836c184241 | ||
|
|
50ec5b2832 | ||
|
|
08919d4b8e | ||
|
|
c1bdf55ff3 | ||
|
|
eee62fe84a | ||
|
|
8a23792ae1 | ||
|
|
0332c32bb3 | ||
|
|
9ed165b8bd | ||
|
|
ad8e70c250 | ||
|
|
e7e1e1cad4 | ||
|
|
c386ce8ea0 | ||
|
|
a0574183d9 | ||
|
|
2749c050e7 | ||
|
|
9267be5798 | ||
|
|
82a4d9194d | ||
|
|
339fa3d835 | ||
|
|
c0a9eb3473 | ||
|
|
04b53fa23d | ||
|
|
a22f1d8e63 | ||
|
|
5860518f92 | ||
|
|
68473ae63a | ||
|
|
1ea13954ec | ||
|
|
53a1c938c3 | ||
|
|
229a2cd7a4 | ||
|
|
19980e5d6b | ||
|
|
85be9df481 | ||
|
|
9bc8b51794 | ||
|
|
e9bc3f3e69 | ||
|
|
3f5fdc580a | ||
|
|
f4e02954b7 | ||
|
|
e88dfc53c2 | ||
|
|
2926431dc7 | ||
|
|
70a0341675 | ||
|
|
251c84019f | ||
|
|
8fae7decca | ||
|
|
3415525e2c | ||
|
|
355ea9f9be | ||
|
|
eff674c441 | ||
|
|
9858e3e257 | ||
|
|
b206bcae56 | ||
|
|
3dbfa8ca60 | ||
|
|
779dc8ad70 | ||
|
|
467de580c5 | ||
|
|
adbba788c6 | ||
|
|
99f359b46b | ||
|
|
277df247b9 | ||
|
|
e0a4ceef67 | ||
|
|
234a998abe | ||
|
|
2233141033 | ||
|
|
c47750b2d9 | ||
|
|
3f8dfce9e0 | ||
|
|
20e7690a85 | ||
|
|
7f1e21b587 | ||
|
|
bc3a75836d | ||
|
|
f588c445ae | ||
|
|
7807e2fc31 | ||
|
|
91bde9dccf | ||
|
|
de9d388b96 | ||
|
|
c7d3f1a340 | ||
|
|
d3b44e5ba1 | ||
|
|
ac0e29a54d | ||
|
|
91d08e2160 | ||
|
|
d18d6b2422 | ||
|
|
6cc967742a | ||
|
|
17f2bcc9c9 | ||
|
|
9d499a1430 | ||
|
|
3b55dfad40 | ||
|
|
9d3c7a86b6 | ||
|
|
7f68846023 | ||
|
|
c1534cba9e | ||
|
|
eea31168c1 | ||
|
|
f2de1fdac6 | ||
|
|
8dd29d2319 | ||
|
|
b402dad801 | ||
|
|
7199fead00 | ||
|
|
4ff0f29fe0 | ||
|
|
1b8992eb04 | ||
|
|
ccbcf4f69a | ||
|
|
0747007f58 | ||
|
|
723588666f | ||
|
|
94861ca477 | ||
|
|
e8992c5bed | ||
|
|
e841358f74 | ||
|
|
3c135720a0 | ||
|
|
d7db290892 | ||
|
|
b7626c10da | ||
|
|
d79e8f46f3 | ||
|
|
278b9730cd | ||
|
|
d187c571ea | ||
|
|
b3705531d4 | ||
|
|
666b389821 | ||
|
|
ddc076b141 | ||
|
|
eee1791a79 | ||
|
|
fb5a6d3243 | ||
|
|
113c00ac05 | ||
|
|
85ca85ac6d | ||
|
|
da917a7dde | ||
|
|
b03d1599e1 | ||
|
|
2b3c56ad38 | ||
|
|
8dc35bec5a | ||
|
|
3f4f27612b | ||
|
|
17f9487fe0 | ||
|
|
e15b10ae7e | ||
|
|
17d56beeaa | ||
|
|
53c7912592 | ||
|
|
1f318aff3c | ||
|
|
2bb1c0b3f2 | ||
|
|
b057f027d5 | ||
|
|
cc079ad44e | ||
|
|
974c0069e6 | ||
|
|
9028109fe3 | ||
|
|
db27f3b0d9 | ||
|
|
cb542b6af4 | ||
|
|
fcea37c340 | ||
|
|
cf3d13f9d3 | ||
|
|
6789beb4b5 | ||
|
|
8f1ec4a519 | ||
|
|
b8bc9d87ec | ||
|
|
52aff8bc30 | ||
|
|
5c81ac06ac | ||
|
|
8364148305 | ||
|
|
2bcff5a5c2 | ||
|
|
c227923bdf | ||
|
|
4569b1b45a | ||
|
|
3a193d1dd4 | ||
|
|
9334cba564 | ||
|
|
4dc1daa100 | ||
|
|
0d99fc3d36 | ||
|
|
eee78288c9 | ||
|
|
c2f112e3a6 | ||
|
|
ef3f7125b3 | ||
|
|
4558088412 |
87
changelog.md
87
changelog.md
@@ -1,30 +1,113 @@
|
||||
# 4.1.2
|
||||
|
||||
Saves from 4.1.1 are compatible with 4.1.2.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Mission Generation]** EWRs are now also headed towards the center of the conflict
|
||||
* **[UI]** Sell Button for aircraft will be disabled if there are no units available to be sold or all are already assigned to a mission
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[UI]** Selling of Units is now visible again in the UI dialog and shows the correct amount of sold units
|
||||
* **[Mission Generation]** Mission results and other files will now be opened with enforced utf-8 encoding to prevent an issue where destroyed ground units were untracked because of special characters in their names.
|
||||
|
||||
# 4.1.1
|
||||
|
||||
Saves from 4.1.0 are compatible with 4.1.1.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed broken support for Mariana Islands map.
|
||||
* **[Mission Generation]** Fix SAM sites pointing towards the center of the conflict.
|
||||
* **[Flight Planning]** No longer using Su-34 for CAP missions.
|
||||
|
||||
# 4.1.0
|
||||
|
||||
Saves from 4.0.0 are compatible with 4.1.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Campaign]** Air defense sites now generate a fixed number of launchers per type.
|
||||
* **[Campaign]** Added support for Mariana Islands map.
|
||||
* **[Campaign AI]** Adjustments to aircraft selection priorities for most mission types.
|
||||
* **[Engine]** Support for DCS 2.7.4.9632 and newer, including the Marianas map, F-16 JSOWs, NASAMS, and Tin Shield EWR.
|
||||
* **[Flight Planning]** CAP patrol altitudes are now set per-aircraft. By default the altitude will be set based on the aircraft's maximum speed.
|
||||
* **[Flight Planning]** CAP patrol speeds are now set per-aircraft to be more suitable/sensible. By default the speed will be set based on the aircraft's maximum speed.
|
||||
* **[Mission Generation]** Improvements for better support of the Skynet Plugin and long range SAMs are now acting as EWR
|
||||
* **[Mods]** Support for version v1.5.0-Beta of Gripen mod. In-progress campaigns may need to re-plan Gripen flights to pick up updated loadouts.
|
||||
* **[Mission Generation]** SAM sites are now headed towards the center of the conflict
|
||||
* **[Plugins]** Increased time JTAC Autolase messages stay visible on the UI.
|
||||
* **[Plugins]** Updated SkynetIADS to 2.2.0 (adds NASAMS support).
|
||||
* **[UI]** Added ability to take notes and have those notes appear as a kneeboard page.
|
||||
* **[UI]** Hovering over the weather information now dispalys the cloud base (meters and feet).
|
||||
* **[UI]** Google search link added to unit information when there is no information provided.
|
||||
* **[UI]** Control point name displayed with ground object group name on map.
|
||||
* **[UI]** Buy or Replace will now show the correct price for generated ground objects like sams.
|
||||
* **[UI]** Improved logging for frontline movement to be more descriptive about what happened and why.
|
||||
* **[UI]** Brought ruler map module into source, which should fix file integrity issues with the module.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed the Silkworm generator to include launchers and not all radars.
|
||||
* **[Data]** Fixed Introduction dates for targeting pods (ATFLIR and LITENING were both a few years too early).
|
||||
* **[Data]** Removed SA-10 from Syria 2011 faction.
|
||||
* **[Economy]** EWRs can now be bought and sold for the correct price and can no longer be used to generate money
|
||||
* **[Flight Planning]** Helicopters are now correctly identified, and will fly ingress/CAS/BAI/egress and similar at low altitude.
|
||||
* **[Flight Planning]** Fixed potential issue with angles > 360° or < 0° being generated when summing two angles.
|
||||
* **[Mission Generation]** The lua data for other plugins is now generated correctly
|
||||
* **[Mission Generation]** Fixed problem with opfor planning missions against sold ground objects like SAMs
|
||||
* **[Mission Generation]** The legacy always-available tanker option no longer prevents mission creation.
|
||||
* **[Mission Generation]** Prevent the creation of a transfer order with 0 units for a rare situtation when a point was captured.
|
||||
* **[Mission Generation]** Planned transfers which will be impossible after a base capture will no longer prevent the mission result submit.
|
||||
* **[Mission Generation]** Fix occasional KeyError preventing mission generation when all units of the same type in a convoy were killed.
|
||||
* **[Mission Generation]** Fix for AAA Flak generator using Opel Blitz preventing the mission from being generated because duplicate unit names were used.
|
||||
* **[Campaign AI]** Transport aircraft will now be bought only if necessary at control points which can produce ground units and are capable to operate transport aircraft.
|
||||
* **[UI]** Statistics window tick marks are now always integers.
|
||||
* **[UI]** Statistics window now shows the correct info for the turn
|
||||
* **[UI]** Toggling custom loadout for an aircraft with no preset loadouts no longer breaks the flight.
|
||||
|
||||
# 4.0.0
|
||||
|
||||
Saves from 3.x are not compatible with 4.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Campaign]** Squadrons now have a maximum size and killed pilots replenish at a limited rate.
|
||||
* **[Engine]** Support for DCS 2.7.2.7910.1 and newer, including Cyprus, F-16 JDAMs, and the Hind.
|
||||
* **[Campaign]** Squadrons now (optionally, off by default) have a maximum size and killed pilots replenish at a limited rate.
|
||||
* **[Campaign]** Added an option to disable levelling up of AI pilots.
|
||||
* **[Campaign]** Added Russian Intervention 2015 campaign on Syria, for a small and somewhat realistic Russian COIN scenario.
|
||||
* **[Campaign]** Added Operation Atilla campaign on Syria, for a reasonably large invasion of Cyprus scenario.
|
||||
* **[Campaign AI]** AI will plan Tanker flights.
|
||||
* **[Campaign AI]** Removed max distance for AEW&C auto planning.
|
||||
* **[Economy]** Adjusted prices for aircraft to balance out some price inconsistencies.
|
||||
* **[Factions]** Added more tankers to factions.
|
||||
* **[Flight Planner]** Added ability to plan Tankers.
|
||||
* **[Modding]** Campaign format version is now 7.0 to account for DCS map changes that made scenery strike targets incompatible with existing campaigns.
|
||||
* **[Mods]** Added support for the Gripen mod.
|
||||
* **[Mods]** Removes MB-339PAN support, as the mod is now deprecated and no longer works with DCS 2.7+.
|
||||
* **[Mission Generation]** Added support for "Neutral Dot" label options.
|
||||
* **[New Game Wizard]** Mods are now selected via checkboxes in the new game wizard, not as separate factions.
|
||||
* **[UI]** Ctrl click and shift click now buy or sell 5 or 10 units respectively.
|
||||
* **[UI]** Multiple waypoints can now be deleted simultaneously if multiple waypoints are selected.
|
||||
* **[UI]** Carriers and LHAs now match the colour of airfields, and their destination icons are translucent.
|
||||
* **[UI]** Updated intel box text for first turn.
|
||||
* **[UI]** Base Capture Cheat is now usable at all bases and can also be used to transfer player-owned bases to OPFOR.
|
||||
* **[UI]** Pass Turn button is relabled as "Begin Campaign" on Turn 0.
|
||||
* **[UI]** Added a ruler to the map.
|
||||
* **[UI]** Liberation now saves games to `<DCS user directory>/Liberation/Saves` by default to declutter the main directory.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
|
||||
* **[Campaign AI]** Improved pruning of unplannable missions which should improve turn cycle time and prevent the auto-planner from quitting early.
|
||||
* **[Campaign AI]** Fix auto purchase of aircraft for factions that have no transport aircraft.
|
||||
* **[Campaign AI]** Fix refunding of pending aircraft purchases when a side has no factory available.
|
||||
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
|
||||
* **[Mission Generation]** Fixed EWR group names so they contribute to Skynet again.
|
||||
* **[Mission Generation]** Fixed duplicate name error when generating convoys and cargo ships when creating manual transfers after loading a game.
|
||||
* **[Mission Generation]** Fixed empty convoys not being disbanded when all units are killed/removed.
|
||||
* **[Mission Generation]** Fixed player losing frontline progress when skipping from turn 0 to turn 1.
|
||||
* **[Mission Generation]** Fixed issue where frontline would only search to the right for valid locations.
|
||||
* **[UI]** Made non-interactive map elements less obstructive.
|
||||
* **[UI]** Added support for Neutral Dot difficulty label
|
||||
* **[UI]** Clear skies at night no longer described as "Sunny" by the weather widget.
|
||||
|
||||
@@ -25,6 +25,7 @@ class AlicCodes:
|
||||
AirDefence.SNR_75V.id: 126,
|
||||
AirDefence.HQ_7_LN_SP.id: 127,
|
||||
AirDefence.HQ_7_STR_SP.id: 128,
|
||||
AirDefence.RLS_19J6.id: 130,
|
||||
AirDefence.Roland_ADS.id: 201,
|
||||
AirDefence.Patriot_str.id: 202,
|
||||
AirDefence.Hawk_sr.id: 203,
|
||||
@@ -33,6 +34,7 @@ class AlicCodes:
|
||||
AirDefence.Hawk_cwar.id: 206,
|
||||
AirDefence.Gepard.id: 207,
|
||||
AirDefence.Vulcan.id: 208,
|
||||
AirDefence.NASAMS_Radar_MPQ64F1.id: 209,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
from dcs.planes import (
|
||||
Bf_109K_4,
|
||||
C_101CC,
|
||||
FW_190A8,
|
||||
FW_190D9,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
I_16,
|
||||
L_39ZA,
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
P_51D,
|
||||
P_51D_30_NA,
|
||||
SpitfireLFMkIX,
|
||||
SpitfireLFMkIXCW,
|
||||
)
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
|
||||
"""
|
||||
This list contains the aircraft that do not use the guns as the last resort weapons, but as a main weapon
|
||||
They'll RTB when they don't have gun ammo left
|
||||
"""
|
||||
GUNFIGHTERS = [
|
||||
# Cold War
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
F_86F_Sabre,
|
||||
A_4E_C,
|
||||
F_5E_3,
|
||||
# Trainers
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
# WW2
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
P_47D_30bl1,
|
||||
P_47D_40,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
I_16,
|
||||
]
|
||||
@@ -5,14 +5,14 @@ import inspect
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Iterator, Optional, Set, Tuple, Union, cast
|
||||
from typing import Dict, Iterator, Optional, Set, Tuple, cast, Any
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.weapons_data import Weapons, weapon_ids
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
PydcsWeapon = Dict[str, Union[int, str]]
|
||||
PydcsWeapon = Any
|
||||
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class Pylon:
|
||||
# configuration.
|
||||
return weapon in self.allowed or weapon.cls_id == "<CLEAN>"
|
||||
|
||||
def equip(self, group: FlyingGroup, weapon: Weapon) -> None:
|
||||
def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None:
|
||||
if not self.can_equip(weapon):
|
||||
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
|
||||
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
|
||||
@@ -888,16 +888,16 @@ WEAPON_INTRODUCTION_YEARS = {
|
||||
Weapon.from_pydcs(Weapons.ALQ_184): 1989,
|
||||
Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984,
|
||||
# TGP Pods
|
||||
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod): 1995,
|
||||
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod_): 1995,
|
||||
Weapon.from_pydcs(Weapons.AN_ASQ_228_ATFLIR___Targeting_Pod): 1993,
|
||||
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod): 1999,
|
||||
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod_): 1999,
|
||||
Weapon.from_pydcs(Weapons.AN_ASQ_228_ATFLIR___Targeting_Pod): 2003,
|
||||
Weapon.from_pydcs(
|
||||
Weapons.AN_ASQ_173_Laser_Spot_Tracker_Strike_CAMera__LST_SCAM_
|
||||
): 1993,
|
||||
Weapon.from_pydcs(Weapons.AWW_13_DATALINK_POD): 1967,
|
||||
Weapon.from_pydcs(Weapons.LANTIRN_Targeting_Pod): 1985,
|
||||
Weapon.from_pydcs(Weapons.Lantirn_F_16): 1985,
|
||||
Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1985,
|
||||
Weapon.from_pydcs(Weapons.LANTIRN_Targeting_Pod): 1990,
|
||||
Weapon.from_pydcs(Weapons.Lantirn_F_16): 1990,
|
||||
Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1990,
|
||||
Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982,
|
||||
# BLU-107
|
||||
Weapon.from_pydcs(Weapons.BLU_107___440lb_Anti_Runway_Penetrator_Bomb): 1983,
|
||||
|
||||
29
game/db.py
29
game/db.py
@@ -29,8 +29,9 @@ from dcs.ships import (
|
||||
CV_1143_5,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport
|
||||
from dcs.unit import Ship
|
||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.unittype import UnitType, FlyingType, ShipType, VehicleType
|
||||
from dcs.vehicles import (
|
||||
vehicle_map,
|
||||
)
|
||||
@@ -44,12 +45,10 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
plane_map["A-4E-C"] = A_4E_C
|
||||
plane_map["F-22A"] = F_22A
|
||||
plane_map["MB-339PAN"] = MB_339PAN
|
||||
plane_map["Su-57"] = Su_57
|
||||
plane_map["Hercules"] = Hercules
|
||||
plane_map["JAS39Gripen"] = JAS39Gripen
|
||||
@@ -89,6 +88,14 @@ vehicle_map["Toyota_bleu"] = frenchpack.DIM__TOYOTA_BLUE
|
||||
vehicle_map["Toyota_vert"] = frenchpack.DIM__TOYOTA_GREEN
|
||||
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
|
||||
vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
|
||||
vehicle_map["AMX1375"] = frenchpack.AMX_13_75mm
|
||||
vehicle_map["AMX1390"] = frenchpack.AMX_13_90mm
|
||||
vehicle_map["VBCI"] = frenchpack.VBCI
|
||||
vehicle_map["T62"] = frenchpack.Char_T_62
|
||||
vehicle_map["T64BV"] = frenchpack.Char_T_64BV
|
||||
vehicle_map["T72M"] = frenchpack.Char_T_72A
|
||||
vehicle_map["KORNET"] = frenchpack.KORNET_ATGM
|
||||
|
||||
|
||||
vehicle_map[highdigitsams.AAA_SON_9_Fire_Can.id] = highdigitsams.AAA_SON_9_Fire_Can
|
||||
vehicle_map[highdigitsams.AAA_100mm_KS_19.id] = highdigitsams.AAA_100mm_KS_19
|
||||
@@ -249,7 +256,7 @@ Aircraft livery overrides. Syntax as follows:
|
||||
`Identifier` is aircraft identifier (as used troughout the file) and "LiveryName" (with double quotes)
|
||||
is livery name as found in mission editor.
|
||||
"""
|
||||
PLANE_LIVERY_OVERRIDES = {
|
||||
PLANE_LIVERY_OVERRIDES: dict[Type[FlyingType], str] = {
|
||||
FA_18C_hornet: "VFA-34", # default livery for the hornet is blue angels one
|
||||
}
|
||||
|
||||
@@ -320,7 +327,7 @@ REWARDS = {
|
||||
StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
|
||||
|
||||
|
||||
def upgrade_to_supercarrier(unit, name: str):
|
||||
def upgrade_to_supercarrier(unit: Type[ShipType], name: str) -> Type[ShipType]:
|
||||
if unit == Stennis:
|
||||
if name == "CVN-71 Theodore Roosevelt":
|
||||
return CVN_71
|
||||
@@ -353,7 +360,15 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
||||
return None
|
||||
|
||||
|
||||
def country_id_from_name(name):
|
||||
def vehicle_type_from_name(name: str) -> Type[VehicleType]:
|
||||
return vehicle_map[name]
|
||||
|
||||
|
||||
def ship_type_from_name(name: str) -> Type[ShipType]:
|
||||
return ship_map[name]
|
||||
|
||||
|
||||
def country_id_from_name(name: str) -> int:
|
||||
for k, v in country_dict.items():
|
||||
if v.name == name:
|
||||
return k
|
||||
@@ -366,7 +381,7 @@ class DefaultLiveries:
|
||||
|
||||
|
||||
OH_58D.Liveries = DefaultLiveries
|
||||
F_16C_50.Liveries = DefaultLiveries
|
||||
F_16C_50.Liveries = DefaultLiveries # type: ignore
|
||||
P_51D_30_NA.Liveries = DefaultLiveries
|
||||
Ju_88A4.Liveries = DefaultLiveries
|
||||
B_17G.Liveries = DefaultLiveries
|
||||
|
||||
@@ -29,7 +29,14 @@ from game.radio.channels import (
|
||||
ViggenRadioChannelAllocator,
|
||||
NoOpChannelAllocator,
|
||||
)
|
||||
from game.utils import Speed, kph
|
||||
from game.utils import (
|
||||
Distance,
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||
Speed,
|
||||
feet,
|
||||
kph,
|
||||
knots,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.aircraft import FlightData
|
||||
@@ -91,11 +98,34 @@ class RadioConfig:
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AircraftType(UnitType[FlyingType]):
|
||||
class PatrolConfig:
|
||||
altitude: Optional[Distance]
|
||||
speed: Optional[Speed]
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> PatrolConfig:
|
||||
altitude = data.get("altitude", None)
|
||||
speed = data.get("speed", None)
|
||||
return PatrolConfig(
|
||||
feet(altitude) if altitude is not None else None,
|
||||
knots(speed) if speed is not None else None,
|
||||
)
|
||||
|
||||
|
||||
# TODO: Split into PlaneType and HelicopterType?
|
||||
@dataclass(frozen=True)
|
||||
class AircraftType(UnitType[Type[FlyingType]]):
|
||||
carrier_capable: bool
|
||||
lha_capable: bool
|
||||
always_keeps_gun: bool
|
||||
|
||||
# If true, the aircraft does not use the guns as the last resort weapons, but as a main weapon.
|
||||
# It'll RTB when it doesn't have gun ammo left.
|
||||
gunfighter: bool
|
||||
|
||||
max_group_size: int
|
||||
patrol_altitude: Optional[Distance]
|
||||
patrol_speed: Optional[Speed]
|
||||
intra_flight_radio: Optional[Radio]
|
||||
channel_allocator: Optional[RadioChannelAllocator]
|
||||
channel_namer: Type[ChannelNamer]
|
||||
@@ -121,13 +151,86 @@ class AircraftType(UnitType[FlyingType]):
|
||||
def max_speed(self) -> Speed:
|
||||
return kph(self.dcs_unit_type.max_speed)
|
||||
|
||||
@property
|
||||
def preferred_patrol_altitude(self) -> Distance:
|
||||
if self.patrol_altitude is not None:
|
||||
return self.patrol_altitude
|
||||
else:
|
||||
# Estimate based on max speed.
|
||||
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft
|
||||
# Aircraft with max speed 2800 kph will prefer pratrol at 33 000 ft
|
||||
altitude_for_lowest_speed = feet(10 * 1000)
|
||||
altitude_for_highest_speed = feet(33 * 1000)
|
||||
lowest_speed = kph(600)
|
||||
highest_speed = kph(2800)
|
||||
factor = (self.max_speed - lowest_speed).kph / (
|
||||
highest_speed - lowest_speed
|
||||
).kph
|
||||
altitude = (
|
||||
altitude_for_lowest_speed
|
||||
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
|
||||
)
|
||||
logging.debug(
|
||||
f"Preferred patrol altitude for {self.dcs_unit_type.id}: {altitude.feet}"
|
||||
)
|
||||
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
|
||||
return max(
|
||||
altitude_for_lowest_speed,
|
||||
min(altitude_for_highest_speed, rounded_altitude),
|
||||
)
|
||||
|
||||
def preferred_patrol_speed(self, altitude: Distance) -> Speed:
|
||||
"""Preferred true airspeed when patrolling"""
|
||||
if self.patrol_speed is not None:
|
||||
return self.patrol_speed
|
||||
else:
|
||||
# Estimate based on max speed.
|
||||
max_speed = self.max_speed
|
||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 1.6:
|
||||
# Fast airplanes, should manage pretty high patrol speed
|
||||
return (
|
||||
Speed.from_mach(0.85, altitude)
|
||||
if altitude.feet > 20000
|
||||
else Speed.from_mach(0.7, altitude)
|
||||
)
|
||||
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 1.2:
|
||||
# Medium-fast like F/A-18C
|
||||
return (
|
||||
Speed.from_mach(0.8, altitude)
|
||||
if altitude.feet > 20000
|
||||
else Speed.from_mach(0.65, altitude)
|
||||
)
|
||||
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 0.7:
|
||||
# Semi-fast like airliners or similar
|
||||
return (
|
||||
Speed.from_mach(0.5, altitude)
|
||||
if altitude.feet > 20000
|
||||
else Speed.from_mach(0.4, altitude)
|
||||
)
|
||||
else:
|
||||
# Slow like warbirds or helicopters
|
||||
# Use whichever is slowest - mach 0.35 or 70% of max speed
|
||||
logging.debug(f"{self.name} max_speed * 0.7 is {max_speed * 0.7}")
|
||||
return min(Speed.from_mach(0.35, altitude), max_speed * 0.7)
|
||||
|
||||
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
|
||||
from gen.radios import ChannelInUseError, MHz
|
||||
from gen.radios import ChannelInUseError, kHz
|
||||
|
||||
if self.intra_flight_radio is not None:
|
||||
return radio_registry.alloc_for_radio(self.intra_flight_radio)
|
||||
|
||||
freq = MHz(self.dcs_unit_type.radio_frequency)
|
||||
# The default radio frequency is set in megahertz. For some aircraft, it is a
|
||||
# floating point value. For all current aircraft, adjusting to kilohertz will be
|
||||
# sufficient to convert to an integer.
|
||||
in_khz = float(self.dcs_unit_type.radio_frequency) * 1000
|
||||
if not in_khz.is_integer():
|
||||
logging.warning(
|
||||
f"Found unexpected sub-kHz default radio for {self}: {in_khz} kHz. "
|
||||
"Truncating to integer. The truncated frequency may not be valid for "
|
||||
"the aircraft."
|
||||
)
|
||||
|
||||
freq = kHz(int(in_khz))
|
||||
try:
|
||||
radio_registry.reserve(freq)
|
||||
except ChannelInUseError:
|
||||
@@ -162,6 +265,8 @@ class AircraftType(UnitType[FlyingType]):
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
@@ -183,7 +288,7 @@ class AircraftType(UnitType[FlyingType]):
|
||||
logging.warning(f"No data for {aircraft.id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open() as data_file:
|
||||
with data_path.open(encoding="utf-8") as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
try:
|
||||
@@ -192,6 +297,7 @@ class AircraftType(UnitType[FlyingType]):
|
||||
raise KeyError(f"Missing required price field: {data_path}") from ex
|
||||
|
||||
radio_config = RadioConfig.from_data(data.get("radios", {}))
|
||||
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
|
||||
|
||||
try:
|
||||
introduction = data["introduced"]
|
||||
@@ -204,7 +310,10 @@ class AircraftType(UnitType[FlyingType]):
|
||||
yield AircraftType(
|
||||
dcs_unit_type=aircraft,
|
||||
name=variant,
|
||||
description=data.get("description", "No data."),
|
||||
description=data.get(
|
||||
"description",
|
||||
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
|
||||
),
|
||||
year_introduced=introduction,
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
@@ -213,7 +322,10 @@ class AircraftType(UnitType[FlyingType]):
|
||||
carrier_capable=data.get("carrier_capable", False),
|
||||
lha_capable=data.get("lha_capable", False),
|
||||
always_keeps_gun=data.get("always_keeps_gun", False),
|
||||
gunfighter=data.get("gunfighter", False),
|
||||
max_group_size=data.get("max_group_size", aircraft.group_size_max),
|
||||
patrol_altitude=patrol_config.altitude,
|
||||
patrol_speed=patrol_config.speed,
|
||||
intra_flight_radio=radio_config.intra_flight,
|
||||
channel_allocator=radio_config.channel_allocator,
|
||||
channel_namer=radio_config.channel_namer,
|
||||
|
||||
@@ -15,7 +15,7 @@ from game.dcs.unittype import UnitType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitType(UnitType[VehicleType]):
|
||||
class GroundUnitType(UnitType[Type[VehicleType]]):
|
||||
unit_class: Optional[GroundUnitClass]
|
||||
spawn_weight: int
|
||||
|
||||
@@ -45,6 +45,8 @@ class GroundUnitType(UnitType[VehicleType]):
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
@@ -65,7 +67,7 @@ class GroundUnitType(UnitType[VehicleType]):
|
||||
logging.warning(f"No data for {vehicle.id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open() as data_file:
|
||||
with data_path.open(encoding="utf-8") as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
try:
|
||||
@@ -86,7 +88,10 @@ class GroundUnitType(UnitType[VehicleType]):
|
||||
unit_class=unit_class,
|
||||
spawn_weight=data.get("spawn_weight", 0),
|
||||
name=variant,
|
||||
description=data.get("description", "No data."),
|
||||
description=data.get(
|
||||
"description",
|
||||
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
|
||||
),
|
||||
year_introduced=introduction,
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
|
||||
@@ -4,12 +4,12 @@ from typing import TypeVar, Generic, Type
|
||||
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
|
||||
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UnitType(Generic[DcsUnitTypeT]):
|
||||
dcs_unit_type: Type[DcsUnitTypeT]
|
||||
dcs_unit_type: DcsUnitTypeT
|
||||
name: str
|
||||
description: str
|
||||
year_introduced: str
|
||||
|
||||
@@ -15,6 +15,7 @@ from typing import (
|
||||
Iterator,
|
||||
List,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
|
||||
from game import db
|
||||
@@ -77,8 +78,8 @@ class GroundLosses:
|
||||
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
|
||||
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
||||
player_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||
|
||||
player_buildings: List[Building] = field(default_factory=list)
|
||||
enemy_buildings: List[Building] = field(default_factory=list)
|
||||
@@ -104,8 +105,9 @@ class StateData:
|
||||
#: Names of vehicle (and ship) units that were killed during the mission.
|
||||
killed_ground_units: List[str]
|
||||
|
||||
#: Names of static units that were destroyed during the mission.
|
||||
destroyed_statics: List[str]
|
||||
#: List of descriptions of destroyed statics. Format of each element is a mapping of
|
||||
#: the coordinate type ("x", "y", "z", "type", "orientation") to the value.
|
||||
destroyed_statics: List[dict[str, Union[float, str]]]
|
||||
|
||||
#: Mangled names of bases that were captured during the mission.
|
||||
base_capture_events: List[str]
|
||||
@@ -164,7 +166,7 @@ class Debriefing:
|
||||
yield from self.ground_losses.enemy_airlifts
|
||||
|
||||
@property
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectUnit[Any]]:
|
||||
yield from self.ground_losses.player_ground_objects
|
||||
yield from self.ground_losses.enemy_ground_objects
|
||||
|
||||
@@ -370,32 +372,38 @@ class PollDebriefingFileThread(threading.Thread):
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
self._stop_event.set()
|
||||
|
||||
def stopped(self):
|
||||
def stopped(self) -> bool:
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
if os.path.isfile("state.json"):
|
||||
last_modified = os.path.getmtime("state.json")
|
||||
else:
|
||||
last_modified = 0
|
||||
while not self.stopped():
|
||||
if (
|
||||
os.path.isfile("state.json")
|
||||
and os.path.getmtime("state.json") > last_modified
|
||||
):
|
||||
with open("state.json", "r") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
try:
|
||||
if (
|
||||
os.path.isfile("state.json")
|
||||
and os.path.getmtime("state.json") > last_modified
|
||||
):
|
||||
with open("state.json", "r", encoding="utf-8") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
logging.exception(
|
||||
"Failed to decode state.json. Probably attempted read while DCS "
|
||||
"was still writing the file. Will retry in 5 seconds."
|
||||
)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def wait_for_debriefing(
|
||||
callback: Callable[[Debriefing], None], game: Game, unit_map
|
||||
callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
|
||||
) -> PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||
thread.start()
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .event import Event
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
class AirWarEvent(Event):
|
||||
"""Event handler for the air battle"""
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "AirWar"
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import List, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import Task
|
||||
from dcs.unittype import VehicleType
|
||||
|
||||
from game import persistency
|
||||
from game.debriefing import AirLosses, Debriefing
|
||||
@@ -38,13 +37,13 @@ class Event:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
game,
|
||||
game: Game,
|
||||
from_cp: ControlPoint,
|
||||
target_cp: ControlPoint,
|
||||
location: Point,
|
||||
attacker_name: str,
|
||||
defender_name: str,
|
||||
):
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.from_cp = from_cp
|
||||
self.to_cp = target_cp
|
||||
@@ -54,7 +53,7 @@ class Event:
|
||||
|
||||
@property
|
||||
def is_player_attacking(self) -> bool:
|
||||
return self.attacker_name == self.game.player_name
|
||||
return self.attacker_name == self.game.player_faction.name
|
||||
|
||||
@property
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
@@ -220,10 +219,10 @@ class Event:
|
||||
for loss in debriefing.ground_object_losses:
|
||||
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
||||
if not hasattr(loss.group, "units_losts"):
|
||||
loss.group.units_losts = []
|
||||
loss.group.units_losts = [] # type: ignore
|
||||
|
||||
loss.group.units.remove(loss.unit)
|
||||
loss.group.units_losts.append(loss.unit)
|
||||
loss.group.units_losts.append(loss.unit) # type: ignore
|
||||
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
@@ -265,7 +264,7 @@ class Event:
|
||||
except Exception:
|
||||
logging.exception(f"Could not process base capture {captured}")
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
def commit(self, debriefing: Debriefing) -> None:
|
||||
logging.info("Committing mission results")
|
||||
|
||||
self.commit_air_losses(debriefing)
|
||||
@@ -298,15 +297,16 @@ class Event:
|
||||
|
||||
delta = 0.0
|
||||
player_won = True
|
||||
status_msg: str = ""
|
||||
ally_casualties = debriefing.casualty_count(cp)
|
||||
enemy_casualties = debriefing.casualty_count(enemy_cp)
|
||||
ally_units_alive = cp.base.total_armor
|
||||
enemy_units_alive = enemy_cp.base.total_armor
|
||||
|
||||
print(ally_units_alive)
|
||||
print(enemy_units_alive)
|
||||
print(ally_casualties)
|
||||
print(enemy_casualties)
|
||||
print(f"Remaining allied units: {ally_units_alive}")
|
||||
print(f"Remaining enemy units: {enemy_units_alive}")
|
||||
print(f"Allied casualties {ally_casualties}")
|
||||
print(f"Enemy casualties {enemy_casualties}")
|
||||
|
||||
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
||||
|
||||
@@ -319,24 +319,31 @@ class Event:
|
||||
if ally_units_alive == 0:
|
||||
player_won = False
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"No allied units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces suffer a strong defeat."
|
||||
elif enemy_units_alive == 0:
|
||||
player_won = True
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"No enemy units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces win a strong victory."
|
||||
elif cp.stances[enemy_cp.id] == CombatStance.RETREAT:
|
||||
player_won = False
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Allied forces are retreating along the {cp.name}-{enemy_cp.name} frontline, suffering a strong defeat."
|
||||
else:
|
||||
if enemy_casualties > ally_casualties:
|
||||
player_won = True
|
||||
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Allied forces break through the {cp.name}-{enemy_cp.name} frontline, winning a strong victory"
|
||||
else:
|
||||
if ratio > 3:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Enemy casualties massively outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a strong victory."
|
||||
elif ratio < 1.5:
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
status_msg = f"Enemy casualties minorly outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a minor victory."
|
||||
else:
|
||||
delta = DEFEAT_INFLUENCE
|
||||
status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory."
|
||||
elif ally_casualties > enemy_casualties:
|
||||
|
||||
if (
|
||||
@@ -346,54 +353,66 @@ class Event:
|
||||
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
|
||||
player_won = True
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
status_msg = f"Despite suffering losses, allied forces still outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a minor victory."
|
||||
elif (
|
||||
ally_units_alive > 3 * enemy_units_alive
|
||||
and player_aggresive
|
||||
):
|
||||
player_won = True
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Despite suffering losses, allied forces still heavily outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a major victory."
|
||||
else:
|
||||
# But is the enemy is not outnumbered, we lose
|
||||
# But if the enemy is not outnumbered, we lose
|
||||
player_won = False
|
||||
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces have overextended themselves, suffering a major defeat."
|
||||
else:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
delta = DEFEAT_INFLUENCE
|
||||
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces suffer a defeat."
|
||||
|
||||
# No progress with defensive strategies
|
||||
if player_won and cp.stances[enemy_cp.id] in [
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.AMBUSH,
|
||||
]:
|
||||
print("Defensive stance, progress is limited")
|
||||
print(
|
||||
f"Allied forces have adopted a defensive stance along the {cp.name}-{enemy_cp.name} "
|
||||
f"frontline, making only limited progress."
|
||||
)
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
|
||||
if player_won:
|
||||
print(cp.name + " won ! factor > " + str(delta))
|
||||
cp.base.affect_strength(delta)
|
||||
enemy_cp.base.affect_strength(-delta)
|
||||
# Handle the case where there are no casualties at all on either side but both sides still have units
|
||||
if delta == 0.0:
|
||||
print(status_msg)
|
||||
info = Information(
|
||||
"Frontline Report",
|
||||
"Our ground forces from "
|
||||
+ cp.name
|
||||
+ " are making progress toward "
|
||||
+ enemy_cp.name,
|
||||
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
else:
|
||||
print(cp.name + " lost ! factor > " + str(delta))
|
||||
enemy_cp.base.affect_strength(delta)
|
||||
cp.base.affect_strength(-delta)
|
||||
info = Information(
|
||||
"Frontline Report",
|
||||
"Our ground forces from "
|
||||
+ cp.name
|
||||
+ " are losing ground against the enemy forces from "
|
||||
+ enemy_cp.name,
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
if player_won:
|
||||
print(status_msg)
|
||||
cp.base.affect_strength(delta)
|
||||
enemy_cp.base.affect_strength(-delta)
|
||||
info = Information(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
else:
|
||||
print(status_msg)
|
||||
enemy_cp.base.affect_strength(delta)
|
||||
cp.base.affect_strength(-delta)
|
||||
info = Information(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} are losing ground against the enemy forces from "
|
||||
f"{enemy_cp.name}. {status_msg}",
|
||||
self.game.turn,
|
||||
)
|
||||
self.game.informations.append(info)
|
||||
|
||||
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||
""" "
|
||||
|
||||
@@ -8,5 +8,5 @@ class FrontlineAttackEvent(Event):
|
||||
future unique Event handling
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "Frontline attack"
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import itertools
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Type, List, Any, Iterator
|
||||
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
|
||||
|
||||
import dcs
|
||||
from dcs.countries import country_dict
|
||||
@@ -25,6 +25,9 @@ from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.start_generator import ModSettings
|
||||
|
||||
|
||||
@dataclass
|
||||
class Faction:
|
||||
@@ -81,10 +84,10 @@ class Faction:
|
||||
requirements: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# possible aircraft carrier units
|
||||
aircraft_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# possible helicopter carrier units
|
||||
helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
|
||||
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Possible carrier names
|
||||
carrier_names: List[str] = field(default_factory=list)
|
||||
@@ -257,6 +260,83 @@ class Faction:
|
||||
if unit.unit_class is unit_class:
|
||||
yield unit
|
||||
|
||||
def apply_mod_settings(self, mod_settings: ModSettings) -> Faction:
|
||||
# aircraft
|
||||
if not mod_settings.a4_skyhawk:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
if not mod_settings.hercules:
|
||||
self.remove_aircraft("Hercules")
|
||||
if not mod_settings.f22_raptor:
|
||||
self.remove_aircraft("F-22A")
|
||||
if not mod_settings.jas39_gripen:
|
||||
self.remove_aircraft("JAS39Gripen")
|
||||
self.remove_aircraft("JAS39Gripen_AG")
|
||||
if not mod_settings.su57_felon:
|
||||
self.remove_aircraft("Su-57")
|
||||
# frenchpack
|
||||
if not mod_settings.frenchpack:
|
||||
self.remove_vehicle("AMX10RCR")
|
||||
self.remove_vehicle("SEPAR")
|
||||
self.remove_vehicle("ERC")
|
||||
self.remove_vehicle("M120")
|
||||
self.remove_vehicle("AA20")
|
||||
self.remove_vehicle("TRM2000")
|
||||
self.remove_vehicle("TRM2000_Citerne")
|
||||
self.remove_vehicle("TRM2000_AA20")
|
||||
self.remove_vehicle("TRMMISTRAL")
|
||||
self.remove_vehicle("VABH")
|
||||
self.remove_vehicle("VAB_RADIO")
|
||||
self.remove_vehicle("VAB_50")
|
||||
self.remove_vehicle("VIB_VBR")
|
||||
self.remove_vehicle("VAB_HOT")
|
||||
self.remove_vehicle("VAB_MORTIER")
|
||||
self.remove_vehicle("VBL50")
|
||||
self.remove_vehicle("VBLANF1")
|
||||
self.remove_vehicle("VBL-radio")
|
||||
self.remove_vehicle("VBAE")
|
||||
self.remove_vehicle("VBAE_MMP")
|
||||
self.remove_vehicle("AMX-30B2")
|
||||
self.remove_vehicle("Tracma")
|
||||
self.remove_vehicle("JTACFP")
|
||||
self.remove_vehicle("SHERIDAN")
|
||||
self.remove_vehicle("Leclerc_XXI")
|
||||
self.remove_vehicle("Toyota_bleu")
|
||||
self.remove_vehicle("Toyota_vert")
|
||||
self.remove_vehicle("Toyota_desert")
|
||||
self.remove_vehicle("Kamikaze")
|
||||
self.remove_vehicle("AMX1375")
|
||||
self.remove_vehicle("AMX1390")
|
||||
self.remove_vehicle("VBCI")
|
||||
self.remove_vehicle("T62")
|
||||
self.remove_vehicle("T64BV")
|
||||
self.remove_vehicle("T72M")
|
||||
self.remove_vehicle("KORNET")
|
||||
# high digit sams
|
||||
if not mod_settings.high_digit_sams:
|
||||
self.remove_air_defenses("SA10BGenerator")
|
||||
self.remove_air_defenses("SA12Generator")
|
||||
self.remove_air_defenses("SA20Generator")
|
||||
self.remove_air_defenses("SA20BGenerator")
|
||||
self.remove_air_defenses("SA23Generator")
|
||||
self.remove_air_defenses("SA17Generator")
|
||||
self.remove_air_defenses("KS19Generator")
|
||||
return self
|
||||
|
||||
def remove_aircraft(self, name: str) -> None:
|
||||
for i in self.aircrafts:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_air_defenses(self, name: str) -> None:
|
||||
for i in self.air_defenses:
|
||||
if i == name:
|
||||
self.air_defenses.remove(i)
|
||||
|
||||
def remove_vehicle(self, name: str) -> None:
|
||||
for i in self.frontline_units:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.frontline_units.remove(i)
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
if (ship := getattr(dcs.ships, name, None)) is not None:
|
||||
@@ -265,7 +345,7 @@ def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
return None
|
||||
|
||||
|
||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
||||
def load_all_ships(data: list[str]) -> List[Type[ShipType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_ship(name)
|
||||
|
||||
298
game/game.py
298
game/game.py
@@ -1,10 +1,11 @@
|
||||
import itertools
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, List, Type, Union, cast
|
||||
|
||||
from dcs.action import Coalition
|
||||
from dcs.mapping import Point
|
||||
@@ -12,7 +13,6 @@ from dcs.task import CAP, CAS, PinpointStrike
|
||||
from dcs.vehicles import AirDefence
|
||||
from faker import Faker
|
||||
|
||||
from game import db
|
||||
from game.inventory import GlobalAircraftInventory
|
||||
from game.models.game_stats import GameStats
|
||||
from game.plugins import LuaPluginManager
|
||||
@@ -35,7 +35,7 @@ from .procurement import AircraftProcurementRequest, ProcurementAi
|
||||
from .profiling import logged_duration
|
||||
from .settings import Settings, AutoAtoBehavior
|
||||
from .squadrons import AirWing
|
||||
from .theater import ConflictTheater
|
||||
from .theater import ConflictTheater, ControlPoint
|
||||
from .theater.bullseye import Bullseye
|
||||
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||
from .threatzones import ThreatZones
|
||||
@@ -86,8 +86,8 @@ class TurnState(Enum):
|
||||
class Game:
|
||||
def __init__(
|
||||
self,
|
||||
player_name: str,
|
||||
enemy_name: str,
|
||||
player_faction: Faction,
|
||||
enemy_faction: Faction,
|
||||
theater: ConflictTheater,
|
||||
start_date: datetime,
|
||||
settings: Settings,
|
||||
@@ -97,23 +97,23 @@ class Game:
|
||||
self.settings = settings
|
||||
self.events: List[Event] = []
|
||||
self.theater = theater
|
||||
self.player_name = player_name
|
||||
self.player_country = db.FACTIONS[player_name].country
|
||||
self.enemy_name = enemy_name
|
||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
||||
self.player_faction = player_faction
|
||||
self.player_country = player_faction.country
|
||||
self.enemy_faction = enemy_faction
|
||||
self.enemy_country = enemy_faction.country
|
||||
# pass_turn() will be called when initialization is complete which will
|
||||
# increment this to turn 0 before it reaches the player.
|
||||
self.turn = -1
|
||||
# NB: This is the *start* date. It is never updated.
|
||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||
self.game_stats = GameStats()
|
||||
self.game_stats.update(self)
|
||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
||||
self.notes = ""
|
||||
self.ground_planners: dict[int, GroundPlanner] = {}
|
||||
self.informations = []
|
||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||
self.__culling_zones: List[Point] = []
|
||||
self.__destroyed_units: List[str] = []
|
||||
self.__destroyed_units: list[dict[str, Union[float, str]]] = []
|
||||
self.savepath = ""
|
||||
self.budget = player_budget
|
||||
self.enemy_budget = enemy_budget
|
||||
@@ -149,7 +149,7 @@ class Game:
|
||||
|
||||
self.on_load(game_still_initializing=True)
|
||||
|
||||
def __getstate__(self) -> Dict[str, Any]:
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
@@ -161,7 +161,7 @@ class Game:
|
||||
del state["red_faker"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
@@ -188,7 +188,7 @@ class Game:
|
||||
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
|
||||
)
|
||||
|
||||
def sanitize_sides(self):
|
||||
def sanitize_sides(self) -> None:
|
||||
"""
|
||||
Make sure the opposing factions are using different countries
|
||||
:return:
|
||||
@@ -201,14 +201,6 @@ class Game:
|
||||
else:
|
||||
self.enemy_country = "Russia"
|
||||
|
||||
@property
|
||||
def player_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.player_name]
|
||||
|
||||
@property
|
||||
def enemy_faction(self) -> Faction:
|
||||
return db.FACTIONS[self.enemy_name]
|
||||
|
||||
def faction_for(self, player: bool) -> Faction:
|
||||
if player:
|
||||
return self.player_faction
|
||||
@@ -234,26 +226,21 @@ class Game:
|
||||
return self.blue_bullseye
|
||||
return self.red_bullseye
|
||||
|
||||
def _roll(self, prob, mult):
|
||||
if self.settings.version == "dev":
|
||||
# always generate all events for dev
|
||||
return 100
|
||||
else:
|
||||
return random.randint(1, 100) <= prob * mult
|
||||
|
||||
def _generate_player_event(self, event_class, player_cp, enemy_cp):
|
||||
def _generate_player_event(
|
||||
self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint
|
||||
) -> None:
|
||||
self.events.append(
|
||||
event_class(
|
||||
self,
|
||||
player_cp,
|
||||
enemy_cp,
|
||||
enemy_cp.position,
|
||||
self.player_name,
|
||||
self.enemy_name,
|
||||
self.player_faction.name,
|
||||
self.enemy_faction.name,
|
||||
)
|
||||
)
|
||||
|
||||
def _generate_events(self):
|
||||
def _generate_events(self) -> None:
|
||||
for front_line in self.theater.conflicts():
|
||||
self._generate_player_event(
|
||||
FrontlineAttackEvent,
|
||||
@@ -267,21 +254,22 @@ class Game:
|
||||
else:
|
||||
self.enemy_budget += amount
|
||||
|
||||
def process_player_income(self):
|
||||
def process_player_income(self) -> None:
|
||||
self.budget += Income(self, player=True).total
|
||||
|
||||
def process_enemy_income(self):
|
||||
def process_enemy_income(self) -> None:
|
||||
# TODO: Clean up save compat.
|
||||
if not hasattr(self, "enemy_budget"):
|
||||
self.enemy_budget = 0
|
||||
self.enemy_budget += Income(self, player=False).total
|
||||
|
||||
def initiate_event(self, event: Event) -> UnitMap:
|
||||
@staticmethod
|
||||
def initiate_event(event: Event) -> UnitMap:
|
||||
# assert event in self.events
|
||||
logging.info("Generating {} (regular)".format(event))
|
||||
return event.generate()
|
||||
|
||||
def finish_event(self, event: Event, debriefing: Debriefing):
|
||||
def finish_event(self, event: Event, debriefing: Debriefing) -> None:
|
||||
logging.info("Finishing event {}".format(event))
|
||||
event.commit(debriefing)
|
||||
|
||||
@@ -290,16 +278,6 @@ class Game:
|
||||
else:
|
||||
logging.info("finish_event: event not in the events!")
|
||||
|
||||
def is_player_attack(self, event):
|
||||
if isinstance(event, Event):
|
||||
return (
|
||||
event
|
||||
and event.attacker_name
|
||||
and event.attacker_name == self.player_name
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(f"{event} was passed when an Event type was expected")
|
||||
|
||||
def on_load(self, game_still_initializing: bool = False) -> None:
|
||||
if not hasattr(self, "name_generator"):
|
||||
self.name_generator = naming.namegen
|
||||
@@ -322,6 +300,33 @@ class Game:
|
||||
self.red_ato.clear()
|
||||
|
||||
def finish_turn(self, skipped: bool = False) -> None:
|
||||
"""Finalizes the current turn and advances to the next turn.
|
||||
|
||||
This handles the turn-end portion of passing a turn. Initialization of the next
|
||||
turn is handled by `initialize_turn`. These are separate processes because while
|
||||
turns may be initialized more than once under some circumstances (see the
|
||||
documentation for `initialize_turn`), `finish_turn` performs the work that
|
||||
should be guaranteed to happen only once per turn:
|
||||
|
||||
* Turn counter increment.
|
||||
* Delivering units ordered the previous turn.
|
||||
* Transfer progress.
|
||||
* Squadron replenishment.
|
||||
* Income distribution.
|
||||
* Base strength (front line position) adjustment.
|
||||
* Weather/time-of-day generation.
|
||||
|
||||
Some actions (like transit network assembly) will happen both here and in
|
||||
`initialize_turn`. We need the network to be up to date so we can account for
|
||||
base captures when processing the transfers that occurred last turn, but we also
|
||||
need it to be up to date in the case of a re-initialization in `initialize_turn`
|
||||
(such as to account for a cheat base capture) so that orders are only placed
|
||||
where a supply route exists to the destination. This is a relatively cheap
|
||||
operation so duplicating the effort is not a problem.
|
||||
|
||||
Args:
|
||||
skipped: True if the turn was skipped.
|
||||
"""
|
||||
self.informations.append(
|
||||
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
||||
)
|
||||
@@ -344,10 +349,10 @@ class Game:
|
||||
self.blue_air_wing.replenish()
|
||||
self.red_air_wing.replenish()
|
||||
|
||||
if not skipped and self.turn > 1:
|
||||
if not skipped:
|
||||
for cp in self.theater.player_points():
|
||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
else:
|
||||
elif self.turn > 1:
|
||||
for cp in self.theater.player_points():
|
||||
if not cp.is_carrier and not cp.is_lha:
|
||||
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
||||
@@ -358,10 +363,18 @@ class Game:
|
||||
self.process_player_income()
|
||||
|
||||
def begin_turn_0(self) -> None:
|
||||
"""Initialization for the first turn of the game."""
|
||||
self.turn = 0
|
||||
self.initialize_turn()
|
||||
|
||||
def pass_turn(self, no_action: bool = False) -> None:
|
||||
"""Ends the current turn and initializes the new turn.
|
||||
|
||||
Called both when skipping a turn or by ending the turn as the result of combat.
|
||||
|
||||
Args:
|
||||
no_action: True if the turn was skipped.
|
||||
"""
|
||||
logging.info("Pass turn")
|
||||
with logged_duration("Turn finalization"):
|
||||
self.finish_turn(no_action)
|
||||
@@ -371,7 +384,7 @@ class Game:
|
||||
# Autosave progress
|
||||
persistency.autosave(self)
|
||||
|
||||
def check_win_loss(self):
|
||||
def check_win_loss(self) -> TurnState:
|
||||
player_airbases = {
|
||||
cp for cp in self.theater.player_points() if cp.runway_is_operational()
|
||||
}
|
||||
@@ -391,26 +404,90 @@ class Game:
|
||||
self.blue_bullseye = Bullseye(enemy_cp.position)
|
||||
self.red_bullseye = Bullseye(player_cp.position)
|
||||
|
||||
def initialize_turn(self) -> None:
|
||||
def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None:
|
||||
"""Performs turn initialization for the specified players.
|
||||
|
||||
Turn initialization performs all of the beginning-of-turn actions. *End-of-turn*
|
||||
processing happens in `pass_turn` (despite the name, it's called both for
|
||||
skipping the turn and ending the turn after combat).
|
||||
|
||||
Special care needs to be taken here because initialization can occur more than
|
||||
once per turn. A number of events can require re-initializing a turn:
|
||||
|
||||
* Cheat capture. Bases changing hands invalidates many missions in both ATOs,
|
||||
purchase orders, threat zones, transit networks, etc. Practically speaking,
|
||||
after a base capture the turn needs to be treated as fully new. The game might
|
||||
even be over after a capture.
|
||||
* Cheat front line position. CAS missions are no longer in the correct location,
|
||||
and the ground planner may also need changes.
|
||||
* Selling/buying units at TGOs. Selling a TGO might leave missions in the ATO
|
||||
with invalid targets. Buying a new SAM (or even replacing some units in a SAM)
|
||||
potentially changes the threat zone and may alter mission priorities and
|
||||
flight planning.
|
||||
|
||||
Most of the work is delegated to initialize_turn_for, which handles the
|
||||
coalition-specific turn initialization. In some cases only one coalition will be
|
||||
(re-) initialized. This is the case when buying or selling TGO units, since we
|
||||
don't want to force the player to redo all their planning just because they
|
||||
repaired a SAM, but should replan opfor when that happens. On the other hand,
|
||||
base captures are significant enough (and likely enough to be the first thing
|
||||
the player does in a turn) that we replan blue as well. Front lines are less
|
||||
impactful but also likely to be early, so they also cause a blue replan.
|
||||
|
||||
Args:
|
||||
for_red: True if opfor should be re-initialized.
|
||||
for_blue: True if the player coalition should be re-initialized.
|
||||
"""
|
||||
self.events = []
|
||||
self._generate_events()
|
||||
|
||||
self.set_bullseye()
|
||||
|
||||
# Update statistics
|
||||
self.game_stats.update(self)
|
||||
|
||||
self.blue_air_wing.reset()
|
||||
self.red_air_wing.reset()
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
|
||||
# Check for win or loss condition
|
||||
turn_state = self.check_win_loss()
|
||||
if turn_state in (TurnState.LOSS, TurnState.WIN):
|
||||
return self.process_win_loss(turn_state)
|
||||
|
||||
# Plan Coalition specific turn
|
||||
if for_red:
|
||||
self.initialize_turn_for(player=False)
|
||||
if for_blue:
|
||||
self.initialize_turn_for(player=True)
|
||||
|
||||
# Plan GroundWar
|
||||
for cp in self.theater.controlpoints:
|
||||
if cp.has_frontline:
|
||||
gplanner = GroundPlanner(cp, self)
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
def initialize_turn_for(self, player: bool) -> None:
|
||||
"""Processes coalition-specific turn initialization.
|
||||
|
||||
For more information on turn initialization in general, see the documentation
|
||||
for `Game.initialize_turn`.
|
||||
|
||||
Args:
|
||||
player: True if the player coalition is being initialized. False for opfor
|
||||
initialization.
|
||||
"""
|
||||
self.ato_for(player).clear()
|
||||
self.air_wing_for(player).reset()
|
||||
|
||||
self.aircraft_inventory.reset()
|
||||
for cp in self.theater.controlpoints:
|
||||
self.aircraft_inventory.set_from_control_point(cp)
|
||||
# Refund all pending deliveries for opfor and if player
|
||||
# has automate_aircraft_reinforcements
|
||||
if (not player and not cp.captured) or (
|
||||
player
|
||||
and cp.captured
|
||||
and self.settings.automate_aircraft_reinforcements
|
||||
):
|
||||
cp.pending_unit_deliveries.refund_all(self)
|
||||
|
||||
# Plan flights & combat for next turn
|
||||
with logged_duration("Computing conflict positions"):
|
||||
self.compute_conflicts_position()
|
||||
@@ -420,55 +497,48 @@ class Game:
|
||||
self.compute_transit_networks()
|
||||
self.ground_planners = {}
|
||||
|
||||
self.blue_procurement_requests.clear()
|
||||
self.red_procurement_requests.clear()
|
||||
self.procurement_requests_for(player).clear()
|
||||
|
||||
with logged_duration("Procurement of airlift assets"):
|
||||
self.transfers.order_airlift_assets()
|
||||
with logged_duration("Transport planning"):
|
||||
self.transfers.plan_transports()
|
||||
|
||||
with logged_duration("Blue mission planning"):
|
||||
if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
|
||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
||||
blue_planner.plan_missions()
|
||||
if not player or (
|
||||
player and self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
|
||||
):
|
||||
color = "Blue" if player else "Red"
|
||||
with logged_duration(f"{color} mission planning"):
|
||||
mission_planner = CoalitionMissionPlanner(self, player)
|
||||
mission_planner.plan_missions()
|
||||
|
||||
with logged_duration("Red mission planning"):
|
||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
||||
red_planner.plan_missions()
|
||||
self.plan_procurement_for(player)
|
||||
|
||||
for cp in self.theater.controlpoints:
|
||||
if cp.has_frontline:
|
||||
gplanner = GroundPlanner(cp, self)
|
||||
gplanner.plan_groundwar()
|
||||
self.ground_planners[cp.id] = gplanner
|
||||
|
||||
self.plan_procurement()
|
||||
|
||||
def plan_procurement(self) -> None:
|
||||
def plan_procurement_for(self, for_player: bool) -> None:
|
||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
||||
# gets much more of the budget that turn. Otherwise budget (after
|
||||
# repairs) is split evenly between air and ground. For the default
|
||||
# starting budget of 2000 this gives 600 to ground forces and 1400 to
|
||||
# aircraft. After that the budget will be spend proportionally based on how much is already invested
|
||||
|
||||
self.budget = ProcurementAi(
|
||||
self,
|
||||
for_player=True,
|
||||
faction=self.player_faction,
|
||||
manage_runways=self.settings.automate_runway_repair,
|
||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||
).spend_budget(self.budget)
|
||||
|
||||
self.enemy_budget = ProcurementAi(
|
||||
self,
|
||||
for_player=False,
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True,
|
||||
).spend_budget(self.enemy_budget)
|
||||
if for_player:
|
||||
self.budget = ProcurementAi(
|
||||
self,
|
||||
for_player=True,
|
||||
faction=self.player_faction,
|
||||
manage_runways=self.settings.automate_runway_repair,
|
||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||
).spend_budget(self.budget)
|
||||
else:
|
||||
self.enemy_budget = ProcurementAi(
|
||||
self,
|
||||
for_player=False,
|
||||
faction=self.enemy_faction,
|
||||
manage_runways=True,
|
||||
manage_front_line=True,
|
||||
manage_aircraft=True,
|
||||
).spend_budget(self.enemy_budget)
|
||||
|
||||
def message(self, text: str) -> None:
|
||||
self.informations.append(Information(text, turn=self.turn))
|
||||
@@ -481,14 +551,14 @@ class Game:
|
||||
def current_day(self) -> date:
|
||||
return self.date + timedelta(days=self.turn // 4)
|
||||
|
||||
def next_unit_id(self):
|
||||
def next_unit_id(self) -> int:
|
||||
"""
|
||||
Next unit id for pre-generated units
|
||||
"""
|
||||
self.current_unit_id += 1
|
||||
return self.current_unit_id
|
||||
|
||||
def next_group_id(self):
|
||||
def next_group_id(self) -> int:
|
||||
"""
|
||||
Next unit id for pre-generated units
|
||||
"""
|
||||
@@ -522,7 +592,7 @@ class Game:
|
||||
return self.blue_navmesh
|
||||
return self.red_navmesh
|
||||
|
||||
def compute_conflicts_position(self):
|
||||
def compute_conflicts_position(self) -> None:
|
||||
"""
|
||||
Compute the current conflict center position(s), mainly used for culling calculation
|
||||
:return: List of points of interests
|
||||
@@ -545,7 +615,7 @@ class Game:
|
||||
# If there is no conflict take the center point between the two nearest opposing bases
|
||||
if len(zones) == 0:
|
||||
cpoint = None
|
||||
min_distance = sys.maxsize
|
||||
min_distance = math.inf
|
||||
for cp in self.theater.player_points():
|
||||
for cp2 in self.theater.enemy_points():
|
||||
d = cp.position.distance_to_point(cp2.position)
|
||||
@@ -581,15 +651,15 @@ class Game:
|
||||
|
||||
self.__culling_zones = zones
|
||||
|
||||
def add_destroyed_units(self, data):
|
||||
pos = Point(data["x"], data["z"])
|
||||
def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None:
|
||||
pos = Point(cast(float, data["x"]), cast(float, data["z"]))
|
||||
if self.theater.is_on_land(pos):
|
||||
self.__destroyed_units.append(data)
|
||||
|
||||
def get_destroyed_units(self):
|
||||
def get_destroyed_units(self) -> list[dict[str, Union[float, str]]]:
|
||||
return self.__destroyed_units
|
||||
|
||||
def position_culled(self, pos):
|
||||
def position_culled(self, pos: Point) -> bool:
|
||||
"""
|
||||
Check if unit can be generated at given position depending on culling performance settings
|
||||
:param pos: Position you are tryng to spawn stuff at
|
||||
@@ -602,7 +672,7 @@ class Game:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_culling_zones(self):
|
||||
def get_culling_zones(self) -> list[Point]:
|
||||
"""
|
||||
Check culling points
|
||||
:return: List of culling zones
|
||||
@@ -610,30 +680,28 @@ class Game:
|
||||
return self.__culling_zones
|
||||
|
||||
# 1 = red, 2 = blue
|
||||
def get_player_coalition_id(self):
|
||||
def get_player_coalition_id(self) -> int:
|
||||
return 2
|
||||
|
||||
def get_enemy_coalition_id(self):
|
||||
def get_enemy_coalition_id(self) -> int:
|
||||
return 1
|
||||
|
||||
def get_player_coalition(self):
|
||||
def get_player_coalition(self) -> Coalition:
|
||||
return Coalition.Blue
|
||||
|
||||
def get_enemy_coalition(self):
|
||||
def get_enemy_coalition(self) -> Coalition:
|
||||
return Coalition.Red
|
||||
|
||||
def get_player_color(self):
|
||||
def get_player_color(self) -> str:
|
||||
return "blue"
|
||||
|
||||
def get_enemy_color(self):
|
||||
def get_enemy_color(self) -> str:
|
||||
return "red"
|
||||
|
||||
def process_win_loss(self, turn_state: TurnState):
|
||||
def process_win_loss(self, turn_state: TurnState) -> None:
|
||||
if turn_state is TurnState.WIN:
|
||||
return self.message(
|
||||
"Congratulations, you are victorious! Start a new campaign to continue."
|
||||
self.message(
|
||||
"Congratulations, you are victorious! Start a new campaign to continue."
|
||||
)
|
||||
elif turn_state is TurnState.LOSS:
|
||||
return self.message(
|
||||
"Game Over, you lose. Start a new campaign to continue."
|
||||
)
|
||||
self.message("Game Over, you lose. Start a new campaign to continue.")
|
||||
|
||||
@@ -2,13 +2,13 @@ import datetime
|
||||
|
||||
|
||||
class Information:
|
||||
def __init__(self, title="", text="", turn=0):
|
||||
def __init__(self, title: str = "", text: str = "", turn: int = 0) -> None:
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.turn = turn
|
||||
self.timestamp = datetime.datetime.now()
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "[{}][{}] {} {}".format(
|
||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.timestamp is not None
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
class DestroyedUnit:
|
||||
"""
|
||||
Store info about a destroyed unit
|
||||
"""
|
||||
|
||||
x: int
|
||||
y: int
|
||||
name: str
|
||||
|
||||
def __init__(self, x, y, name):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.name = name
|
||||
@@ -1,4 +1,9 @@
|
||||
from typing import List
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class FactionTurnMetadata:
|
||||
@@ -10,7 +15,7 @@ class FactionTurnMetadata:
|
||||
vehicles_count: int = 0
|
||||
sam_count: int = 0
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.aircraft_count = 0
|
||||
self.vehicles_count = 0
|
||||
self.sam_count = 0
|
||||
@@ -24,7 +29,7 @@ class GameTurnMetadata:
|
||||
allied_units: FactionTurnMetadata
|
||||
enemy_units: FactionTurnMetadata
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.allied_units = FactionTurnMetadata()
|
||||
self.enemy_units = FactionTurnMetadata()
|
||||
|
||||
@@ -34,15 +39,19 @@ class GameStats:
|
||||
Store statistics for the current game
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.data_per_turn: List[GameTurnMetadata] = []
|
||||
|
||||
def update(self, game):
|
||||
def update(self, game: Game) -> None:
|
||||
"""
|
||||
Save data for current turn
|
||||
:param game: Game we want to save the data about
|
||||
"""
|
||||
|
||||
# Remove the current turn if its just an update for this turn
|
||||
if 0 < game.turn < len(self.data_per_turn):
|
||||
del self.data_per_turn[-1]
|
||||
|
||||
turn_data = GameTurnMetadata()
|
||||
|
||||
for cp in game.theater.controlpoints:
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Set, TYPE_CHECKING
|
||||
from typing import Iterable, List, Set, TYPE_CHECKING, cast
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.action import DoScript, DoScriptFile
|
||||
@@ -62,28 +62,14 @@ class Operation:
|
||||
plugin_scripts: List[str] = []
|
||||
|
||||
@classmethod
|
||||
def prepare(cls, game: Game):
|
||||
with open("resources/default_options.lua", "r") as f:
|
||||
def prepare(cls, game: Game) -> None:
|
||||
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
|
||||
options_dict = loads(f.read())["options"]
|
||||
cls._set_mission(Mission(game.theater.terrain))
|
||||
cls.game = game
|
||||
cls._setup_mission_coalitions()
|
||||
cls.current_mission.options.load_from_dict(options_dict)
|
||||
|
||||
@classmethod
|
||||
def conflicts(cls) -> Iterable[Conflict]:
|
||||
assert cls.game
|
||||
for frontline in cls.game.theater.conflicts():
|
||||
yield Conflict(
|
||||
cls.game.theater,
|
||||
frontline,
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
frontline.position,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def air_conflict(cls) -> Conflict:
|
||||
assert cls.game
|
||||
@@ -95,10 +81,10 @@ class Operation:
|
||||
return Conflict(
|
||||
cls.game.theater,
|
||||
FrontLine(player_cp, enemy_cp),
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_country,
|
||||
cls.game.enemy_country,
|
||||
cls.game.player_faction.name,
|
||||
cls.game.enemy_faction.name,
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
mid_point,
|
||||
)
|
||||
|
||||
@@ -107,7 +93,7 @@ class Operation:
|
||||
cls.current_mission = mission
|
||||
|
||||
@classmethod
|
||||
def _setup_mission_coalitions(cls):
|
||||
def _setup_mission_coalitions(cls) -> None:
|
||||
cls.current_mission.coalition["blue"] = Coalition(
|
||||
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
|
||||
)
|
||||
@@ -163,7 +149,7 @@ class Operation:
|
||||
airsupportgen: AirSupportConflictGenerator,
|
||||
jtacs: List[JtacInfo],
|
||||
airgen: AircraftConflictGenerator,
|
||||
):
|
||||
) -> None:
|
||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
|
||||
|
||||
gens: List[MissionInfoGenerator] = [
|
||||
@@ -251,7 +237,7 @@ class Operation:
|
||||
# beacon list.
|
||||
|
||||
@classmethod
|
||||
def _generate_ground_units(cls):
|
||||
def _generate_ground_units(cls) -> None:
|
||||
cls.groundobjectgen = GroundObjectsGenerator(
|
||||
cls.current_mission,
|
||||
cls.game,
|
||||
@@ -266,11 +252,16 @@ class Operation:
|
||||
"""Add destroyed units to the Mission"""
|
||||
for d in cls.game.get_destroyed_units():
|
||||
try:
|
||||
utype = db.unit_type_from_name(d["type"])
|
||||
type_name = d["type"]
|
||||
if not isinstance(type_name, str):
|
||||
raise TypeError(
|
||||
"Expected the type of the destroyed static to be a string"
|
||||
)
|
||||
utype = db.unit_type_from_name(type_name)
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
pos = Point(d["x"], d["z"])
|
||||
pos = Point(cast(float, d["x"]), cast(float, d["z"]))
|
||||
if (
|
||||
utype is not None
|
||||
and not cls.game.position_culled(pos)
|
||||
@@ -389,8 +380,8 @@ class Operation:
|
||||
player_cp = front_line.blue_cp
|
||||
enemy_cp = front_line.red_cp
|
||||
conflict = Conflict.frontline_cas_conflict(
|
||||
cls.game.player_name,
|
||||
cls.game.enemy_name,
|
||||
cls.game.player_faction.name,
|
||||
cls.game.enemy_faction.name,
|
||||
cls.current_mission.country(cls.game.player_country),
|
||||
cls.current_mission.country(cls.game.enemy_country),
|
||||
front_line,
|
||||
@@ -418,7 +409,7 @@ class Operation:
|
||||
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
|
||||
|
||||
@classmethod
|
||||
def reset_naming_ids(cls):
|
||||
def reset_naming_ids(cls) -> None:
|
||||
namegen.reset_numbers()
|
||||
|
||||
@classmethod
|
||||
@@ -439,8 +430,8 @@ class Operation:
|
||||
"BlueAA": {},
|
||||
} # type: ignore
|
||||
|
||||
for tanker in airsupportgen.air_support.tankers:
|
||||
luaData["Tankers"][tanker.callsign] = {
|
||||
for i, tanker in enumerate(airsupportgen.air_support.tankers):
|
||||
luaData["Tankers"][i] = {
|
||||
"dcsGroupName": tanker.group_name,
|
||||
"callsign": tanker.callsign,
|
||||
"variant": tanker.variant,
|
||||
@@ -448,23 +439,22 @@ class Operation:
|
||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
|
||||
}
|
||||
|
||||
if airsupportgen.air_support.awacs:
|
||||
for awacs in airsupportgen.air_support.awacs:
|
||||
luaData["AWACs"][awacs.callsign] = {
|
||||
"dcsGroupName": awacs.group_name,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz,
|
||||
}
|
||||
for i, awacs in enumerate(airsupportgen.air_support.awacs):
|
||||
luaData["AWACs"][i] = {
|
||||
"dcsGroupName": awacs.group_name,
|
||||
"callsign": awacs.callsign,
|
||||
"radio": awacs.freq.mhz,
|
||||
}
|
||||
|
||||
for jtac in jtacs:
|
||||
luaData["JTACs"][jtac.callsign] = {
|
||||
for i, jtac in enumerate(jtacs):
|
||||
luaData["JTACs"][i] = {
|
||||
"dcsGroupName": jtac.group_name,
|
||||
"callsign": jtac.callsign,
|
||||
"zone": jtac.region,
|
||||
"dcsUnit": jtac.unit_name,
|
||||
"laserCode": jtac.code,
|
||||
}
|
||||
|
||||
flight_count = 0
|
||||
for flight in airgen.flights:
|
||||
if flight.friendly and flight.flight_type in [
|
||||
FlightType.ANTISHIP,
|
||||
@@ -485,7 +475,7 @@ class Operation:
|
||||
elif hasattr(flightTarget, "name"):
|
||||
flightTargetName = flightTarget.name
|
||||
flightTargetType = flightType + " TGT (Airbase)"
|
||||
luaData["TargetPoints"][flightTargetName] = {
|
||||
luaData["TargetPoints"][flight_count] = {
|
||||
"name": flightTargetName,
|
||||
"type": flightTargetType,
|
||||
"position": {
|
||||
@@ -493,6 +483,7 @@ class Operation:
|
||||
"y": flightTarget.position.y,
|
||||
},
|
||||
}
|
||||
flight_count += 1
|
||||
|
||||
for cp in cls.game.theater.controlpoints:
|
||||
for ground_object in cp.ground_objects:
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import shutil
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
_dcs_saved_game_folder: Optional[str] = None
|
||||
_file_abs_path = None
|
||||
|
||||
|
||||
def setup(user_folder: str):
|
||||
def setup(user_folder: str) -> None:
|
||||
global _dcs_saved_game_folder
|
||||
_dcs_saved_game_folder = user_folder
|
||||
_file_abs_path = os.path.join(base_path(), "default.liberation")
|
||||
if not save_dir().exists():
|
||||
save_dir().mkdir(parents=True)
|
||||
|
||||
|
||||
def base_path() -> str:
|
||||
@@ -20,19 +26,23 @@ def base_path() -> str:
|
||||
return _dcs_saved_game_folder
|
||||
|
||||
|
||||
def save_dir() -> Path:
|
||||
return Path(base_path()) / "Liberation" / "Saves"
|
||||
|
||||
|
||||
def _temporary_save_file() -> str:
|
||||
return os.path.join(base_path(), "tmpsave.liberation")
|
||||
return str(save_dir() / "tmpsave.liberation")
|
||||
|
||||
|
||||
def _autosave_path() -> str:
|
||||
return os.path.join(base_path(), "autosave.liberation")
|
||||
return str(save_dir() / "autosave.liberation")
|
||||
|
||||
|
||||
def mission_path_for(name: str) -> str:
|
||||
return os.path.join(base_path(), "Missions", "{}".format(name))
|
||||
return os.path.join(base_path(), "Missions", name)
|
||||
|
||||
|
||||
def load_game(path):
|
||||
def load_game(path: str) -> Optional[Game]:
|
||||
with open(path, "rb") as f:
|
||||
try:
|
||||
save = pickle.load(f)
|
||||
@@ -43,7 +53,7 @@ def load_game(path):
|
||||
return None
|
||||
|
||||
|
||||
def save_game(game) -> bool:
|
||||
def save_game(game: Game) -> bool:
|
||||
try:
|
||||
with open(_temporary_save_file(), "wb") as f:
|
||||
pickle.dump(game, f)
|
||||
@@ -54,7 +64,7 @@ def save_game(game) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def autosave(game) -> bool:
|
||||
def autosave(game: Game) -> bool:
|
||||
"""
|
||||
Autosave to the autosave location
|
||||
:param game: Game to save
|
||||
|
||||
@@ -38,7 +38,7 @@ class PluginSettings:
|
||||
self.settings = Settings()
|
||||
self.initialize_settings()
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
def set_settings(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
self.initialize_settings()
|
||||
|
||||
@@ -146,7 +146,7 @@ class LuaPlugin(PluginSettings):
|
||||
|
||||
return cls(definition)
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
def set_settings(self, settings: Settings) -> None:
|
||||
super().set_settings(settings)
|
||||
for option in self.definition.options:
|
||||
option.set_settings(self.settings)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dcs import Point
|
||||
|
||||
|
||||
class PointWithHeading(Point):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super(PointWithHeading, self).__init__(0, 0)
|
||||
self.heading = 0
|
||||
|
||||
@staticmethod
|
||||
def from_point(point: Point, heading: int):
|
||||
def from_point(point: Point, heading: int) -> PointWithHeading:
|
||||
p = PointWithHeading()
|
||||
p.x = point.x
|
||||
p.y = point.y
|
||||
|
||||
9
game/positioned.py
Normal file
9
game/positioned.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import Protocol
|
||||
|
||||
from dcs import Point
|
||||
|
||||
|
||||
class Positioned(Protocol):
|
||||
@property
|
||||
def position(self) -> Point:
|
||||
raise NotImplementedError
|
||||
@@ -5,7 +5,8 @@ import timeit
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from datetime import timedelta
|
||||
from typing import Iterator
|
||||
from types import TracebackType
|
||||
from typing import Iterator, Optional, Type
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -23,7 +24,12 @@ class MultiEventTracer:
|
||||
def __enter__(self) -> MultiEventTracer:
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: Optional[Type[BaseException]],
|
||||
exc_val: Optional[BaseException],
|
||||
exc_tb: Optional[TracebackType],
|
||||
) -> None:
|
||||
for event, duration in self.events.items():
|
||||
logging.debug("%s took %s", event, duration)
|
||||
|
||||
|
||||
48
game/savecompat.py
Normal file
48
game/savecompat.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Tools for aiding in save compat removal after compatibility breaks."""
|
||||
from collections import Callable
|
||||
from typing import TypeVar
|
||||
|
||||
from game.version import MAJOR_VERSION
|
||||
|
||||
ReturnT = TypeVar("ReturnT")
|
||||
|
||||
|
||||
class DeprecatedSaveCompatError(RuntimeError):
|
||||
def __init__(self, function_name: str) -> None:
|
||||
super().__init__(
|
||||
f"{function_name} has save compat code for a different major version."
|
||||
)
|
||||
|
||||
|
||||
def has_save_compat_for(
|
||||
major: int,
|
||||
) -> Callable[[Callable[..., ReturnT]], Callable[..., ReturnT]]:
|
||||
"""Declares a function or method as having save compat code for a given version.
|
||||
|
||||
If the function has save compatibility for the current major version, there is no
|
||||
change in behavior.
|
||||
|
||||
If the function has save compatibility for a *different* (future or past) major
|
||||
version, DeprecatedSaveCompatError will be raised during startup. Since a break in
|
||||
save compatibility is the definition of a major version break, there's no need to
|
||||
keep around old save compat code; it only serves to mask initialization bugs.
|
||||
|
||||
Args:
|
||||
major: The major version for which the decorated function has save
|
||||
compatibility.
|
||||
|
||||
Returns:
|
||||
The decorated function or method.
|
||||
|
||||
Raises:
|
||||
DeprecatedSaveCompatError: The decorated function has save compat code for
|
||||
another version of liberation, and that code (and the decorator declaring it)
|
||||
should be removed from this branch.
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., ReturnT]) -> Callable[..., ReturnT]:
|
||||
if major != MAJOR_VERSION:
|
||||
raise DeprecatedSaveCompatError(func.__name__)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from enum import Enum, unique
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
from dcs.forcedoptions import ForcedOptions
|
||||
|
||||
@@ -34,11 +34,14 @@ class Settings:
|
||||
player_income_multiplier: float = 1.0
|
||||
enemy_income_multiplier: float = 1.0
|
||||
|
||||
#: Feature flag for squadron limits.
|
||||
enable_squadron_pilot_limits: bool = False
|
||||
|
||||
#: The maximum number of pilots a squadron can have at one time. Changing this after
|
||||
#: the campaign has started will have no immediate effect; pilots already in the
|
||||
#: squadron will not be removed if the limit is lowered and pilots will not be
|
||||
#: immediately created if the limit is raised.
|
||||
squadron_pilot_limit: int = 24
|
||||
squadron_pilot_limit: int = 12
|
||||
|
||||
#: The number of pilots a squadron can replace per turn.
|
||||
squadron_replenishment_rate: int = 4
|
||||
@@ -101,7 +104,7 @@ class Settings:
|
||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# __setstate__ is called with the dict of the object being unpickled. We
|
||||
# can provide save compatibility for new settings options (which
|
||||
# normally would not be present in the unpickled object) by creating a
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import (
|
||||
Optional,
|
||||
Iterator,
|
||||
Sequence,
|
||||
Any,
|
||||
)
|
||||
|
||||
import yaml
|
||||
@@ -112,9 +113,19 @@ class Squadron:
|
||||
return self.name
|
||||
return f'{self.name} "{self.nickname}"'
|
||||
|
||||
@property
|
||||
def pilot_limits_enabled(self) -> bool:
|
||||
return self.game.settings.enable_squadron_pilot_limits
|
||||
|
||||
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
|
||||
if self.pilot_limits_enabled:
|
||||
return None
|
||||
self._recruit_pilots(1)
|
||||
return self.available_pilots.pop()
|
||||
|
||||
def claim_available_pilot(self) -> Optional[Pilot]:
|
||||
if not self.available_pilots:
|
||||
return None
|
||||
return self.claim_new_pilot_if_allowed()
|
||||
|
||||
# For opfor, so player/AI option is irrelevant.
|
||||
if not self.player:
|
||||
@@ -140,7 +151,7 @@ class Squadron:
|
||||
# If they only *prefer* players and we're out of players, just return an AI
|
||||
# pilot.
|
||||
if not prefer_players:
|
||||
return None
|
||||
return self.claim_new_pilot_if_allowed()
|
||||
return self.available_pilots.pop()
|
||||
|
||||
def claim_pilot(self, pilot: Pilot) -> None:
|
||||
@@ -169,9 +180,12 @@ class Squadron:
|
||||
self.available_pilots.extend(new_pilots)
|
||||
|
||||
def replenish_lost_pilots(self) -> None:
|
||||
if not self.pilot_limits_enabled:
|
||||
return
|
||||
|
||||
replenish_count = min(
|
||||
self.game.settings.squadron_replenishment_rate,
|
||||
self.number_of_unfilled_pilot_slots,
|
||||
self._number_of_unfilled_pilot_slots,
|
||||
)
|
||||
if replenish_count > 0:
|
||||
self._recruit_pilots(replenish_count)
|
||||
@@ -183,7 +197,7 @@ class Squadron:
|
||||
def send_on_leave(pilot: Pilot) -> None:
|
||||
pilot.send_on_leave()
|
||||
|
||||
def return_from_leave(self, pilot: Pilot):
|
||||
def return_from_leave(self, pilot: Pilot) -> None:
|
||||
if not self.has_unfilled_pilot_slots:
|
||||
raise RuntimeError(
|
||||
f"Cannot return {pilot} from leave because {self} is full"
|
||||
@@ -213,20 +227,23 @@ class Squadron:
|
||||
return len(self.current_roster)
|
||||
|
||||
@property
|
||||
def number_of_unfilled_pilot_slots(self) -> int:
|
||||
def _number_of_unfilled_pilot_slots(self) -> int:
|
||||
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
|
||||
|
||||
@property
|
||||
def number_of_available_pilots(self) -> int:
|
||||
return len(self.available_pilots)
|
||||
|
||||
def can_provide_pilots(self, count: int) -> bool:
|
||||
return not self.pilot_limits_enabled or self.number_of_available_pilots >= count
|
||||
|
||||
@property
|
||||
def has_available_pilots(self) -> bool:
|
||||
return bool(self.available_pilots)
|
||||
return not self.pilot_limits_enabled or bool(self.available_pilots)
|
||||
|
||||
@property
|
||||
def has_unfilled_pilot_slots(self) -> bool:
|
||||
return self.number_of_unfilled_pilot_slots > 0
|
||||
return not self.pilot_limits_enabled or self._number_of_unfilled_pilot_slots > 0
|
||||
|
||||
def can_auto_assign(self, task: FlightType) -> bool:
|
||||
return task in self.auto_assignable_mission_types
|
||||
@@ -274,7 +291,7 @@ class Squadron:
|
||||
player=player,
|
||||
)
|
||||
|
||||
def __setstate__(self, state) -> None:
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# TODO: Remove save compat.
|
||||
if "auto_assignable_mission_types" not in state:
|
||||
state["auto_assignable_mission_types"] = set(state["mission_types"])
|
||||
@@ -368,6 +385,13 @@ class AirWing:
|
||||
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
|
||||
return self.squadrons[aircraft]
|
||||
|
||||
def can_auto_plan(self, task: FlightType) -> bool:
|
||||
try:
|
||||
next(self.auto_assignable_for_task(task))
|
||||
return True
|
||||
except StopIteration:
|
||||
return False
|
||||
|
||||
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
||||
for squadron in self.iter_squadrons():
|
||||
if squadron.can_auto_assign(task):
|
||||
|
||||
@@ -6,15 +6,15 @@ from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.dcs.unittype import UnitType
|
||||
|
||||
BASE_MAX_STRENGTH = 1
|
||||
BASE_MIN_STRENGTH = 0
|
||||
BASE_MAX_STRENGTH = 1.0
|
||||
BASE_MIN_STRENGTH = 0.0
|
||||
|
||||
|
||||
class Base:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.aircraft: dict[AircraftType, int] = {}
|
||||
self.armor: dict[GroundUnitType, int] = {}
|
||||
self.strength = 1
|
||||
self.strength = 1.0
|
||||
|
||||
@property
|
||||
def total_aircraft(self) -> int:
|
||||
@@ -31,7 +31,7 @@ class Base:
|
||||
total += unit_type.price * count
|
||||
return total
|
||||
|
||||
def total_units_of_type(self, unit_type: UnitType) -> int:
|
||||
def total_units_of_type(self, unit_type: UnitType[Any]) -> int:
|
||||
return sum(
|
||||
[
|
||||
c
|
||||
@@ -40,7 +40,7 @@ class Base:
|
||||
]
|
||||
)
|
||||
|
||||
def commission_units(self, units: dict[Any, int]):
|
||||
def commission_units(self, units: dict[Any, int]) -> None:
|
||||
for unit_type, unit_count in units.items():
|
||||
if unit_count <= 0:
|
||||
continue
|
||||
@@ -56,7 +56,7 @@ class Base:
|
||||
|
||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
||||
|
||||
def commit_losses(self, units_lost: dict[Any, int]):
|
||||
def commit_losses(self, units_lost: dict[Any, int]) -> None:
|
||||
for unit_type, count in units_lost.items():
|
||||
target_dict: dict[Any, int]
|
||||
if unit_type in self.aircraft:
|
||||
@@ -75,7 +75,7 @@ class Base:
|
||||
if target_dict[unit_type] == 0:
|
||||
del target_dict[unit_type]
|
||||
|
||||
def affect_strength(self, amount):
|
||||
def affect_strength(self, amount: float) -> None:
|
||||
self.strength += amount
|
||||
if self.strength > BASE_MAX_STRENGTH:
|
||||
self.strength = BASE_MAX_STRENGTH
|
||||
|
||||
@@ -5,7 +5,7 @@ import math
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.countries import (
|
||||
@@ -29,14 +29,14 @@ from dcs.terrain import (
|
||||
persiangulf,
|
||||
syria,
|
||||
thechannel,
|
||||
marianaislands,
|
||||
)
|
||||
from dcs.terrain.terrain import Airport, Terrain
|
||||
from dcs.unitgroup import (
|
||||
FlyingGroup,
|
||||
Group,
|
||||
ShipGroup,
|
||||
StaticGroup,
|
||||
VehicleGroup,
|
||||
PlaneGroup,
|
||||
)
|
||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||
from pyproj import CRS, Transformer
|
||||
@@ -56,10 +56,14 @@ from .landmap import Landmap, load_landmap, poly_contains
|
||||
from .latlon import LatLon
|
||||
from .projections import TransverseMercator
|
||||
from ..point_with_heading import PointWithHeading
|
||||
from ..positioned import Positioned
|
||||
from ..profiling import logged_duration
|
||||
from ..scenery_group import SceneryGroup
|
||||
from ..utils import Distance, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TheaterGroundObject
|
||||
|
||||
SIZE_TINY = 150
|
||||
SIZE_SMALL = 600
|
||||
SIZE_REGULAR = 1000
|
||||
@@ -181,7 +185,7 @@ class MizCampaignLoader:
|
||||
def red(self) -> Country:
|
||||
return self.country(blue=False)
|
||||
|
||||
def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
|
||||
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
|
||||
for group in self.country(blue).plane_group:
|
||||
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
||||
yield group
|
||||
@@ -305,26 +309,26 @@ class MizCampaignLoader:
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.carriers(blue):
|
||||
for ship in self.carriers(blue):
|
||||
# TODO: Name the carrier.
|
||||
control_point = Carrier(
|
||||
"carrier", group.position, next(self.control_point_id)
|
||||
"carrier", ship.position, next(self.control_point_id)
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.lhas(blue):
|
||||
for ship in self.lhas(blue):
|
||||
# TODO: Name the LHA.db
|
||||
control_point = Lha("lha", group.position, next(self.control_point_id))
|
||||
control_point = Lha("lha", ship.position, next(self.control_point_id))
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for group in self.fobs(blue):
|
||||
for fob in self.fobs(blue):
|
||||
control_point = Fob(
|
||||
str(group.name), group.position, next(self.control_point_id)
|
||||
str(fob.name), fob.position, next(self.control_point_id)
|
||||
)
|
||||
control_point.captured = blue
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
return control_points
|
||||
@@ -385,22 +389,22 @@ class MizCampaignLoader:
|
||||
origin, list(reversed(waypoints))
|
||||
)
|
||||
|
||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(group.position)
|
||||
distance = meters(closest.position.distance_to_point(group.position))
|
||||
def objective_info(self, near: Positioned) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(near.position)
|
||||
distance = meters(closest.position.distance_to_point(near.position))
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
for group in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
for static in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.offshore_strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ships:
|
||||
closest, distance = self.objective_info(group)
|
||||
for ship in self.ships:
|
||||
closest, distance = self.objective_info(ship)
|
||||
closest.preset_locations.ships.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
PointWithHeading.from_point(ship.position, ship.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.missile_sites:
|
||||
@@ -451,33 +455,33 @@ class MizCampaignLoader:
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.helipads:
|
||||
closest, distance = self.objective_info(group)
|
||||
for static in self.helipads:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.helipads.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.factories:
|
||||
closest, distance = self.objective_info(group)
|
||||
for static in self.factories:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.factories.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.ammunition_depots:
|
||||
closest, distance = self.objective_info(group)
|
||||
for static in self.ammunition_depots:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.ammunition_depots.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.strike_targets:
|
||||
closest, distance = self.objective_info(group)
|
||||
for static in self.strike_targets:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.strike_locations.append(
|
||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||
)
|
||||
|
||||
for group in self.scenery:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.scenery.append(group)
|
||||
for scenery_group in self.scenery:
|
||||
closest, distance = self.objective_info(scenery_group)
|
||||
closest.preset_locations.scenery.append(scenery_group)
|
||||
|
||||
def populate_theater(self) -> None:
|
||||
for control_point in self.control_points.values():
|
||||
@@ -504,7 +508,7 @@ class ConflictTheater:
|
||||
"""
|
||||
daytime_map: Dict[str, Tuple[int, int]]
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.controlpoints: List[ControlPoint] = []
|
||||
self.point_to_ll_transformer = Transformer.from_crs(
|
||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
||||
@@ -536,10 +540,12 @@ class ConflictTheater:
|
||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
||||
)
|
||||
|
||||
def add_controlpoint(self, point: ControlPoint):
|
||||
def add_controlpoint(self, point: ControlPoint) -> None:
|
||||
self.controlpoints.append(point)
|
||||
|
||||
def find_ground_objects_by_obj_name(self, obj_name):
|
||||
def find_ground_objects_by_obj_name(
|
||||
self, obj_name: str
|
||||
) -> list[TheaterGroundObject[Any]]:
|
||||
found = []
|
||||
for cp in self.controlpoints:
|
||||
for g in cp.ground_objects:
|
||||
@@ -581,12 +587,12 @@ class ConflictTheater:
|
||||
|
||||
return True
|
||||
|
||||
def nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point:
|
||||
def nearest_land_pos(self, near: Point, extend_dist: int = 50) -> Point:
|
||||
"""Returns the nearest point inside a land exclusion zone from point
|
||||
`extend_dist` determines how far inside the zone the point should be placed"""
|
||||
if self.is_on_land(point):
|
||||
return point
|
||||
point = geometry.Point(point.x, point.y)
|
||||
if self.is_on_land(near):
|
||||
return near
|
||||
point = geometry.Point(near.x, near.y)
|
||||
nearest_points = []
|
||||
if not self.landmap:
|
||||
raise RuntimeError("Landmap not initialized")
|
||||
@@ -698,6 +704,7 @@ class ConflictTheater:
|
||||
"Normandy": NormandyTheater,
|
||||
"The Channel": TheChannelTheater,
|
||||
"Syria": SyriaTheater,
|
||||
"MarianaIslands": MarianaIslandsTheater,
|
||||
}
|
||||
theater = theaters[data["theater"]]
|
||||
t = theater()
|
||||
@@ -856,3 +863,22 @@ class SyriaTheater(ConflictTheater):
|
||||
from .syria import PARAMETERS
|
||||
|
||||
return PARAMETERS
|
||||
|
||||
|
||||
class MarianaIslandsTheater(ConflictTheater):
|
||||
terrain = marianaislands.MarianaIslands()
|
||||
overview_image = "marianaislands.gif"
|
||||
|
||||
landmap = load_landmap("resources\\marianaislandslandmap.p")
|
||||
daytime_map = {
|
||||
"dawn": (6, 8),
|
||||
"day": (8, 16),
|
||||
"dusk": (16, 18),
|
||||
"night": (0, 5),
|
||||
}
|
||||
|
||||
@property
|
||||
def projection_parameters(self) -> TransverseMercator:
|
||||
from .marianaislands import PARAMETERS
|
||||
|
||||
return PARAMETERS
|
||||
|
||||
@@ -43,6 +43,7 @@ from .missiontarget import MissionTarget
|
||||
from .theatergroundobject import (
|
||||
GenericCarrierGroundObject,
|
||||
TheaterGroundObject,
|
||||
NavalGroundObject,
|
||||
)
|
||||
from ..dcs.aircrafttype import AircraftType
|
||||
from ..dcs.groundunittype import GroundUnitType
|
||||
@@ -290,15 +291,15 @@ class ControlPoint(MissionTarget, ABC):
|
||||
at: db.StartingPosition,
|
||||
size: int,
|
||||
importance: float,
|
||||
has_frontline=True,
|
||||
cptype=ControlPointType.AIRBASE,
|
||||
):
|
||||
has_frontline: bool = True,
|
||||
cptype: ControlPointType = ControlPointType.AIRBASE,
|
||||
) -> None:
|
||||
super().__init__(name, position)
|
||||
# TODO: Should be Airbase specific.
|
||||
self.id = cp_id
|
||||
self.full_name = name
|
||||
self.at = at
|
||||
self.connected_objectives: List[TheaterGroundObject] = []
|
||||
self.connected_objectives: List[TheaterGroundObject[Any]] = []
|
||||
self.preset_locations = PresetLocations()
|
||||
self.helipads: List[PointWithHeading] = []
|
||||
|
||||
@@ -322,11 +323,11 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
self.target_position: Optional[Point] = None
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{__class__}: {self.name}>"
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__}: {self.name}>"
|
||||
|
||||
@property
|
||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
||||
def ground_objects(self) -> List[TheaterGroundObject[Any]]:
|
||||
return list(self.connected_objectives)
|
||||
|
||||
@property
|
||||
@@ -334,11 +335,11 @@ class ControlPoint(MissionTarget, ABC):
|
||||
def heading(self) -> int:
|
||||
...
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_global(self):
|
||||
def is_global(self) -> bool:
|
||||
return not self.connected_points
|
||||
|
||||
def transitive_connected_friendly_points(
|
||||
@@ -405,21 +406,21 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_carrier(self):
|
||||
def is_carrier(self) -> bool:
|
||||
"""
|
||||
:return: Whether this control point is an aircraft carrier
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_fleet(self):
|
||||
def is_fleet(self) -> bool:
|
||||
"""
|
||||
:return: Whether this control point is a boat (mobile)
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_lha(self):
|
||||
def is_lha(self) -> bool:
|
||||
"""
|
||||
:return: Whether this control point is an LHA
|
||||
"""
|
||||
@@ -439,7 +440,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def total_aircraft_parking(self):
|
||||
def total_aircraft_parking(self) -> int:
|
||||
"""
|
||||
:return: The maximum number of aircraft that can be stored in this
|
||||
control point
|
||||
@@ -471,7 +472,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
...
|
||||
|
||||
# TODO: Should be naval specific.
|
||||
def get_carrier_group_name(self):
|
||||
def get_carrier_group_name(self) -> Optional[str]:
|
||||
"""
|
||||
Get the carrier group name if the airbase is a carrier
|
||||
:return: Carrier group name
|
||||
@@ -497,10 +498,12 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return None
|
||||
|
||||
# TODO: Should be Airbase specific.
|
||||
def is_connected(self, to) -> bool:
|
||||
def is_connected(self, to: ControlPoint) -> bool:
|
||||
return to in self.connected_points
|
||||
|
||||
def find_ground_objects_by_obj_name(self, obj_name):
|
||||
def find_ground_objects_by_obj_name(
|
||||
self, obj_name: str
|
||||
) -> list[TheaterGroundObject[Any]]:
|
||||
found = []
|
||||
for g in self.ground_objects:
|
||||
if g.obj_name == obj_name:
|
||||
@@ -522,7 +525,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
f"vehicles have been captured and sold for ${total}M."
|
||||
)
|
||||
|
||||
def retreat_ground_units(self, game: Game):
|
||||
def retreat_ground_units(self, game: Game) -> None:
|
||||
# When there are multiple valid destinations, deliver units to whichever
|
||||
# base is least defended first. The closest approximation of unit
|
||||
# strength we have is price
|
||||
@@ -748,7 +751,7 @@ class ControlPoint(MissionTarget, ABC):
|
||||
return len([obj for obj in self.connected_objectives if obj.category == "ammo"])
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||
return []
|
||||
|
||||
@property
|
||||
@@ -764,8 +767,8 @@ class ControlPoint(MissionTarget, ABC):
|
||||
|
||||
class Airfield(ControlPoint):
|
||||
def __init__(
|
||||
self, airport: Airport, size: int, importance: float, has_frontline=True
|
||||
):
|
||||
self, airport: Airport, size: int, importance: float, has_frontline: bool = True
|
||||
) -> None:
|
||||
super().__init__(
|
||||
airport.id,
|
||||
airport.name,
|
||||
@@ -879,9 +882,12 @@ class NavalControlPoint(ControlPoint, ABC):
|
||||
def heading(self) -> int:
|
||||
return 0 # TODO compute heading
|
||||
|
||||
def find_main_tgo(self) -> TheaterGroundObject:
|
||||
def find_main_tgo(self) -> GenericCarrierGroundObject:
|
||||
for g in self.ground_objects:
|
||||
if g.dcs_identifier in ["CARRIER", "LHA"]:
|
||||
if isinstance(g, GenericCarrierGroundObject) and g.dcs_identifier in [
|
||||
"CARRIER",
|
||||
"LHA",
|
||||
]:
|
||||
return g
|
||||
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
|
||||
|
||||
@@ -960,7 +966,7 @@ class Carrier(NavalControlPoint):
|
||||
raise RuntimeError("Carriers cannot be captured")
|
||||
|
||||
@property
|
||||
def is_carrier(self):
|
||||
def is_carrier(self) -> bool:
|
||||
return True
|
||||
|
||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator, List, Tuple
|
||||
from typing import Iterator, List, Tuple, Any
|
||||
|
||||
from dcs.mapping import Point
|
||||
|
||||
@@ -66,7 +66,15 @@ class FrontLine(MissionTarget):
|
||||
self.segments: List[FrontLineSegment] = [
|
||||
FrontLineSegment(a, b) for a, b in pairwise(route)
|
||||
]
|
||||
self.name = f"Front line {blue_point}/{red_point}"
|
||||
super().__init__(
|
||||
f"Front line {blue_point}/{red_point}",
|
||||
self.point_from_a(self._position_distance),
|
||||
)
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
if not hasattr(self, "position"):
|
||||
self.position = self.point_from_a(self._position_distance)
|
||||
|
||||
def control_point_hostile_to(self, player: bool) -> ControlPoint:
|
||||
if player:
|
||||
@@ -87,14 +95,6 @@ class FrontLine(MissionTarget):
|
||||
]
|
||||
yield from super().mission_types(for_player)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""
|
||||
The position where the conflict should occur
|
||||
according to the current strength of each control point.
|
||||
"""
|
||||
return self.point_from_a(self._position_distance)
|
||||
|
||||
@property
|
||||
def points(self) -> Iterator[Point]:
|
||||
yield self.segments[0].point_a
|
||||
@@ -107,12 +107,12 @@ class FrontLine(MissionTarget):
|
||||
return self.blue_cp, self.red_cp
|
||||
|
||||
@property
|
||||
def attack_distance(self):
|
||||
def attack_distance(self) -> float:
|
||||
"""The total distance of all segments"""
|
||||
return sum(i.attack_distance for i in self.segments)
|
||||
|
||||
@property
|
||||
def attack_heading(self):
|
||||
def attack_heading(self) -> float:
|
||||
"""The heading of the active attack segment from player to enemy control point"""
|
||||
return self.active_segment.attack_heading
|
||||
|
||||
@@ -149,6 +149,9 @@ class FrontLine(MissionTarget):
|
||||
)
|
||||
else:
|
||||
remaining_dist -= segment.attack_distance
|
||||
raise RuntimeError(
|
||||
f"Could not find front line point {distance} from {self.blue_cp}"
|
||||
)
|
||||
|
||||
@property
|
||||
def _position_distance(self) -> float:
|
||||
|
||||
@@ -14,7 +14,7 @@ class Landmap:
|
||||
exclusion_zones: MultiPolygon
|
||||
sea_zones: MultiPolygon
|
||||
|
||||
def __post_init__(self):
|
||||
def __post_init__(self) -> None:
|
||||
if not self.inclusion_zones.is_valid:
|
||||
raise RuntimeError("Inclusion zones not valid")
|
||||
if not self.exclusion_zones.is_valid:
|
||||
@@ -36,13 +36,5 @@ def load_landmap(filename: str) -> Optional[Landmap]:
|
||||
return None
|
||||
|
||||
|
||||
def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]):
|
||||
def poly_contains(x: float, y: float, poly: Union[MultiPolygon, Polygon]) -> bool:
|
||||
return poly.contains(geometry.Point(x, y))
|
||||
|
||||
|
||||
def poly_centroid(poly) -> Tuple[float, float]:
|
||||
x_list = [vertex[0] for vertex in poly]
|
||||
y_list = [vertex[1] for vertex in poly]
|
||||
x = sum(x_list) / len(poly)
|
||||
y = sum(y_list) / len(poly)
|
||||
return (x, y)
|
||||
|
||||
8
game/theater/marianaislands.py
Normal file
8
game/theater/marianaislands.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from game.theater.projections import TransverseMercator
|
||||
|
||||
PARAMETERS = TransverseMercator(
|
||||
central_meridian=147,
|
||||
false_easting=238417.99999989968,
|
||||
false_northing=-1491840.000000048,
|
||||
scale_factor=0.9996,
|
||||
)
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Sequence
|
||||
from typing import Iterator, TYPE_CHECKING, List, Union
|
||||
|
||||
from dcs.mapping import Point
|
||||
@@ -20,7 +21,7 @@ class MissionTarget:
|
||||
self.name = name
|
||||
self.position = position
|
||||
|
||||
def distance_to(self, other: MissionTarget) -> int:
|
||||
def distance_to(self, other: MissionTarget) -> float:
|
||||
"""Computes the distance to the given mission target."""
|
||||
return self.position.distance_to_point(other.position)
|
||||
|
||||
@@ -45,5 +46,5 @@ class MissionTarget:
|
||||
]
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||
return []
|
||||
|
||||
@@ -78,20 +78,33 @@ class GeneratorSettings:
|
||||
no_enemy_navy: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModSettings:
|
||||
a4_skyhawk: bool = False
|
||||
f22_raptor: bool = False
|
||||
hercules: bool = False
|
||||
jas39_gripen: bool = False
|
||||
su57_felon: bool = False
|
||||
frenchpack: bool = False
|
||||
high_digit_sams: bool = False
|
||||
|
||||
|
||||
class GameGenerator:
|
||||
def __init__(
|
||||
self,
|
||||
player: str,
|
||||
enemy: str,
|
||||
player: Faction,
|
||||
enemy: Faction,
|
||||
theater: ConflictTheater,
|
||||
settings: Settings,
|
||||
generator_settings: GeneratorSettings,
|
||||
mod_settings: ModSettings,
|
||||
) -> None:
|
||||
self.player = player
|
||||
self.enemy = enemy
|
||||
self.theater = theater
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
self.mod_settings = mod_settings
|
||||
|
||||
def generate(self) -> Game:
|
||||
with logged_duration("TGO population"):
|
||||
@@ -99,8 +112,8 @@ class GameGenerator:
|
||||
namegen.reset()
|
||||
self.prepare_theater()
|
||||
game = Game(
|
||||
player_name=self.player,
|
||||
enemy_name=self.enemy,
|
||||
player_faction=self.player.apply_mod_settings(self.mod_settings),
|
||||
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings),
|
||||
theater=self.theater,
|
||||
start_date=self.generator_settings.start_date,
|
||||
settings=self.settings,
|
||||
@@ -159,9 +172,9 @@ class ControlPointGroundObjectGenerator:
|
||||
@property
|
||||
def faction_name(self) -> str:
|
||||
if self.control_point.captured:
|
||||
return self.game.player_name
|
||||
return self.game.player_faction.name
|
||||
else:
|
||||
return self.game.enemy_name
|
||||
return self.game.enemy_faction.name
|
||||
|
||||
@property
|
||||
def faction(self) -> Faction:
|
||||
|
||||
@@ -2,13 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union
|
||||
from collections import Sequence
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union, Generic, TypeVar, Any
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.unitgroup import ShipGroup, VehicleGroup
|
||||
|
||||
from .. import db
|
||||
from ..data.radar_db import (
|
||||
@@ -47,7 +47,10 @@ NAME_BY_CATEGORY = {
|
||||
}
|
||||
|
||||
|
||||
class TheaterGroundObject(MissionTarget):
|
||||
GroupT = TypeVar("GroupT", ShipGroup, VehicleGroup)
|
||||
|
||||
|
||||
class TheaterGroundObject(MissionTarget, Generic[GroupT]):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -66,7 +69,7 @@ class TheaterGroundObject(MissionTarget):
|
||||
self.control_point = control_point
|
||||
self.dcs_identifier = dcs_identifier
|
||||
self.sea_object = sea_object
|
||||
self.groups: List[Group] = []
|
||||
self.groups: List[GroupT] = []
|
||||
|
||||
@property
|
||||
def is_dead(self) -> bool:
|
||||
@@ -147,7 +150,7 @@ class TheaterGroundObject(MissionTarget):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
|
||||
def _max_range_of_type(self, group: GroupT, range_type: str) -> Distance:
|
||||
if not self.might_have_aa:
|
||||
return meters(0)
|
||||
|
||||
@@ -168,13 +171,13 @@ class TheaterGroundObject(MissionTarget):
|
||||
def max_detection_range(self) -> Distance:
|
||||
return max(self.detection_range(g) for g in self.groups)
|
||||
|
||||
def detection_range(self, group: Group) -> Distance:
|
||||
def detection_range(self, group: GroupT) -> Distance:
|
||||
return self._max_range_of_type(group, "detection_range")
|
||||
|
||||
def max_threat_range(self) -> Distance:
|
||||
return max(self.threat_range(g) for g in self.groups)
|
||||
|
||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
||||
def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance:
|
||||
return self._max_range_of_type(group, "threat_range")
|
||||
|
||||
@property
|
||||
@@ -187,7 +190,7 @@ class TheaterGroundObject(MissionTarget):
|
||||
return False
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
||||
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||
return self.units
|
||||
|
||||
@property
|
||||
@@ -206,7 +209,7 @@ class TheaterGroundObject(MissionTarget):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BuildingGroundObject(TheaterGroundObject):
|
||||
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -217,7 +220,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
heading: int,
|
||||
control_point: ControlPoint,
|
||||
dcs_identifier: str,
|
||||
is_fob_structure=False,
|
||||
is_fob_structure: bool = False,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
@@ -253,7 +256,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
||||
def kill(self) -> None:
|
||||
self._dead = True
|
||||
|
||||
def iter_building_group(self) -> Iterator[TheaterGroundObject]:
|
||||
def iter_building_group(self) -> Iterator[TheaterGroundObject[Any]]:
|
||||
for tgo in self.control_point.ground_objects:
|
||||
if tgo.obj_name == self.obj_name and not tgo.is_dead:
|
||||
yield tgo
|
||||
@@ -338,7 +341,7 @@ class FactoryGroundObject(BuildingGroundObject):
|
||||
)
|
||||
|
||||
|
||||
class NavalGroundObject(TheaterGroundObject):
|
||||
class NavalGroundObject(TheaterGroundObject[ShipGroup]):
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
@@ -407,7 +410,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||
|
||||
|
||||
class MissileSiteGroundObject(TheaterGroundObject):
|
||||
class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
def __init__(
|
||||
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||
) -> None:
|
||||
@@ -431,14 +434,14 @@ class MissileSiteGroundObject(TheaterGroundObject):
|
||||
return False
|
||||
|
||||
|
||||
class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
group_id: int,
|
||||
position: Point,
|
||||
control_point: ControlPoint,
|
||||
heading,
|
||||
heading: int,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=name,
|
||||
@@ -460,10 +463,10 @@ class CoastalSiteGroundObject(TheaterGroundObject):
|
||||
return False
|
||||
|
||||
|
||||
# TODO: Differentiate types.
|
||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
||||
# be split into their own types.
|
||||
class SamGroundObject(TheaterGroundObject):
|
||||
# The SamGroundObject represents all type of AA
|
||||
# The TGO can have multiple types of units (AAA,SAM,Support...)
|
||||
# Differentiation can be made during generation with the airdefensegroupgenerator
|
||||
class SamGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -481,18 +484,6 @@ class SamGroundObject(TheaterGroundObject):
|
||||
dcs_identifier="AA",
|
||||
sea_object=False,
|
||||
)
|
||||
# Set by the SAM unit generator if the generated group is compatible
|
||||
# with Skynet.
|
||||
self.skynet_capable = False
|
||||
|
||||
@property
|
||||
def group_name(self) -> str:
|
||||
if self.skynet_capable:
|
||||
# Prefix the group names of SAM sites with the side color so Skynet
|
||||
# can find them.
|
||||
return f"{self.faction_color}|SAM|{self.group_id}"
|
||||
else:
|
||||
return super().group_name
|
||||
|
||||
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||
from gen.flights.flight import FlightType
|
||||
@@ -506,33 +497,25 @@ class SamGroundObject(TheaterGroundObject):
|
||||
def might_have_aa(self) -> bool:
|
||||
return True
|
||||
|
||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
||||
def threat_range(self, group: VehicleGroup, radar_only: bool = False) -> Distance:
|
||||
max_non_radar = meters(0)
|
||||
live_trs = set()
|
||||
max_telar_range = meters(0)
|
||||
launchers = set()
|
||||
for unit in group.units:
|
||||
unit_type = db.unit_type_from_name(unit.type)
|
||||
if unit_type is None or not issubclass(unit_type, VehicleType):
|
||||
continue
|
||||
unit_type = db.vehicle_type_from_name(unit.type)
|
||||
if unit_type in TRACK_RADARS:
|
||||
live_trs.add(unit_type)
|
||||
elif unit_type in TELARS:
|
||||
max_telar_range = max(
|
||||
max_telar_range, meters(getattr(unit_type, "threat_range", 0))
|
||||
)
|
||||
max_telar_range = max(max_telar_range, meters(unit_type.threat_range))
|
||||
elif unit_type in LAUNCHER_TRACKER_PAIRS:
|
||||
launchers.add(unit_type)
|
||||
else:
|
||||
max_non_radar = max(
|
||||
max_non_radar, meters(getattr(unit_type, "threat_range", 0))
|
||||
)
|
||||
max_non_radar = max(max_non_radar, meters(unit_type.threat_range))
|
||||
max_tel_range = meters(0)
|
||||
for launcher in launchers:
|
||||
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
|
||||
max_tel_range = max(
|
||||
max_tel_range, meters(getattr(launcher, "threat_range"))
|
||||
)
|
||||
max_tel_range = max(max_tel_range, meters(unit_type.threat_range))
|
||||
if radar_only:
|
||||
return max(max_tel_range, max_telar_range)
|
||||
else:
|
||||
@@ -547,7 +530,7 @@ class SamGroundObject(TheaterGroundObject):
|
||||
return True
|
||||
|
||||
|
||||
class VehicleGroupGroundObject(TheaterGroundObject):
|
||||
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -575,7 +558,7 @@ class VehicleGroupGroundObject(TheaterGroundObject):
|
||||
return True
|
||||
|
||||
|
||||
class EwrGroundObject(TheaterGroundObject):
|
||||
class EwrGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
|
||||
@@ -27,7 +27,10 @@ ThreatPoly = Union[MultiPolygon, Polygon]
|
||||
|
||||
class ThreatZones:
|
||||
def __init__(
|
||||
self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
|
||||
self,
|
||||
airbases: ThreatPoly,
|
||||
air_defenses: ThreatPoly,
|
||||
radar_sam_threats: ThreatPoly,
|
||||
) -> None:
|
||||
self.airbases = airbases
|
||||
self.air_defenses = air_defenses
|
||||
@@ -44,8 +47,10 @@ class ThreatZones:
|
||||
boundary = self.closest_boundary(point)
|
||||
return meters(boundary.distance_to_point(point))
|
||||
|
||||
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||
# definitions. The implementation methods are all typed, so should be fine.
|
||||
@singledispatchmethod
|
||||
def threatened(self, position) -> bool:
|
||||
def threatened(self, position) -> bool: # type: ignore
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened.register
|
||||
@@ -61,8 +66,10 @@ class ThreatZones:
|
||||
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
|
||||
)
|
||||
|
||||
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||
# definitions. The implementation methods are all typed, so should be fine.
|
||||
@singledispatchmethod
|
||||
def threatened_by_aircraft(self, target) -> bool:
|
||||
def threatened_by_aircraft(self, target) -> bool: # type: ignore
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_aircraft.register
|
||||
@@ -82,8 +89,10 @@ class ThreatZones:
|
||||
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
|
||||
)
|
||||
|
||||
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||
# definitions. The implementation methods are all typed, so should be fine.
|
||||
@singledispatchmethod
|
||||
def threatened_by_air_defense(self, target) -> bool:
|
||||
def threatened_by_air_defense(self, target) -> bool: # type: ignore
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_air_defense.register
|
||||
@@ -102,8 +111,10 @@ class ThreatZones:
|
||||
self.dcs_to_shapely_point(target.position)
|
||||
)
|
||||
|
||||
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||
# definitions. The implementation methods are all typed, so should be fine.
|
||||
@singledispatchmethod
|
||||
def threatened_by_radar_sam(self, target) -> bool:
|
||||
def threatened_by_radar_sam(self, target) -> bool: # type: ignore
|
||||
raise NotImplementedError
|
||||
|
||||
@threatened_by_radar_sam.register
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from functools import singledispatchmethod
|
||||
from typing import (
|
||||
Dict,
|
||||
Generic,
|
||||
Iterator,
|
||||
List,
|
||||
@@ -72,10 +71,18 @@ class TransferOrder:
|
||||
player: bool = field(init=False)
|
||||
|
||||
#: The units being transferred.
|
||||
units: Dict[GroundUnitType, int]
|
||||
units: dict[GroundUnitType, int]
|
||||
|
||||
transport: Optional[Transport] = field(default=None)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Returns the text that should be displayed for the transfer."""
|
||||
count = self.size
|
||||
origin = self.origin.name
|
||||
destination = self.destination.name
|
||||
description = "Transfer" if self.player else "Enemy transfer"
|
||||
return f"{description} of {count} units from {origin} to {destination}"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.position = self.origin
|
||||
self.player = self.origin.is_friendly(to_player=True)
|
||||
@@ -91,12 +98,15 @@ class TransferOrder:
|
||||
|
||||
def kill_unit(self, unit_type: GroundUnitType) -> None:
|
||||
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
|
||||
raise KeyError(f"{self} has no {unit_type} remaining")
|
||||
if self.units[unit_type] == 1:
|
||||
del self.units[unit_type]
|
||||
else:
|
||||
self.units[unit_type] -= 1
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(c for c in self.units.values())
|
||||
return sum(self.units.values())
|
||||
|
||||
def iter_units(self) -> Iterator[GroundUnitType]:
|
||||
for unit_type, count in self.units.items():
|
||||
@@ -105,7 +115,7 @@ class TransferOrder:
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self.destination == self.position or not self.units
|
||||
return self.destination == self.position or not self.size
|
||||
|
||||
def disband_at(self, location: ControlPoint) -> None:
|
||||
logging.info(f"Units halting at {location}.")
|
||||
@@ -120,22 +130,64 @@ class TransferOrder:
|
||||
)
|
||||
return self.transport.destination
|
||||
|
||||
def proceed(self) -> None:
|
||||
if self.transport is None:
|
||||
return
|
||||
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||
if self.transport is not None:
|
||||
return self.transport.find_escape_route()
|
||||
return None
|
||||
|
||||
if not self.destination.is_friendly(self.player):
|
||||
logging.info(f"Transfer destination {self.destination} was captured.")
|
||||
if self.position.is_friendly(self.player):
|
||||
self.disband_at(self.position)
|
||||
elif (escape_route := self.transport.find_escape_route()) is not None:
|
||||
self.disband_at(escape_route)
|
||||
else:
|
||||
def disband(self) -> None:
|
||||
"""
|
||||
Disbands the specific transfer at the current position if friendly, at a
|
||||
possible escape route or kills all units if none is possible
|
||||
"""
|
||||
if self.position.is_friendly(self.player):
|
||||
self.disband_at(self.position)
|
||||
elif (escape_route := self.find_escape_route()) is not None:
|
||||
self.disband_at(escape_route)
|
||||
else:
|
||||
logging.info(
|
||||
f"No escape route available. Units were surrounded and destroyed "
|
||||
"during transfer."
|
||||
)
|
||||
self.kill_all()
|
||||
|
||||
def is_completable(self, network: TransitNetwork) -> bool:
|
||||
"""
|
||||
Checks if the transfer can be completed with the current theater state / transit
|
||||
network to ensure that there is possible route between the current position and
|
||||
the planned destination. This also ensures that the points are friendly.
|
||||
"""
|
||||
if self.transport is None:
|
||||
# Check if unplanned transfers could be completed
|
||||
if not self.position.is_friendly(self.player):
|
||||
logging.info(
|
||||
f"No escape route available. Units were surrounded and destroyed "
|
||||
"during transfer."
|
||||
f"Current position ({self.position}) "
|
||||
f"of the halting transfer was captured."
|
||||
)
|
||||
self.kill_all()
|
||||
return False
|
||||
if not network.has_path_between(self.position, self.destination):
|
||||
logging.info(
|
||||
f"Destination of transfer ({self.destination}) "
|
||||
f"can not be reached anymore."
|
||||
)
|
||||
return False
|
||||
|
||||
if self.transport is not None and not self.next_stop.is_friendly(self.player):
|
||||
# check if already proceeding transfers can reach the next stop
|
||||
logging.info(
|
||||
f"The next stop of the transfer ({self.next_stop}) "
|
||||
f"was captured while transfer was on route."
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def proceed(self) -> None:
|
||||
"""
|
||||
Let the transfer proceed to the next stop and disbands it if the next stop
|
||||
is the destination
|
||||
"""
|
||||
if self.transport is None:
|
||||
return
|
||||
|
||||
self.position = self.next_stop
|
||||
@@ -156,7 +208,7 @@ class Airlift(Transport):
|
||||
self.flight = flight
|
||||
|
||||
@property
|
||||
def units(self) -> Dict[GroundUnitType, int]:
|
||||
def units(self) -> dict[GroundUnitType, int]:
|
||||
return self.transfer.units
|
||||
|
||||
@property
|
||||
@@ -261,8 +313,12 @@ class AirliftPlanner:
|
||||
required,
|
||||
available_aircraft,
|
||||
squadron.aircraft.dcs_unit_type.group_size_max,
|
||||
squadron.number_of_available_pilots,
|
||||
)
|
||||
# TODO: Use number_of_available_pilots directly once feature flag is gone.
|
||||
# The number of currently available pilots is not relevant when pilot limits
|
||||
# are disabled.
|
||||
if not squadron.can_provide_pilots(flight_size):
|
||||
flight_size = squadron.number_of_available_pilots
|
||||
capacity = flight_size * capacity_each
|
||||
|
||||
if capacity < self.transfer.size:
|
||||
@@ -334,11 +390,11 @@ class MultiGroupTransport(MissionTarget, Transport):
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum(sum(t.units.values()) for t in self.transfers)
|
||||
return sum(t.size for t in self.transfers)
|
||||
|
||||
@property
|
||||
def units(self) -> dict[GroundUnitType, int]:
|
||||
units: Dict[GroundUnitType, int] = defaultdict(int)
|
||||
units: dict[GroundUnitType, int] = defaultdict(int)
|
||||
for transfer in self.transfers:
|
||||
for unit_type, count in transfer.units.items():
|
||||
units[unit_type] += count
|
||||
@@ -414,8 +470,8 @@ TransportType = TypeVar("TransportType", bound=MultiGroupTransport)
|
||||
class TransportMap(Generic[TransportType]):
|
||||
def __init__(self) -> None:
|
||||
# Dict of origin -> destination -> transport.
|
||||
self.transports: Dict[
|
||||
ControlPoint, Dict[ControlPoint, TransportType]
|
||||
self.transports: dict[
|
||||
ControlPoint, dict[ControlPoint, TransportType]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def create_transport(
|
||||
@@ -469,14 +525,14 @@ class TransportMap(Generic[TransportType]):
|
||||
yield from destination_dict.values()
|
||||
|
||||
|
||||
class ConvoyMap(TransportMap):
|
||||
class ConvoyMap(TransportMap[Convoy]):
|
||||
def create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> Convoy:
|
||||
return Convoy(origin, destination)
|
||||
|
||||
|
||||
class CargoShipMap(TransportMap):
|
||||
class CargoShipMap(TransportMap[CargoShip]):
|
||||
def create_transport(
|
||||
self, origin: ControlPoint, destination: ControlPoint
|
||||
) -> CargoShip:
|
||||
@@ -542,8 +598,14 @@ class PendingTransfers:
|
||||
self.pending_transfers.append(new_transfer)
|
||||
return new_transfer
|
||||
|
||||
# Type checking ignored because singledispatchmethod doesn't work with required type
|
||||
# definitions. The implementation methods are all typed, so should be fine.
|
||||
@singledispatchmethod
|
||||
def cancel_transport(self, transport, transfer: TransferOrder) -> None:
|
||||
def cancel_transport( # type: ignore
|
||||
self,
|
||||
transport,
|
||||
transfer: TransferOrder,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@cancel_transport.register
|
||||
@@ -576,6 +638,12 @@ class PendingTransfers:
|
||||
transfer.origin.base.commission_units(transfer.units)
|
||||
|
||||
def perform_transfers(self) -> None:
|
||||
"""
|
||||
Performs completable transfers from the list of pending transfers and adds
|
||||
uncompleted transfers which are en route back to the list of pending transfers.
|
||||
Disbands all convoys and cargo ships
|
||||
"""
|
||||
self.disband_uncompletable_transfers()
|
||||
incomplete = []
|
||||
for transfer in self.pending_transfers:
|
||||
transfer.proceed()
|
||||
@@ -586,17 +654,71 @@ class PendingTransfers:
|
||||
self.cargo_ships.disband_all()
|
||||
|
||||
def plan_transports(self) -> None:
|
||||
"""
|
||||
Plan transports for all pending and completable transfers which don't have a
|
||||
transport assigned already. This calculates the shortest path between current
|
||||
position and destination on every execution to ensure the route is adopted to
|
||||
recent changes in the theater state / transit network.
|
||||
"""
|
||||
self.disband_uncompletable_transfers()
|
||||
for transfer in self.pending_transfers:
|
||||
if transfer.transport is None:
|
||||
self.arrange_transport(transfer)
|
||||
|
||||
def disband_uncompletable_transfers(self) -> None:
|
||||
"""
|
||||
Disbands all transfers from the list of pending_transfers which can not be
|
||||
completed anymore because the theater state changed or the transit network does
|
||||
not allow a route to the destination anymore
|
||||
"""
|
||||
completable_transfers = []
|
||||
for transfer in self.pending_transfers:
|
||||
if not transfer.is_completable(self.network_for(transfer.position)):
|
||||
transfer.disband()
|
||||
else:
|
||||
completable_transfers.append(transfer)
|
||||
self.pending_transfers = completable_transfers
|
||||
|
||||
def order_airlift_assets(self) -> None:
|
||||
for control_point in self.game.theater.controlpoints:
|
||||
self.order_airlift_assets_at(control_point)
|
||||
if self.game.air_wing_for(control_point.captured).can_auto_plan(
|
||||
FlightType.TRANSPORT
|
||||
):
|
||||
self.order_airlift_assets_at(control_point)
|
||||
|
||||
@staticmethod
|
||||
def desired_airlift_capacity(control_point: ControlPoint) -> int:
|
||||
return 4 if control_point.has_factory else 0
|
||||
def desired_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||
|
||||
if control_point.has_factory:
|
||||
is_major_hub = control_point.total_aircraft_parking > 0
|
||||
# Check if there is a CP which is only reachable via Airlift
|
||||
transit_network = self.network_for(control_point)
|
||||
for cp in self.game.theater.control_points_for(control_point.captured):
|
||||
# check if the CP has no factory, is reachable from the current
|
||||
# position and can only be reached with airlift connections
|
||||
if (
|
||||
cp.can_deploy_ground_units
|
||||
and not cp.has_factory
|
||||
and transit_network.has_link(control_point, cp)
|
||||
and not any(
|
||||
link_type
|
||||
for link, link_type in transit_network.nodes[cp].items()
|
||||
if not link_type == TransitConnection.Airlift
|
||||
)
|
||||
):
|
||||
return 4
|
||||
|
||||
if (
|
||||
is_major_hub
|
||||
and cp.has_factory
|
||||
and cp.total_aircraft_parking > control_point.total_aircraft_parking
|
||||
):
|
||||
is_major_hub = False
|
||||
|
||||
if is_major_hub:
|
||||
# If the current CP is a major hub keep always 2 planes on reserve
|
||||
return 2
|
||||
|
||||
return 0
|
||||
|
||||
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||
inventory = self.game.aircraft_inventory.for_control_point(control_point)
|
||||
@@ -611,9 +733,16 @@ class PendingTransfers:
|
||||
)
|
||||
|
||||
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
||||
gap = self.desired_airlift_capacity(
|
||||
control_point
|
||||
) - self.current_airlift_capacity(control_point)
|
||||
unclaimed_parking = control_point.unclaimed_parking(self.game)
|
||||
# Buy a maximum of unclaimed_parking only to prevent that aircraft procurement
|
||||
# take place at another base
|
||||
gap = min(
|
||||
[
|
||||
self.desired_airlift_capacity(control_point)
|
||||
- self.current_airlift_capacity(control_point),
|
||||
unclaimed_parking,
|
||||
]
|
||||
)
|
||||
|
||||
if gap <= 0:
|
||||
return
|
||||
@@ -623,6 +752,10 @@ class PendingTransfers:
|
||||
# aesthetic.
|
||||
gap += 1
|
||||
|
||||
if gap > unclaimed_parking:
|
||||
# Prevent to buy more aircraft than possible
|
||||
return
|
||||
|
||||
self.game.procurement_requests_for(player=control_point.captured).append(
|
||||
AircraftProcurementRequest(
|
||||
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, TYPE_CHECKING, Any
|
||||
from typing import Optional, TYPE_CHECKING, Any
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from .dcs.groundunittype import GroundUnitType
|
||||
@@ -28,37 +28,47 @@ class PendingUnitDeliveries:
|
||||
self.destination = destination
|
||||
|
||||
# Maps unit type to order quantity.
|
||||
self.units: Dict[UnitType, int] = defaultdict(int)
|
||||
self.units: dict[UnitType[Any], int] = defaultdict(int)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Pending delivery to {self.destination}"
|
||||
|
||||
def order(self, units: Dict[UnitType, int]) -> None:
|
||||
def order(self, units: dict[UnitType[Any], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] += v
|
||||
|
||||
def sell(self, units: Dict[UnitType, int]) -> None:
|
||||
def sell(self, units: dict[UnitType[Any], int]) -> None:
|
||||
for k, v in units.items():
|
||||
self.units[k] -= v
|
||||
if self.units[k] == 0:
|
||||
del self.units[k]
|
||||
|
||||
def refund_all(self, game: Game) -> None:
|
||||
self.refund(game, self.units)
|
||||
self.units = defaultdict(int)
|
||||
|
||||
def refund(self, game: Game, units: Dict[UnitType, int]) -> None:
|
||||
def refund_ground_units(self, game: Game) -> None:
|
||||
ground_units: dict[UnitType[Any], int] = {
|
||||
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
|
||||
}
|
||||
self.refund(game, ground_units)
|
||||
for gu in ground_units.keys():
|
||||
del self.units[gu]
|
||||
|
||||
def refund(self, game: Game, units: dict[UnitType[Any], int]) -> None:
|
||||
for unit_type, count in units.items():
|
||||
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
||||
game.adjust_budget(
|
||||
unit_type.price * count, player=self.destination.captured
|
||||
)
|
||||
|
||||
def pending_orders(self, unit_type: UnitType) -> int:
|
||||
def pending_orders(self, unit_type: UnitType[Any]) -> int:
|
||||
pending_units = self.units.get(unit_type)
|
||||
if pending_units is None:
|
||||
pending_units = 0
|
||||
return pending_units
|
||||
|
||||
def available_next_turn(self, unit_type: UnitType) -> int:
|
||||
def available_next_turn(self, unit_type: UnitType[Any]) -> int:
|
||||
current_units = self.destination.base.total_units_of_type(unit_type)
|
||||
return self.pending_orders(unit_type) + current_units
|
||||
|
||||
@@ -69,12 +79,11 @@ class PendingUnitDeliveries:
|
||||
f"{self.destination.name} lost its source for ground unit "
|
||||
"reinforcements. Refunding purchase price."
|
||||
)
|
||||
self.refund_all(game)
|
||||
return
|
||||
self.refund_ground_units(game)
|
||||
|
||||
bought_units: Dict[UnitType, int] = {}
|
||||
units_needing_transfer: Dict[GroundUnitType, int] = {}
|
||||
sold_units: Dict[UnitType, int] = {}
|
||||
bought_units: dict[UnitType[Any], int] = {}
|
||||
units_needing_transfer: dict[GroundUnitType, int] = {}
|
||||
sold_units: dict[UnitType[Any], int] = {}
|
||||
for unit_type, count in self.units.items():
|
||||
coalition = "Ally" if self.destination.captured else "Enemy"
|
||||
d: dict[Any, int]
|
||||
@@ -102,11 +111,16 @@ class PendingUnitDeliveries:
|
||||
self.destination.base.commit_losses(sold_units)
|
||||
|
||||
if units_needing_transfer:
|
||||
if ground_unit_source is None:
|
||||
raise RuntimeError(
|
||||
f"ground unit source could not be found for {self.destination} but still tried to "
|
||||
f"transfer units to there"
|
||||
)
|
||||
ground_unit_source.base.commission_units(units_needing_transfer)
|
||||
self.create_transfer(game, ground_unit_source, units_needing_transfer)
|
||||
|
||||
def create_transfer(
|
||||
self, game: Game, source: ControlPoint, units: Dict[GroundUnitType, int]
|
||||
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int]
|
||||
) -> None:
|
||||
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import itertools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, Any, Union, TypeVar, Generic
|
||||
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
||||
from dcs.unit import Vehicle, Ship
|
||||
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.squadrons import Pilot
|
||||
@@ -27,11 +27,14 @@ class FrontLineUnit:
|
||||
origin: ControlPoint
|
||||
|
||||
|
||||
UnitT = TypeVar("UnitT", Ship, Vehicle)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundObjectUnit:
|
||||
ground_object: TheaterGroundObject
|
||||
group: Group
|
||||
unit: Unit
|
||||
class GroundObjectUnit(Generic[UnitT]):
|
||||
ground_object: TheaterGroundObject[Any]
|
||||
group: MovingGroup[UnitT]
|
||||
unit: UnitT
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -56,13 +59,13 @@ class UnitMap:
|
||||
self.aircraft: Dict[str, FlyingUnit] = {}
|
||||
self.airfields: Dict[str, Airfield] = {}
|
||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
||||
self.ground_object_units: Dict[str, GroundObjectUnit[Any]] = {}
|
||||
self.buildings: Dict[str, Building] = {}
|
||||
self.convoys: Dict[str, ConvoyUnit] = {}
|
||||
self.cargo_ships: Dict[str, CargoShip] = {}
|
||||
self.airlifts: Dict[str, AirliftUnits] = {}
|
||||
|
||||
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
def add_aircraft(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
for pilot, unit in zip(flight.roster.pilots, group.units):
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
@@ -85,7 +88,7 @@ class UnitMap:
|
||||
return self.airfields.get(name, None)
|
||||
|
||||
def add_front_line_units(
|
||||
self, group: Group, origin: ControlPoint, unit_type: GroundUnitType
|
||||
self, group: VehicleGroup, origin: ControlPoint, unit_type: GroundUnitType
|
||||
) -> None:
|
||||
for unit in group.units:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
@@ -100,9 +103,9 @@ class UnitMap:
|
||||
|
||||
def add_ground_object_units(
|
||||
self,
|
||||
ground_object: TheaterGroundObject,
|
||||
persistence_group: Group,
|
||||
miz_group: Group,
|
||||
ground_object: TheaterGroundObject[Any],
|
||||
persistence_group: Union[ShipGroup, VehicleGroup],
|
||||
miz_group: Union[ShipGroup, VehicleGroup],
|
||||
) -> None:
|
||||
"""Adds a group associated with a TGO to the unit map.
|
||||
|
||||
@@ -131,10 +134,10 @@ class UnitMap:
|
||||
ground_object, persistence_group, persistent_unit
|
||||
)
|
||||
|
||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
|
||||
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit[Any]]:
|
||||
return self.ground_object_units.get(name, None)
|
||||
|
||||
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
|
||||
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
|
||||
for unit, unit_type in zip(group.units, convoy.iter_units()):
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
@@ -146,7 +149,7 @@ class UnitMap:
|
||||
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
||||
return self.convoys.get(name, None)
|
||||
|
||||
def add_cargo_ship(self, group: Group, ship: CargoShip) -> None:
|
||||
def add_cargo_ship(self, group: ShipGroup, ship: CargoShip) -> None:
|
||||
if len(group.units) > 1:
|
||||
# Cargo ship "groups" are single units. Killing the one ship kills the whole
|
||||
# transfer. If we ever want to add escorts or create multiple cargo ships in
|
||||
@@ -163,7 +166,9 @@ class UnitMap:
|
||||
def cargo_ship(self, name: str) -> Optional[CargoShip]:
|
||||
return self.cargo_ships.get(name, None)
|
||||
|
||||
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
|
||||
def add_airlift_units(
|
||||
self, group: FlyingGroup[Any], transfer: TransferOrder
|
||||
) -> None:
|
||||
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
|
||||
@@ -186,7 +191,9 @@ class UnitMap:
|
||||
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
|
||||
return self.airlifts.get(name, None)
|
||||
|
||||
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
|
||||
def add_building(
|
||||
self, ground_object: BuildingGroundObject, group: StaticGroup
|
||||
) -> None:
|
||||
# The actual name is a String (the pydcs translatable string), which
|
||||
# doesn't define __eq__.
|
||||
# The name of the initiator in the DCS dead event will have " object"
|
||||
|
||||
@@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import math
|
||||
from collections import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import Union
|
||||
from typing import Union, Any
|
||||
|
||||
METERS_TO_FEET = 3.28084
|
||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||
@@ -16,17 +17,12 @@ MS_TO_KPH = 3.6
|
||||
KPH_TO_MS = 1 / MS_TO_KPH
|
||||
|
||||
|
||||
def heading_sum(h, a) -> int:
|
||||
def heading_sum(h: int, a: int) -> int:
|
||||
h += a
|
||||
if h > 360:
|
||||
return h - 360
|
||||
elif h < 0:
|
||||
return 360 + h
|
||||
else:
|
||||
return h
|
||||
return h % 360
|
||||
|
||||
|
||||
def opposite_heading(h):
|
||||
def opposite_heading(h: int) -> int:
|
||||
return heading_sum(h, 180)
|
||||
|
||||
|
||||
@@ -185,7 +181,7 @@ def mach(value: float, altitude: Distance) -> Speed:
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
||||
|
||||
|
||||
def pairwise(iterable):
|
||||
def pairwise(iterable: Iterable[Any]) -> Iterable[tuple[Any, Any]]:
|
||||
"""
|
||||
itertools recipe
|
||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MAJOR_VERSION = 4
|
||||
MINOR_VERSION = 1
|
||||
MICRO_VERSION = 2
|
||||
|
||||
|
||||
def _build_version_string() -> str:
|
||||
components = ["4.0.0"]
|
||||
components = [
|
||||
".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
|
||||
]
|
||||
build_number_path = Path("resources/buildnumber")
|
||||
if build_number_path.exists():
|
||||
with build_number_path.open("r") as build_number_file:
|
||||
with build_number_path.open("r", encoding="utf-8") as build_number_file:
|
||||
components.append(build_number_file.readline())
|
||||
|
||||
if not Path("resources/final").exists():
|
||||
@@ -90,4 +97,17 @@ VERSION = _build_version_string()
|
||||
#:
|
||||
#: Version 6.1
|
||||
#: * Support for new Syrian airfields in DCS 2.7.2.7910.1 (Cyprus update).
|
||||
CAMPAIGN_FORMAT_VERSION = (6, 1)
|
||||
#:
|
||||
#: Version 7.0
|
||||
#: * DCS 2.7.2.7910.1 (Cyprus update) changed the IDs of scenery strike targets. Any
|
||||
#: mission using map buildings as strike targets must check and potentially recreate
|
||||
#: all those objectives. This definitely affects all Syria campaigns, other maps are
|
||||
#: not yet verified.
|
||||
#:
|
||||
#: Version 7.1
|
||||
#: * Support for Mariana Islands terrain
|
||||
#:
|
||||
#: Version 8.0
|
||||
#: * DCS 2.7.4.9632 changed scenery target IDs. Any mission using map buildings as
|
||||
#: strike targets must check and potentially recreate all those objectives.
|
||||
CAMPAIGN_FORMAT_VERSION = (8, 0)
|
||||
|
||||
@@ -83,7 +83,7 @@ class Weather:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def random_wind(minimum: int, maximum) -> WindConditions:
|
||||
def random_wind(minimum: int, maximum: int) -> WindConditions:
|
||||
wind_direction = random.randint(0, 360)
|
||||
at_0m_factor = 1
|
||||
at_2000m_factor = 2
|
||||
|
||||
169
gen/aircraft.py
169
gen/aircraft.py
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
from game.savecompat import has_save_compat_for
|
||||
|
||||
import logging
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import cached_property
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable, Any
|
||||
|
||||
from dcs import helicopters
|
||||
from dcs.action import AITaskPush, ActivateGroup
|
||||
@@ -26,7 +27,6 @@ from dcs.planes import (
|
||||
Su_33,
|
||||
Tu_22M3,
|
||||
)
|
||||
from dcs.planes import IL_78M
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
@@ -66,7 +66,6 @@ from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game import db
|
||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
||||
from game.data.weapons import Pylon
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.factions.faction import Faction
|
||||
@@ -83,7 +82,7 @@ from game.theater.missiontarget import MissionTarget
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from game.transfers import MultiGroupTransport
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.utils import Distance, kph, meters, nautical_miles
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.callsigns import create_group_callsign_from_unit
|
||||
from gen.flights.flight import (
|
||||
@@ -323,7 +322,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
@staticmethod
|
||||
def livery_from_db(flight: Flight) -> Optional[str]:
|
||||
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type)
|
||||
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type.dcs_unit_type)
|
||||
|
||||
def livery_from_faction(self, flight: Flight) -> Optional[str]:
|
||||
faction = self.game.faction_for(player=flight.departure.captured)
|
||||
@@ -344,7 +343,7 @@ class AircraftConflictGenerator:
|
||||
return livery
|
||||
return None
|
||||
|
||||
def _setup_livery(self, flight: Flight, group: FlyingGroup) -> None:
|
||||
def _setup_livery(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
||||
livery = self.livery_for(flight)
|
||||
if livery is None:
|
||||
return
|
||||
@@ -353,7 +352,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def _setup_group(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -385,7 +384,18 @@ class AircraftConflictGenerator:
|
||||
channel = self.radio_registry.alloc_uhf()
|
||||
else:
|
||||
channel = flight.unit_type.alloc_flight_radio(self.radio_registry)
|
||||
group.set_frequency(channel.mhz)
|
||||
|
||||
try:
|
||||
group.set_frequency(channel.mhz)
|
||||
except TypeError:
|
||||
# TODO: Remote try/except when pydcs bug is fixed.
|
||||
# https://github.com/pydcs/dcs/issues/175
|
||||
# pydcs now emits an error when attempting to set a preset channel for an
|
||||
# aircraft that doesn't support them. We're not choosing to set a preset
|
||||
# here, we're just trying to set the AI's frequency. pydcs automatically
|
||||
# tries to set channel 1 when it does that and doesn't suppress this new
|
||||
# error.
|
||||
pass
|
||||
|
||||
divert = None
|
||||
if flight.divert is not None:
|
||||
@@ -460,8 +470,8 @@ class AircraftConflictGenerator:
|
||||
unit_type: Type[FlyingType],
|
||||
count: int,
|
||||
start_type: str,
|
||||
airport: Optional[Airport] = None,
|
||||
) -> FlyingGroup:
|
||||
airport: Airport,
|
||||
) -> FlyingGroup[Any]:
|
||||
assert count > 0
|
||||
|
||||
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
||||
@@ -478,7 +488,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def _generate_inflight(
|
||||
self, name: str, side: Country, flight: Flight, origin: ControlPoint
|
||||
) -> FlyingGroup:
|
||||
) -> FlyingGroup[Any]:
|
||||
assert flight.count > 0
|
||||
at = origin.position
|
||||
|
||||
@@ -523,7 +533,7 @@ class AircraftConflictGenerator:
|
||||
count: int,
|
||||
start_type: str,
|
||||
at: Union[ShipGroup, StaticGroup],
|
||||
) -> FlyingGroup:
|
||||
) -> FlyingGroup[Any]:
|
||||
assert count > 0
|
||||
|
||||
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
||||
@@ -538,34 +548,18 @@ class AircraftConflictGenerator:
|
||||
)
|
||||
|
||||
def _add_radio_waypoint(
|
||||
self, group: FlyingGroup, position, altitude: Distance, airspeed: int = 600
|
||||
self,
|
||||
group: FlyingGroup[Any],
|
||||
position: Point,
|
||||
altitude: Distance,
|
||||
airspeed: int = 600,
|
||||
) -> MovingPoint:
|
||||
point = group.add_waypoint(position, altitude.meters, airspeed)
|
||||
point.alt_type = "RADIO"
|
||||
return point
|
||||
|
||||
def _rtb_for(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
cp: ControlPoint,
|
||||
at: Optional[db.StartingPosition] = None,
|
||||
):
|
||||
if at is None:
|
||||
at = cp.at
|
||||
position = at if isinstance(at, Point) else at.position
|
||||
|
||||
last_waypoint = group.points[-1]
|
||||
if last_waypoint is not None:
|
||||
heading = position.heading_between_point(last_waypoint.position)
|
||||
tod_location = position.point_from_heading(heading, RTB_DISTANCE)
|
||||
self._add_radio_waypoint(group, tod_location, last_waypoint.alt)
|
||||
|
||||
destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE)
|
||||
if isinstance(at, Airport):
|
||||
group.land_at(at)
|
||||
return destination_waypoint
|
||||
|
||||
def _at_position(self, at) -> Point:
|
||||
@staticmethod
|
||||
def _at_position(at: Union[Point, ShipGroup, Type[Airport]]) -> Point:
|
||||
if isinstance(at, Point):
|
||||
return at
|
||||
elif isinstance(at, ShipGroup):
|
||||
@@ -575,7 +569,7 @@ class AircraftConflictGenerator:
|
||||
else:
|
||||
assert False
|
||||
|
||||
def _setup_payload(self, flight: Flight, group: FlyingGroup) -> None:
|
||||
def _setup_payload(self, flight: Flight, group: FlyingGroup[Any]) -> None:
|
||||
for p in group.units:
|
||||
p.pylons.clear()
|
||||
|
||||
@@ -595,7 +589,10 @@ class AircraftConflictGenerator:
|
||||
parking_slot.unit_id = None
|
||||
|
||||
def generate_flights(
|
||||
self, country, ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData]
|
||||
self,
|
||||
country: Country,
|
||||
ato: AirTaskingOrder,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
) -> None:
|
||||
|
||||
for package in ato.packages:
|
||||
@@ -674,7 +671,7 @@ class AircraftConflictGenerator:
|
||||
self.unit_map.add_aircraft(group, flight)
|
||||
|
||||
def set_activation_time(
|
||||
self, flight: Flight, group: FlyingGroup, delay: timedelta
|
||||
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
|
||||
) -> None:
|
||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||
# mission editor. Waypoint times will be relative to the group
|
||||
@@ -693,7 +690,7 @@ class AircraftConflictGenerator:
|
||||
self.m.triggerrules.triggers.append(activation_trigger)
|
||||
|
||||
def set_startup_time(
|
||||
self, flight: Flight, group: FlyingGroup, delay: timedelta
|
||||
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
|
||||
) -> None:
|
||||
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
||||
group.uncontrolled = True
|
||||
@@ -721,7 +718,9 @@ class AircraftConflictGenerator:
|
||||
|
||||
trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
||||
|
||||
def generate_planned_flight(self, cp, country, flight: Flight):
|
||||
def generate_planned_flight(
|
||||
self, cp: ControlPoint, country: Country, flight: Flight
|
||||
) -> FlyingGroup[Any]:
|
||||
name = namegen.next_aircraft_name(country, cp.id, flight)
|
||||
try:
|
||||
if flight.start_type == "In Flight":
|
||||
@@ -730,13 +729,19 @@ class AircraftConflictGenerator:
|
||||
)
|
||||
elif isinstance(cp, NavalControlPoint):
|
||||
group_name = cp.get_carrier_group_name()
|
||||
carrier_group = self.m.find_group(group_name)
|
||||
if not isinstance(carrier_group, ShipGroup):
|
||||
raise RuntimeError(
|
||||
f"Carrier group {carrier_group} is a "
|
||||
"{carrier_group.__class__.__name__}, expected a ShipGroup"
|
||||
)
|
||||
group = self._generate_at_group(
|
||||
name=name,
|
||||
side=country,
|
||||
unit_type=flight.unit_type.dcs_unit_type,
|
||||
count=flight.count,
|
||||
start_type=flight.start_type,
|
||||
at=self.m.find_group(group_name),
|
||||
at=carrier_group,
|
||||
)
|
||||
else:
|
||||
if not isinstance(cp, Airfield):
|
||||
@@ -767,7 +772,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
@staticmethod
|
||||
def set_reduced_fuel(
|
||||
flight: Flight, group: FlyingGroup, unit_type: Type[PlaneType]
|
||||
flight: Flight, group: FlyingGroup[Any], unit_type: Type[FlyingType]
|
||||
) -> None:
|
||||
if unit_type is Su_33:
|
||||
for unit in group.units:
|
||||
@@ -793,9 +798,9 @@ class AircraftConflictGenerator:
|
||||
def configure_behavior(
|
||||
self,
|
||||
flight: Flight,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
||||
roe: Optional[OptROE.Values] = None,
|
||||
roe: Optional[int] = None,
|
||||
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
||||
restrict_jettison: Optional[bool] = None,
|
||||
mission_uses_gun: bool = True,
|
||||
@@ -826,13 +831,13 @@ class AircraftConflictGenerator:
|
||||
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
|
||||
|
||||
@staticmethod
|
||||
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
|
||||
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
if flight.unit_type.eplrs_capable:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
def configure_cap(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -840,7 +845,7 @@ class AircraftConflictGenerator:
|
||||
group.task = CAP.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
|
||||
if flight.unit_type not in GUNFIGHTERS:
|
||||
if not flight.unit_type.gunfighter:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||
else:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||
@@ -849,7 +854,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_sweep(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -857,7 +862,7 @@ class AircraftConflictGenerator:
|
||||
group.task = FighterSweep.name
|
||||
self._setup_group(group, package, flight, dynamic_runways)
|
||||
|
||||
if flight.unit_type not in GUNFIGHTERS:
|
||||
if not flight.unit_type.gunfighter:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||
else:
|
||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||
@@ -866,7 +871,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_cas(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -884,7 +889,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_dead(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -909,7 +914,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_sead(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -933,7 +938,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_strike(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -951,7 +956,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_anti_ship(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -969,7 +974,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_runway_attack(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -987,7 +992,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_oca_strike(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -1004,7 +1009,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_awacs(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -1032,7 +1037,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_refueling(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -1058,7 +1063,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_escort(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -1074,7 +1079,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_sead_escort(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -1097,7 +1102,7 @@ class AircraftConflictGenerator:
|
||||
|
||||
def configure_transport(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -1112,13 +1117,13 @@ class AircraftConflictGenerator:
|
||||
restrict_jettison=True,
|
||||
)
|
||||
|
||||
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
|
||||
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
|
||||
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||
self.configure_behavior(flight, group)
|
||||
|
||||
def setup_flight_group(
|
||||
self,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
dynamic_runways: Dict[str, RunwayData],
|
||||
@@ -1162,7 +1167,7 @@ class AircraftConflictGenerator:
|
||||
self.configure_eplrs(group, flight)
|
||||
|
||||
def create_waypoints(
|
||||
self, group: FlyingGroup, package: Package, flight: Flight
|
||||
self, group: FlyingGroup[Any], package: Package, flight: Flight
|
||||
) -> None:
|
||||
|
||||
for waypoint in flight.points:
|
||||
@@ -1182,7 +1187,7 @@ class AircraftConflictGenerator:
|
||||
# under the current flight plans.
|
||||
# TODO: Make this smarter, it currently selects a random unit in the group for target,
|
||||
# this could be updated to make it pick the "best" two targets in the group.
|
||||
if flight.unit_type is AJS37 and flight.client_count:
|
||||
if flight.unit_type.dcs_unit_type is AJS37 and flight.client_count:
|
||||
viggen_target_points = [
|
||||
(idx, point)
|
||||
for idx, point in enumerate(filtered_points)
|
||||
@@ -1230,7 +1235,7 @@ class AircraftConflictGenerator:
|
||||
waypoint: FlightWaypoint,
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
) -> None:
|
||||
estimator = TotEstimator(package)
|
||||
start_time = estimator.mission_start_time(flight)
|
||||
@@ -1273,7 +1278,7 @@ class PydcsWaypointBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
waypoint: FlightWaypoint,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
mission: Mission,
|
||||
@@ -1316,7 +1321,7 @@ class PydcsWaypointBuilder:
|
||||
def for_waypoint(
|
||||
cls,
|
||||
waypoint: FlightWaypoint,
|
||||
group: FlyingGroup,
|
||||
group: FlyingGroup[Any],
|
||||
package: Package,
|
||||
flight: Flight,
|
||||
mission: Mission,
|
||||
@@ -1346,9 +1351,10 @@ class PydcsWaypointBuilder:
|
||||
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
|
||||
If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
|
||||
"""
|
||||
if (self.flight.client_count > 0 and self.flight.unit_type == AJS37) and (
|
||||
self.waypoint.waypoint_type not in TARGET_WAYPOINTS
|
||||
):
|
||||
if (
|
||||
self.flight.client_count > 0
|
||||
and self.flight.unit_type.dcs_unit_type == AJS37
|
||||
) and (self.waypoint.waypoint_type not in TARGET_WAYPOINTS):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -1429,7 +1435,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
||||
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||
waypoint.add_task(
|
||||
EngageTargetsInZone(
|
||||
position=self.flight.flight_plan.target,
|
||||
position=self.flight.flight_plan.target.position,
|
||||
radius=int(self.flight.flight_plan.engagement_distance.meters),
|
||||
targets=[
|
||||
Targets.All.GroundUnits.GroundVehicles,
|
||||
@@ -1705,6 +1711,7 @@ class CargoStopBuilder(PydcsWaypointBuilder):
|
||||
|
||||
|
||||
class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
@has_save_compat_for(4)
|
||||
def build(self) -> MovingPoint:
|
||||
waypoint = super().build()
|
||||
|
||||
@@ -1739,17 +1746,11 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Set orbit speeds for all race tracks and remove this special case.
|
||||
if isinstance(flight_plan, RefuelingFlightPlan):
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack,
|
||||
speed=int(flight_plan.patrol_speed.kph),
|
||||
)
|
||||
else:
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
|
||||
)
|
||||
orbit = OrbitAction(
|
||||
altitude=waypoint.alt,
|
||||
pattern=OrbitAction.OrbitPattern.RaceTrack,
|
||||
speed=int(getattr(flight_plan, "patrol_speed", kph(600)).kph),
|
||||
)
|
||||
|
||||
racetrack = ControlledTask(orbit)
|
||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
||||
@@ -1761,7 +1762,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
|
||||
waypoint.add_task(Tanker())
|
||||
|
||||
if self.flight.unit_type != IL_78M:
|
||||
if self.flight.unit_type.dcs_unit_type.tacan:
|
||||
tanker_info = self.air_support.tankers[-1]
|
||||
tacan = tanker_info.tacan
|
||||
tacan_callsign = {
|
||||
|
||||
@@ -1521,4 +1521,47 @@ AIRFIELD_DATA = {
|
||||
runway_length=3953,
|
||||
atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
|
||||
),
|
||||
"Antonio B. Won Pat Intl": AirfieldData(
|
||||
theater="MarianaIslands",
|
||||
icao="PGUM",
|
||||
elevation=255,
|
||||
runway_length=9359,
|
||||
atc=AtcData(MHz(3, 825), MHz(118, 100), MHz(38, 550), MHz(340, 200)),
|
||||
ils={
|
||||
"06": ("IGUM", MHz(110, 30)),
|
||||
},
|
||||
),
|
||||
"Andersen AFB": AirfieldData(
|
||||
theater="MarianaIslands",
|
||||
icao="PGUA",
|
||||
elevation=545,
|
||||
runway_length=10490,
|
||||
tacan=TacanChannel(54, TacanBand.X),
|
||||
tacan_callsign="UAM",
|
||||
atc=AtcData(MHz(3, 850), MHz(126, 200), MHz(38, 600), MHz(250, 100)),
|
||||
),
|
||||
"Rota Intl": AirfieldData(
|
||||
theater="MarianaIslands",
|
||||
icao="PGRO",
|
||||
elevation=568,
|
||||
runway_length=6105,
|
||||
atc=AtcData(MHz(3, 750), MHz(123, 600), MHz(38, 400), MHz(250, 0)),
|
||||
),
|
||||
"Tinian Intl": AirfieldData(
|
||||
theater="MarianaIslands",
|
||||
icao="PGWT",
|
||||
elevation=240,
|
||||
runway_length=7777,
|
||||
atc=AtcData(MHz(3, 800), MHz(123, 650), MHz(38, 500), MHz(250, 50)),
|
||||
),
|
||||
"Saipan Intl": AirfieldData(
|
||||
theater="MarianaIslands",
|
||||
icao="PGSN",
|
||||
elevation=213,
|
||||
runway_length=7790,
|
||||
atc=AtcData(MHz(3, 775), MHz(125, 700), MHz(38, 450), MHz(256, 900)),
|
||||
ils={
|
||||
"07": ("IGSN", MHz(109, 90)),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from typing import List, Type, Tuple, Optional
|
||||
from typing import List, Type, Tuple, Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.mission import Mission, StartType
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType
|
||||
from dcs.task import (
|
||||
AWACS,
|
||||
ActivateBeaconCommand,
|
||||
@@ -14,15 +15,17 @@ from dcs.task import (
|
||||
SetImmortalCommand,
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game import db
|
||||
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from .naming import namegen
|
||||
from .callsigns import callsign_for_support_unit
|
||||
from .conflictgen import Conflict
|
||||
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||
from .naming import namegen
|
||||
from .radios import RadioFrequency, RadioRegistry
|
||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
TANKER_DISTANCE = 15000
|
||||
TANKER_ALT = 4572
|
||||
@@ -70,7 +73,7 @@ class AirSupportConflictGenerator:
|
||||
self,
|
||||
mission: Mission,
|
||||
conflict: Conflict,
|
||||
game,
|
||||
game: Game,
|
||||
radio_registry: RadioRegistry,
|
||||
tacan_registry: TacanRegistry,
|
||||
) -> None:
|
||||
@@ -95,7 +98,7 @@ class AirSupportConflictGenerator:
|
||||
return (TANKER_ALT + 500, 596)
|
||||
return (TANKER_ALT, 574)
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
player_cp = (
|
||||
self.conflict.blue_cp
|
||||
if self.conflict.blue_cp.captured
|
||||
@@ -108,6 +111,11 @@ class AirSupportConflictGenerator:
|
||||
for i, tanker_unit_type in enumerate(
|
||||
self.game.faction_for(player=True).tankers
|
||||
):
|
||||
unit_type = tanker_unit_type.dcs_unit_type
|
||||
if not issubclass(unit_type, PlaneType):
|
||||
logging.warning(f"Refueling aircraft {unit_type} must be a plane")
|
||||
continue
|
||||
|
||||
# TODO: Make loiter altitude a property of the unit type.
|
||||
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
@@ -127,7 +135,7 @@ class AirSupportConflictGenerator:
|
||||
self.mission.country(self.game.player_country), tanker_unit_type
|
||||
),
|
||||
airport=None,
|
||||
plane_type=tanker_unit_type,
|
||||
plane_type=unit_type,
|
||||
position=tanker_position,
|
||||
altitude=alt,
|
||||
race_distance=58000,
|
||||
@@ -177,6 +185,8 @@ class AirSupportConflictGenerator:
|
||||
tanker_unit_type.name,
|
||||
freq,
|
||||
tacan,
|
||||
start_time=None,
|
||||
end_time=None,
|
||||
blue=True,
|
||||
)
|
||||
)
|
||||
@@ -195,12 +205,17 @@ class AirSupportConflictGenerator:
|
||||
awacs_unit = possible_awacs[0]
|
||||
freq = self.radio_registry.alloc_uhf()
|
||||
|
||||
unit_type = awacs_unit.dcs_unit_type
|
||||
if not issubclass(unit_type, PlaneType):
|
||||
logging.warning(f"AWACS aircraft {unit_type} must be a plane")
|
||||
return
|
||||
|
||||
awacs_flight = self.mission.awacs_flight(
|
||||
country=self.mission.country(self.game.player_country),
|
||||
name=namegen.next_awacs_name(
|
||||
self.mission.country(self.game.player_country)
|
||||
),
|
||||
plane_type=awacs_unit,
|
||||
plane_type=unit_type,
|
||||
altitude=AWACS_ALT,
|
||||
airport=None,
|
||||
position=self.conflict.position.random_point_within(
|
||||
|
||||
48
gen/armor.py
48
gen/armor.py
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
@@ -23,7 +24,7 @@ from dcs.task import (
|
||||
SetInvisibleCommand,
|
||||
)
|
||||
from dcs.triggers import Event, TriggerOnce
|
||||
from dcs.unit import Vehicle
|
||||
from dcs.unit import Vehicle, Skill
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
@@ -97,7 +98,7 @@ class GroundConflictGenerator:
|
||||
self.unit_map = unit_map
|
||||
self.jtacs: List[JtacInfo] = []
|
||||
|
||||
def _enemy_stance(self):
|
||||
def _enemy_stance(self) -> CombatStance:
|
||||
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
|
||||
if len(self.enemy_planned_combat_groups) > len(
|
||||
self.player_planned_combat_groups
|
||||
@@ -122,20 +123,11 @@ class GroundConflictGenerator:
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _group_point(point: Point, base_distance) -> Point:
|
||||
distance = random.randint(
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
|
||||
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
|
||||
)
|
||||
return point.random_point_within(
|
||||
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
position = Conflict.frontline_position(
|
||||
self.conflict.front_line, self.game.theater
|
||||
)
|
||||
|
||||
frontline_vector = Conflict.frontline_vector(
|
||||
self.conflict.front_line, self.game.theater
|
||||
)
|
||||
@@ -150,6 +142,13 @@ class GroundConflictGenerator:
|
||||
self.enemy_planned_combat_groups, frontline_vector, False
|
||||
)
|
||||
|
||||
# TODO: Differentiate AirConflict and GroundConflict classes.
|
||||
if self.conflict.heading is None:
|
||||
raise RuntimeError(
|
||||
"Cannot generate ground units for non-ground conflict. Ground unit "
|
||||
"conflicts cannot have the heading `None`."
|
||||
)
|
||||
|
||||
# Plan combat actions for groups
|
||||
self.plan_action_for_groups(
|
||||
self.player_stance,
|
||||
@@ -174,7 +173,7 @@ class GroundConflictGenerator:
|
||||
code = 1688 - len(self.jtacs)
|
||||
|
||||
utype = self.game.player_faction.jtac_unit
|
||||
if self.game.player_faction.jtac_unit is None:
|
||||
if utype is None:
|
||||
utype = AircraftType.named("MQ-9 Reaper")
|
||||
|
||||
jtac = self.mission.flight_group(
|
||||
@@ -361,7 +360,6 @@ class GroundConflictGenerator:
|
||||
self.mission.triggerrules.triggers.append(artillery_fallback)
|
||||
|
||||
for u in dcs_group.units:
|
||||
u.initial = True
|
||||
u.heading = forward_heading + random.randint(-5, 5)
|
||||
return True
|
||||
return False
|
||||
@@ -570,10 +568,10 @@ class GroundConflictGenerator:
|
||||
)
|
||||
|
||||
# Fallback task
|
||||
fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
|
||||
fallback.enabled = False
|
||||
task = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
|
||||
task.enabled = False
|
||||
dcs_group.add_trigger_action(Hold())
|
||||
dcs_group.add_trigger_action(fallback)
|
||||
dcs_group.add_trigger_action(task)
|
||||
|
||||
# Create trigger
|
||||
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
|
||||
@@ -634,7 +632,7 @@ class GroundConflictGenerator:
|
||||
@param enemy_groups Potential enemy groups
|
||||
@param n number of nearby groups to take
|
||||
"""
|
||||
targets = [] # type: List[Optional[VehicleGroup]]
|
||||
targets = [] # type: List[VehicleGroup]
|
||||
sorted_list = sorted(
|
||||
enemy_groups,
|
||||
key=lambda group: player_group.points[0].position.distance_to_point(
|
||||
@@ -658,7 +656,7 @@ class GroundConflictGenerator:
|
||||
@param group Group for which we should find the nearest ennemy
|
||||
@param enemy_groups Potential enemy groups
|
||||
"""
|
||||
min_distance = 99999999
|
||||
min_distance = math.inf
|
||||
target = None
|
||||
for dcs_group, _ in enemy_groups:
|
||||
dist = player_group.points[0].position.distance_to_point(
|
||||
@@ -696,7 +694,7 @@ class GroundConflictGenerator:
|
||||
"""
|
||||
For artilery group, decide the distance from frontline with the range of the unit
|
||||
"""
|
||||
rg = getattr(group.unit_type.dcs_unit_type, "threat_range", 0) - 7500
|
||||
rg = group.unit_type.dcs_unit_type.threat_range - 7500
|
||||
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||
rg = random.randint(
|
||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
||||
@@ -716,7 +714,7 @@ class GroundConflictGenerator:
|
||||
distance_from_frontline: int,
|
||||
heading: int,
|
||||
spawn_heading: int,
|
||||
):
|
||||
) -> Optional[Point]:
|
||||
shifted = conflict_position.point_from_heading(
|
||||
heading, random.randint(0, combat_width)
|
||||
)
|
||||
@@ -766,9 +764,9 @@ class GroundConflictGenerator:
|
||||
heading=opposite_heading(spawn_heading),
|
||||
)
|
||||
if is_player:
|
||||
g.set_skill(self.game.settings.player_skill)
|
||||
g.set_skill(Skill(self.game.settings.player_skill))
|
||||
else:
|
||||
g.set_skill(self.game.settings.enemy_vehicle_skill)
|
||||
g.set_skill(Skill(self.game.settings.enemy_vehicle_skill))
|
||||
positioned_groups.append((g, group))
|
||||
|
||||
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
|
||||
@@ -790,7 +788,7 @@ class GroundConflictGenerator:
|
||||
count: int,
|
||||
at: Point,
|
||||
move_formation: PointAction = PointAction.OffRoad,
|
||||
heading=0,
|
||||
heading: int = 0,
|
||||
) -> VehicleGroup:
|
||||
|
||||
if side == self.conflict.attackers_country:
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""Support for working with DCS group callsigns."""
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.flyingunit import FlyingUnit
|
||||
|
||||
|
||||
def callsign_for_support_unit(group: FlyingGroup) -> str:
|
||||
def callsign_for_support_unit(group: FlyingGroup[Any]) -> str:
|
||||
# Either something like Overlord11 for Western AWACS, or else just a number.
|
||||
# Convert to either "Overlord" or "Flight 123".
|
||||
lead = group.units[0]
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import logging
|
||||
import random
|
||||
from game import db
|
||||
from typing import Optional
|
||||
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game import db, Game
|
||||
from game.theater.theatergroundobject import CoastalSiteGroundObject
|
||||
from gen.coastal.silkworm import SilkwormGenerator
|
||||
|
||||
COASTAL_MAP = {
|
||||
@@ -8,10 +13,13 @@ COASTAL_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def generate_coastal_group(game, ground_object, faction_name: str):
|
||||
def generate_coastal_group(
|
||||
game: Game, ground_object: CoastalSiteGroundObject, faction_name: str
|
||||
) -> Optional[VehicleGroup]:
|
||||
"""
|
||||
This generate a coastal defenses group
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
:return: The generated group, or None if this faction does not support coastal
|
||||
defenses.
|
||||
"""
|
||||
faction = db.FACTIONS[faction_name]
|
||||
if len(faction.coastal_defenses) > 0:
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import CoastalSiteGroundObject
|
||||
from gen.sam.group_generator import VehicleGroupGenerator
|
||||
|
||||
|
||||
class SilkwormGenerator(GroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: CoastalSiteGroundObject, faction: Faction
|
||||
) -> None:
|
||||
super(SilkwormGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
|
||||
|
||||
@@ -23,7 +28,7 @@ class SilkwormGenerator(GroupGenerator):
|
||||
# Launchers
|
||||
for i, p in enumerate(positions):
|
||||
self.add_unit(
|
||||
MissilesSS.Silkworm_SR,
|
||||
MissilesSS.Hy_launcher,
|
||||
"Missile#" + str(i),
|
||||
p[0],
|
||||
p[1],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Tuple, Optional
|
||||
|
||||
@@ -54,13 +56,15 @@ class Conflict:
|
||||
def frontline_position(
|
||||
cls, frontline: FrontLine, theater: ConflictTheater
|
||||
) -> Tuple[Point, int]:
|
||||
attack_heading = frontline.attack_heading
|
||||
attack_heading = int(frontline.attack_heading)
|
||||
position = cls.find_ground_position(
|
||||
frontline.position,
|
||||
FRONTLINE_LENGTH,
|
||||
heading_sum(attack_heading, 90),
|
||||
theater,
|
||||
)
|
||||
if position is None:
|
||||
raise RuntimeError("Could not find front line position")
|
||||
return position, opposite_heading(attack_heading)
|
||||
|
||||
@classmethod
|
||||
@@ -91,7 +95,7 @@ class Conflict:
|
||||
defender: Country,
|
||||
front_line: FrontLine,
|
||||
theater: ConflictTheater,
|
||||
):
|
||||
) -> Conflict:
|
||||
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp)
|
||||
position, heading, distance = cls.frontline_vector(front_line, theater)
|
||||
conflict = cls(
|
||||
@@ -138,7 +142,7 @@ class Conflict:
|
||||
max_distance: int,
|
||||
heading: int,
|
||||
theater: ConflictTheater,
|
||||
coerce=True,
|
||||
coerce: bool = True,
|
||||
) -> Optional[Point]:
|
||||
"""
|
||||
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
|
||||
@@ -153,6 +157,8 @@ class Conflict:
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
pos = initial.point_from_heading(opposite_heading(heading), distance)
|
||||
if theater.is_on_land(pos):
|
||||
return pos
|
||||
if coerce:
|
||||
pos = theater.nearest_land_pos(initial)
|
||||
return pos
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
@@ -12,7 +13,9 @@ from gen.defenses.armored_group_generator import (
|
||||
)
|
||||
|
||||
|
||||
def generate_armor_group(faction: str, game, ground_object):
|
||||
def generate_armor_group(
|
||||
faction: str, game: Game, ground_object: VehicleGroupGroundObject
|
||||
) -> Optional[VehicleGroup]:
|
||||
"""
|
||||
This generate a group of ground units
|
||||
:return: Generated group
|
||||
|
||||
@@ -3,10 +3,10 @@ import random
|
||||
from game import Game
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.group_generator import VehicleGroupGenerator
|
||||
|
||||
|
||||
class ArmoredGroupGenerator(GroupGenerator):
|
||||
class ArmoredGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
@@ -35,7 +35,7 @@ class ArmoredGroupGenerator(GroupGenerator):
|
||||
)
|
||||
|
||||
|
||||
class FixedSizeArmorGroupGenerator(GroupGenerator):
|
||||
class FixedSizeArmorGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
@@ -47,7 +47,7 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
|
||||
self.unit_type = unit_type
|
||||
self.size = size
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
spacing = random.randint(20, 70)
|
||||
|
||||
index = 0
|
||||
|
||||
@@ -22,7 +22,7 @@ class EnvironmentGenerator:
|
||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||
if fog is None:
|
||||
return
|
||||
self.mission.weather.fog_visibility = fog.visibility.meters
|
||||
self.mission.weather.fog_visibility = int(fog.visibility.meters)
|
||||
self.mission.weather.fog_thickness = fog.thickness
|
||||
|
||||
def set_wind(self, wind: WindConditions) -> None:
|
||||
@@ -30,7 +30,7 @@ class EnvironmentGenerator:
|
||||
self.mission.weather.wind_at_2000 = wind.at_2000m
|
||||
self.mission.weather.wind_at_8000 = wind.at_8000m
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
self.mission.start_time = self.conditions.start_time
|
||||
self.set_clouds(self.conditions.weather.clouds)
|
||||
self.set_fog(self.conditions.weather.fog)
|
||||
|
||||
@@ -6,7 +6,7 @@ from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG
|
||||
|
||||
|
||||
class CarrierGroupGenerator(ShipGroupGenerator):
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
# Carrier Strike Group 8
|
||||
if self.faction.carrier_names[0] == "Carrier Strike Group 8":
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
from dcs.ships import (
|
||||
Type_052C,
|
||||
Type_052B,
|
||||
@@ -11,16 +10,16 @@ from dcs.ships import (
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import ShipGroundObject
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
include_frigate = random.choice([True, True, False])
|
||||
include_dd = random.choice([True, False])
|
||||
@@ -65,9 +64,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
|
||||
class Type54GroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||
super(Type54GroupGenerator, self).__init__(
|
||||
game, ground_object, faction, Type_054A
|
||||
)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from dcs.unittype import ShipType
|
||||
from dcs.ships import PERRY, USS_Arleigh_Burke_IIa
|
||||
from dcs.unittype import ShipType
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import ShipGroundObject
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
@@ -16,14 +17,14 @@ class DDGroupGenerator(ShipGroupGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
ground_object: TheaterGroundObject,
|
||||
ground_object: ShipGroundObject,
|
||||
faction: Faction,
|
||||
ddtype: Type[ShipType],
|
||||
):
|
||||
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
|
||||
self.ddtype = ddtype
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
self.add_unit(
|
||||
self.ddtype,
|
||||
"DD1",
|
||||
@@ -42,18 +43,14 @@ class DDGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
|
||||
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||
super(OliverHazardPerryGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, PERRY
|
||||
)
|
||||
|
||||
|
||||
class ArleighBurkeGroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||
super(ArleighBurkeGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, USS_Arleigh_Burke_IIa
|
||||
)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from dcs.ships import La_Combattante_II
|
||||
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.theater import TheaterGroundObject
|
||||
from game.theater.theatergroundobject import ShipGroundObject
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
|
||||
|
||||
class LaCombattanteIIGroupGenerator(DDGroupGenerator):
|
||||
def __init__(self, game, ground_object: TheaterGroundObject, faction: Faction):
|
||||
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||
super(LaCombattanteIIGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, La_Combattante_II
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class LHAGroupGenerator(ShipGroupGenerator):
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
# Add carrier
|
||||
if len(self.faction.helicopter_carrier) > 0:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -12,18 +13,17 @@ from dcs.ships import (
|
||||
SOM,
|
||||
)
|
||||
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import ShipGroundObject
|
||||
from gen.fleet.dd_group import DDGroupGenerator
|
||||
from gen.sam.group_generator import ShipGroupGenerator
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
include_frigate = random.choice([True, True, False])
|
||||
include_dd = random.choice([True, False])
|
||||
@@ -85,32 +85,24 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||
|
||||
|
||||
class GrishaGroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||
super(GrishaGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, ALBATROS
|
||||
)
|
||||
|
||||
|
||||
class MolniyaGroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||
super(MolniyaGroupGenerator, self).__init__(
|
||||
game, ground_object, faction, MOLNIYA
|
||||
)
|
||||
|
||||
|
||||
class KiloSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, KILO)
|
||||
|
||||
|
||||
class TangoSubGroupGenerator(DDGroupGenerator):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM)
|
||||
|
||||
@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class SchnellbootGroupGenerator(ShipGroupGenerator):
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
for i in range(random.randint(2, 4)):
|
||||
self.add_unit(
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dcs.unitgroup import ShipGroup
|
||||
|
||||
from game import db
|
||||
from game.theater.theatergroundobject import (
|
||||
LhaGroundObject,
|
||||
CarrierGroundObject,
|
||||
ShipGroundObject,
|
||||
)
|
||||
from gen.fleet.carrier_group import CarrierGroupGenerator
|
||||
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
|
||||
from gen.fleet.dd_group import (
|
||||
@@ -21,6 +31,9 @@ from gen.fleet.schnellboot import SchnellbootGroupGenerator
|
||||
from gen.fleet.uboat import UBoatGroupGenerator
|
||||
from gen.fleet.ww2lst import WW2LSTGroupGenerator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
SHIP_MAP = {
|
||||
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
|
||||
@@ -39,10 +52,12 @@ SHIP_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def generate_ship_group(game, ground_object, faction_name: str):
|
||||
def generate_ship_group(
|
||||
game: Game, ground_object: ShipGroundObject, faction_name: str
|
||||
) -> Optional[ShipGroup]:
|
||||
"""
|
||||
This generate a ship group
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
:return: The generated group, or None if this faction does not support ships.
|
||||
"""
|
||||
faction = db.FACTIONS[faction_name]
|
||||
if len(faction.navy_generators) > 0:
|
||||
@@ -61,26 +76,30 @@ def generate_ship_group(game, ground_object, faction_name: str):
|
||||
return None
|
||||
|
||||
|
||||
def generate_carrier_group(faction: str, game, ground_object):
|
||||
"""
|
||||
This generate a carrier group
|
||||
:param parentCp: The parent control point
|
||||
def generate_carrier_group(
|
||||
faction: str, game: Game, ground_object: CarrierGroundObject
|
||||
) -> ShipGroup:
|
||||
"""Generates a carrier group.
|
||||
|
||||
:param faction: The faction the TGO belongs to.
|
||||
:param game: The Game the group is being generated for.
|
||||
:param ground_object: The ground object which will own the ship group
|
||||
:param country: Owner country
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
:return: The generated group.
|
||||
"""
|
||||
generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
||||
generator.generate()
|
||||
return generator.get_generated_group()
|
||||
|
||||
|
||||
def generate_lha_group(faction: str, game, ground_object):
|
||||
"""
|
||||
This generate a lha carrier group
|
||||
:param parentCp: The parent control point
|
||||
def generate_lha_group(
|
||||
faction: str, game: Game, ground_object: LhaGroundObject
|
||||
) -> ShipGroup:
|
||||
"""Generate an LHA group.
|
||||
|
||||
:param faction: The faction the TGO belongs to.
|
||||
:param game: The Game the group is being generated for.
|
||||
:param ground_object: The ground object which will own the ship group
|
||||
:param country: Owner country
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
:return: The generated group.
|
||||
"""
|
||||
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
||||
generator.generate()
|
||||
|
||||
@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class UBoatGroupGenerator(ShipGroupGenerator):
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
for i in range(random.randint(1, 4)):
|
||||
self.add_unit(
|
||||
|
||||
@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
|
||||
|
||||
|
||||
class WW2LSTGroupGenerator(ShipGroupGenerator):
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
# Add LS Samuel Chase
|
||||
self.add_unit(
|
||||
|
||||
@@ -18,6 +18,7 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
Any,
|
||||
)
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
@@ -178,7 +179,7 @@ class AircraftAllocator:
|
||||
aircraft, task
|
||||
)
|
||||
for squadron in squadrons:
|
||||
if squadron.number_of_available_pilots >= flight.num_aircraft:
|
||||
if squadron.can_provide_pilots(flight.num_aircraft):
|
||||
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||
return airfield, squadron
|
||||
return None
|
||||
@@ -284,7 +285,7 @@ class ObjectiveFinder:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
|
||||
def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject, Distance]]:
|
||||
def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject[Any], Distance]]:
|
||||
"""Iterates over all enemy SAM sites."""
|
||||
doctrine = self.game.faction_for(self.is_player).doctrine
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
@@ -314,14 +315,14 @@ class ObjectiveFinder:
|
||||
|
||||
yield ground_object, target_range
|
||||
|
||||
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject]:
|
||||
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject[Any]]:
|
||||
"""Iterates over enemy SAMs in threat range of friendly control points.
|
||||
|
||||
SAM sites are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
|
||||
target_ranges: list[tuple[TheaterGroundObject, Distance]] = []
|
||||
target_ranges: list[tuple[TheaterGroundObject[Any], Distance]] = []
|
||||
for target, threat_range in self.enemy_air_defenses():
|
||||
ranges: list[Distance] = []
|
||||
for cp in self.friendly_control_points():
|
||||
@@ -374,9 +375,9 @@ class ObjectiveFinder:
|
||||
def _targets_by_range(
|
||||
self, targets: Iterable[MissionTargetType]
|
||||
) -> Iterator[MissionTargetType]:
|
||||
target_ranges: List[Tuple[MissionTargetType, int]] = []
|
||||
target_ranges: list[tuple[MissionTargetType, float]] = []
|
||||
for target in targets:
|
||||
ranges: List[int] = []
|
||||
ranges: list[float] = []
|
||||
for cp in self.friendly_control_points():
|
||||
ranges.append(target.distance_to(cp))
|
||||
target_ranges.append((target, min(ranges)))
|
||||
@@ -385,13 +386,13 @@ class ObjectiveFinder:
|
||||
for target, _range in target_ranges:
|
||||
yield target
|
||||
|
||||
def strike_targets(self) -> Iterator[TheaterGroundObject]:
|
||||
def strike_targets(self) -> Iterator[TheaterGroundObject[Any]]:
|
||||
"""Iterates over enemy strike targets.
|
||||
|
||||
Targets are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
targets: List[Tuple[TheaterGroundObject, int]] = []
|
||||
targets: list[tuple[TheaterGroundObject[Any], float]] = []
|
||||
# Building objectives are made of several individual TGOs (one per
|
||||
# building).
|
||||
found_targets: Set[str] = set()
|
||||
@@ -430,7 +431,7 @@ class ObjectiveFinder:
|
||||
continue
|
||||
if ground_object.name in found_targets:
|
||||
continue
|
||||
ranges: List[int] = []
|
||||
ranges: list[float] = []
|
||||
for friendly_cp in self.friendly_control_points():
|
||||
ranges.append(ground_object.distance_to(friendly_cp))
|
||||
targets.append((ground_object, min(ranges)))
|
||||
@@ -604,27 +605,35 @@ class CoalitionMissionPlanner:
|
||||
also possible for the player to exclude mission types from their squadron
|
||||
designs.
|
||||
"""
|
||||
all_compatible = aircraft_for_task(mission_type)
|
||||
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
|
||||
if (
|
||||
squadron.aircraft in all_compatible
|
||||
and mission_type in squadron.auto_assignable_mission_types
|
||||
):
|
||||
return True
|
||||
return False
|
||||
return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type)
|
||||
|
||||
@property
|
||||
def oca_aircraft_plannable(self) -> bool:
|
||||
return (
|
||||
self.air_wing_can_plan(FlightType.OCA_AIRCRAFT)
|
||||
and self.game.settings.default_start_type == "Cold"
|
||||
def critical_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies the most important missions to plan this turn.
|
||||
|
||||
Non-critical missions that cannot be fulfilled will create purchase
|
||||
orders for the next turn. Critical missions will create a purchase order
|
||||
unless the mission can be doubly fulfilled. In other words, the AI will
|
||||
attempt to have *double* the aircraft it needs for these missions to
|
||||
ensure that they can be planned again next turn even if all aircraft are
|
||||
eliminated this turn.
|
||||
"""
|
||||
|
||||
# Find farthest, friendly CP for AEWC.
|
||||
yield ProposedMission(
|
||||
self.objective_finder.farthest_friendly_control_point(),
|
||||
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
|
||||
# Supports all the early CAP flights, so should be in the air ASAP.
|
||||
asap=True,
|
||||
)
|
||||
|
||||
yield ProposedMission(
|
||||
self.objective_finder.closest_friendly_control_point(),
|
||||
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
|
||||
)
|
||||
|
||||
def propose_barcap(self) -> Iterator[ProposedMission]:
|
||||
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||
for cp in self.objective_finder.vulnerable_control_points():
|
||||
# Plan CAP in such a way, that it is established during the whole desired
|
||||
# mission length.
|
||||
# Plan CAP in such a way, that it is established during the whole desired mission length
|
||||
for _ in range(
|
||||
0,
|
||||
int(self.game.settings.desired_player_mission_duration.total_seconds()),
|
||||
@@ -637,31 +646,36 @@ class CoalitionMissionPlanner:
|
||||
],
|
||||
)
|
||||
|
||||
def propose_cas(self) -> Iterator[ProposedMission]:
|
||||
# Find front lines, plan CAS.
|
||||
for front_line in self.objective_finder.front_lines():
|
||||
flights = [ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE)]
|
||||
if self.air_wing_can_plan(FlightType.TARCAP):
|
||||
# This is *not* an escort because front lines don't create a threat
|
||||
# zone. Generating threat zones from front lines causes the front
|
||||
# line to push back BARCAPs as it gets closer to the base. While
|
||||
# front lines do have the same problem of potentially pulling
|
||||
# BARCAPs off bases to engage a front line TARCAP, that's probably
|
||||
# the one time where we do want that.
|
||||
#
|
||||
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
|
||||
# We don't have intercept missions yet so this isn't something we
|
||||
# can do today, but we should probably return to having the front
|
||||
# line project a threat zone (so that strike missions will route
|
||||
# around it) and instead *not plan* a BARCAP at bases near the
|
||||
# front, since there isn't a place to put a barrier. Instead, the
|
||||
# aircraft that would have been a BARCAP could be used as additional
|
||||
# interceptors and TARCAPs which will defend the base but won't be
|
||||
# trying to avoid front line contacts.
|
||||
flights.append(ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE))
|
||||
yield ProposedMission(front_line, flights)
|
||||
yield ProposedMission(
|
||||
front_line,
|
||||
[
|
||||
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
|
||||
# This is *not* an escort because front lines don't create a threat
|
||||
# zone. Generating threat zones from front lines causes the front
|
||||
# line to push back BARCAPs as it gets closer to the base. While
|
||||
# front lines do have the same problem of potentially pulling
|
||||
# BARCAPs off bases to engage a front line TARCAP, that's probably
|
||||
# the one time where we do want that.
|
||||
#
|
||||
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
|
||||
# We don't have intercept missions yet so this isn't something we
|
||||
# can do today, but we should probably return to having the front
|
||||
# line project a threat zone (so that strike missions will route
|
||||
# around it) and instead *not plan* a BARCAP at bases near the
|
||||
# front, since there isn't a place to put a barrier. Instead, the
|
||||
# aircraft that would have been a BARCAP could be used as additional
|
||||
# interceptors and TARCAPs which will defend the base but won't be
|
||||
# trying to avoid front line contacts.
|
||||
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
|
||||
],
|
||||
)
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
yield from self.critical_missions()
|
||||
|
||||
def propose_dead(self) -> Iterator[ProposedMission]:
|
||||
# Find enemy SAM sites with ranges that cover friendly CPs, front lines,
|
||||
# or objects, plan DEAD.
|
||||
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
||||
@@ -686,10 +700,7 @@ class CoalitionMissionPlanner:
|
||||
else:
|
||||
flights.append(
|
||||
ProposedFlight(
|
||||
FlightType.SEAD_ESCORT,
|
||||
2,
|
||||
self.MAX_SEAD_RANGE,
|
||||
EscortType.Sead,
|
||||
FlightType.SEAD_ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.Sead
|
||||
)
|
||||
)
|
||||
# TODO: Max escort range.
|
||||
@@ -700,7 +711,6 @@ class CoalitionMissionPlanner:
|
||||
)
|
||||
yield ProposedMission(sam, flights)
|
||||
|
||||
def propose_convoy_interdiction(self) -> Iterator[ProposedMission]:
|
||||
# These will only rarely get planned. When a convoy is travelling multiple legs,
|
||||
# they're targetable after the first leg. The reason for this is that
|
||||
# procurement happens *after* mission planning so that the missions that could
|
||||
@@ -729,7 +739,6 @@ class CoalitionMissionPlanner:
|
||||
],
|
||||
)
|
||||
|
||||
def propose_shipping_interdiction(self) -> Iterator[ProposedMission]:
|
||||
for ship in self.objective_finder.cargo_ships():
|
||||
yield ProposedMission(
|
||||
ship,
|
||||
@@ -745,7 +754,6 @@ class CoalitionMissionPlanner:
|
||||
],
|
||||
)
|
||||
|
||||
def propose_naval_strikes(self) -> Iterator[ProposedMission]:
|
||||
for group in self.objective_finder.threatening_ships():
|
||||
yield ProposedMission(
|
||||
group,
|
||||
@@ -761,7 +769,6 @@ class CoalitionMissionPlanner:
|
||||
],
|
||||
)
|
||||
|
||||
def propose_bai(self) -> Iterator[ProposedMission]:
|
||||
for group in self.objective_finder.threatening_vehicle_groups():
|
||||
yield ProposedMission(
|
||||
group,
|
||||
@@ -777,25 +784,16 @@ class CoalitionMissionPlanner:
|
||||
],
|
||||
)
|
||||
|
||||
def propose_oca_strikes(self) -> Iterator[ProposedMission]:
|
||||
for target in self.objective_finder.oca_targets(min_aircraft=20):
|
||||
flights = []
|
||||
if self.air_wing_can_plan(FlightType.OCA_RUNWAY):
|
||||
flights.append(
|
||||
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE)
|
||||
)
|
||||
if self.oca_aircraft_plannable:
|
||||
flights = [
|
||||
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE),
|
||||
]
|
||||
if self.game.settings.default_start_type == "Cold":
|
||||
# Only schedule if the default start type is Cold. If the player
|
||||
# has set anything else there are no targets to hit.
|
||||
flights.append(
|
||||
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
|
||||
)
|
||||
if not flights:
|
||||
raise RuntimeError(
|
||||
"Attempted planning of OCA strikes but neither OCA/Runway nor "
|
||||
f"OCA/Aircraft are plannable for {self.faction.name} with the "
|
||||
"current game settings."
|
||||
)
|
||||
flights.extend(
|
||||
[
|
||||
# TODO: Max escort range.
|
||||
@@ -809,7 +807,7 @@ class CoalitionMissionPlanner:
|
||||
)
|
||||
yield ProposedMission(target, flights)
|
||||
|
||||
def propose_building_strikes(self) -> Iterator[ProposedMission]:
|
||||
# Plan strike missions.
|
||||
for target in self.objective_finder.strike_targets():
|
||||
yield ProposedMission(
|
||||
target,
|
||||
@@ -828,48 +826,6 @@ class CoalitionMissionPlanner:
|
||||
],
|
||||
)
|
||||
|
||||
def propose_missions(self) -> Iterator[ProposedMission]:
|
||||
"""Identifies and iterates over potential mission in priority order."""
|
||||
# Find farthest, friendly CP for AEWC.
|
||||
if self.air_wing_can_plan(FlightType.AEWC):
|
||||
yield ProposedMission(
|
||||
self.objective_finder.farthest_friendly_control_point(),
|
||||
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
|
||||
# Supports all the early CAP flights, so should be in the air ASAP.
|
||||
asap=True,
|
||||
)
|
||||
|
||||
if self.air_wing_can_plan(FlightType.REFUELING):
|
||||
yield ProposedMission(
|
||||
self.objective_finder.closest_friendly_control_point(),
|
||||
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
|
||||
)
|
||||
|
||||
if self.air_wing_can_plan(FlightType.BARCAP):
|
||||
yield from self.propose_barcap()
|
||||
|
||||
if self.air_wing_can_plan(FlightType.CAS):
|
||||
yield from self.propose_cas()
|
||||
|
||||
if self.air_wing_can_plan(FlightType.DEAD):
|
||||
yield from self.propose_dead()
|
||||
|
||||
if self.air_wing_can_plan(FlightType.BAI):
|
||||
yield from self.propose_convoy_interdiction()
|
||||
|
||||
if self.air_wing_can_plan(FlightType.ANTISHIP):
|
||||
yield from self.propose_shipping_interdiction()
|
||||
yield from self.propose_naval_strikes()
|
||||
|
||||
if self.air_wing_can_plan(FlightType.BAI):
|
||||
yield from self.propose_bai()
|
||||
|
||||
if self.air_wing_can_plan(FlightType.OCA_RUNWAY) or self.oca_aircraft_plannable:
|
||||
yield from self.propose_oca_strikes()
|
||||
|
||||
if self.air_wing_can_plan(FlightType.STRIKE):
|
||||
yield from self.propose_building_strikes()
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
"""Identifies and plans mission for the turn."""
|
||||
player = "Blue" if self.is_player else "Red"
|
||||
@@ -878,6 +834,11 @@ class CoalitionMissionPlanner:
|
||||
for proposed_mission in self.propose_missions():
|
||||
self.plan_mission(proposed_mission, tracer)
|
||||
|
||||
with logged_duration(f"{player} reserve mission planning"):
|
||||
with MultiEventTracer() as tracer:
|
||||
for critical_mission in self.critical_missions():
|
||||
self.plan_mission(critical_mission, tracer, reserves=True)
|
||||
|
||||
with logged_duration(f"{player} mission scheduling"):
|
||||
self.stagger_missions()
|
||||
|
||||
@@ -1097,7 +1058,7 @@ class CoalitionMissionPlanner:
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) + tot
|
||||
|
||||
def message(self, title, text) -> None:
|
||||
def message(self, title: str, text: str) -> None:
|
||||
"""Emits a planning message to the player.
|
||||
|
||||
If the mission planner belongs to the players coalition, this emits a
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from typing import List, Type
|
||||
from collections import Sequence
|
||||
from typing import Type
|
||||
|
||||
from dcs.helicopters import (
|
||||
AH_1W,
|
||||
@@ -111,7 +112,6 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.f22a.f22a import F_22A
|
||||
from pydcs_extensions.hercules.hercules import Hercules
|
||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
from pydcs_extensions.su57.su57 import Su_57
|
||||
|
||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
||||
@@ -125,29 +125,29 @@ from pydcs_extensions.su57.su57 import Su_57
|
||||
CAP_CAPABLE = [
|
||||
Su_57,
|
||||
F_22A,
|
||||
MiG_31,
|
||||
F_15C,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
MiG_25PD,
|
||||
Su_33,
|
||||
J_11A,
|
||||
Su_30,
|
||||
Su_27,
|
||||
J_11A,
|
||||
F_15C,
|
||||
MiG_29S,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
JF_17,
|
||||
JAS39Gripen,
|
||||
F_16A,
|
||||
F_4E,
|
||||
JAS39Gripen,
|
||||
JF_17,
|
||||
MiG_31,
|
||||
MiG_25PD,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
MiG_23MLD,
|
||||
MiG_21Bis,
|
||||
Mirage_2000_5,
|
||||
M_2000C,
|
||||
F_15E,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
MiG_19P,
|
||||
A_4E_C,
|
||||
@@ -174,6 +174,7 @@ CAS_CAPABLE = [
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
Hercules,
|
||||
Su_34,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
Su_25,
|
||||
@@ -191,17 +192,16 @@ CAS_CAPABLE = [
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
AJS37,
|
||||
Su_24MR,
|
||||
Su_24M,
|
||||
Su_17M4,
|
||||
Su_33,
|
||||
F_4E,
|
||||
S_3B,
|
||||
Su_34,
|
||||
Su_30,
|
||||
MiG_19P,
|
||||
MiG_29S,
|
||||
MiG_27K,
|
||||
MiG_29A,
|
||||
MiG_21Bis,
|
||||
AH_64D,
|
||||
AH_64A,
|
||||
AH_1W,
|
||||
@@ -213,14 +213,14 @@ CAS_CAPABLE = [
|
||||
Mi_24P,
|
||||
Mi_24V,
|
||||
Mi_8MT,
|
||||
UH_1H,
|
||||
MiG_19P,
|
||||
MiG_15bis,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
C_101CC,
|
||||
MB_339PAN,
|
||||
L_39ZA,
|
||||
UH_1H,
|
||||
A_20G,
|
||||
Ju_88A4,
|
||||
P_47D_40,
|
||||
@@ -301,13 +301,14 @@ STRIKE_CAPABLE = [
|
||||
Tornado_GR4,
|
||||
F_16C_50,
|
||||
FA_18C_hornet,
|
||||
AV8BNA,
|
||||
JF_17,
|
||||
F_16A,
|
||||
F_14B,
|
||||
F_14A_135_GR,
|
||||
JAS39Gripen_AG,
|
||||
Tornado_IDS,
|
||||
Su_17M4,
|
||||
Su_24MR,
|
||||
Su_24M,
|
||||
Su_25TM,
|
||||
Su_25T,
|
||||
@@ -319,11 +320,9 @@ STRIKE_CAPABLE = [
|
||||
MiG_29S,
|
||||
MiG_29G,
|
||||
MiG_29A,
|
||||
JF_17,
|
||||
F_4E,
|
||||
A_10C_2,
|
||||
A_10C,
|
||||
AV8BNA,
|
||||
S_3B,
|
||||
A_4E_C,
|
||||
M_2000C,
|
||||
@@ -332,7 +331,6 @@ STRIKE_CAPABLE = [
|
||||
MiG_15bis,
|
||||
F_5E_3,
|
||||
F_86F_Sabre,
|
||||
MB_339PAN,
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
B_17G,
|
||||
@@ -378,6 +376,7 @@ RUNWAY_ATTACK_CAPABLE = [
|
||||
Su_34,
|
||||
Su_30,
|
||||
Tornado_IDS,
|
||||
M_2000C,
|
||||
] + STRIKE_CAPABLE
|
||||
|
||||
# For any aircraft that isn't necessarily directly involved in strike
|
||||
@@ -418,7 +417,7 @@ REFUELING_CAPABALE = [
|
||||
]
|
||||
|
||||
|
||||
def dcs_types_for_task(task: FlightType) -> list[Type[FlyingType]]:
|
||||
def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
|
||||
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
|
||||
if task in cap_missions:
|
||||
return CAP_CAPABLE
|
||||
|
||||
@@ -2,13 +2,12 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import List, Optional, TYPE_CHECKING, Union
|
||||
from typing import List, Optional, TYPE_CHECKING, Union, Sequence
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import MovingPoint, PointAction
|
||||
from dcs.unit import Unit
|
||||
|
||||
from game import db
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons import Pilot, Squadron
|
||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||
@@ -141,8 +140,8 @@ class FlightWaypoint:
|
||||
waypoint_type: The waypoint type.
|
||||
x: X cooidinate of the waypoint.
|
||||
y: Y coordinate of the waypoint.
|
||||
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
||||
changed to MSL by setting alt_type to "RADIO".
|
||||
alt: Altitude of the waypoint. By default this is MSL, but it can be
|
||||
changed to AGL by setting alt_type to "RADIO"
|
||||
"""
|
||||
self.waypoint_type = waypoint_type
|
||||
self.x = x
|
||||
@@ -154,7 +153,7 @@ class FlightWaypoint:
|
||||
# Only used in the waypoint list in the flight edit page. No sense
|
||||
# having three names. A short and long form is enough.
|
||||
self.description = ""
|
||||
self.targets: List[Union[MissionTarget, Unit]] = []
|
||||
self.targets: Sequence[Union[MissionTarget, Unit]] = []
|
||||
self.obj_name = ""
|
||||
self.pretty_name = ""
|
||||
self.only_for_player = False
|
||||
@@ -323,12 +322,12 @@ class Flight:
|
||||
def clear_roster(self) -> None:
|
||||
self.roster.clear()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.custom_name:
|
||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
|
||||
|
||||
@@ -16,17 +16,6 @@ from functools import cached_property
|
||||
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.planes import (
|
||||
E_3A,
|
||||
E_2C,
|
||||
A_50,
|
||||
IL_78M,
|
||||
KC130,
|
||||
KC135MPRS,
|
||||
KC_135,
|
||||
KJ_2000,
|
||||
S_3B_Tanker,
|
||||
)
|
||||
from dcs.unit import Unit
|
||||
from shapely.geometry import Point as ShapelyPoint
|
||||
|
||||
@@ -38,8 +27,9 @@ from game.theater import (
|
||||
MissionTarget,
|
||||
SamGroundObject,
|
||||
TheaterGroundObject,
|
||||
NavalControlPoint,
|
||||
)
|
||||
from game.theater.theatergroundobject import EwrGroundObject
|
||||
from game.theater.theatergroundobject import EwrGroundObject, NavalGroundObject
|
||||
from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
|
||||
from .closestairfields import ObjectiveDistanceCache
|
||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||
@@ -229,11 +219,7 @@ class FlightPlan:
|
||||
tot_waypoint = self.tot_waypoint
|
||||
if tot_waypoint is None:
|
||||
return None
|
||||
|
||||
time = self.tot
|
||||
if time is None:
|
||||
return None
|
||||
return time - self._travel_time_to_waypoint(tot_waypoint)
|
||||
return self.tot - self._travel_time_to_waypoint(tot_waypoint)
|
||||
|
||||
def startup_time(self) -> Optional[timedelta]:
|
||||
takeoff_time = self.takeoff_time()
|
||||
@@ -404,6 +390,9 @@ class PatrollingFlightPlan(FlightPlan):
|
||||
#: Maximum time to remain on station.
|
||||
patrol_duration: timedelta
|
||||
|
||||
#: Racetrack speed TAS.
|
||||
patrol_speed: Speed
|
||||
|
||||
#: The engagement range of any Search Then Engage task, or the radius of a
|
||||
#: Search Then Engage in Zone task. Any enemies of the appropriate type for
|
||||
#: this mission within this range of the flight's current position (or the
|
||||
@@ -785,9 +774,6 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
||||
divert: Optional[FlightWaypoint]
|
||||
bullseye: FlightWaypoint
|
||||
|
||||
#: Racetrack speed.
|
||||
patrol_speed: Speed
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.takeoff
|
||||
yield from self.nav_to
|
||||
@@ -1092,35 +1078,28 @@ class FlightPlanBuilder:
|
||||
|
||||
orbit_location = self.aewc_orbit(location)
|
||||
|
||||
# As high as possible to maximize detection and on-station time.
|
||||
if flight.unit_type == E_2C:
|
||||
patrol_alt = feet(30000)
|
||||
elif flight.unit_type == E_3A:
|
||||
patrol_alt = feet(35000)
|
||||
elif flight.unit_type == A_50:
|
||||
patrol_alt = feet(33000)
|
||||
elif flight.unit_type == KJ_2000:
|
||||
patrol_alt = feet(40000)
|
||||
if flight.unit_type.patrol_altitude is not None:
|
||||
patrol_alt = flight.unit_type.patrol_altitude
|
||||
else:
|
||||
patrol_alt = feet(25000)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
orbit_location = builder.orbit(orbit_location, patrol_alt)
|
||||
orbit = builder.orbit(orbit_location, patrol_alt)
|
||||
|
||||
return AwacsFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, orbit_location.position, patrol_alt
|
||||
flight.departure.position, orbit.position, patrol_alt
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
orbit_location.position, flight.arrival.position, patrol_alt
|
||||
orbit.position, flight.arrival.position, patrol_alt
|
||||
),
|
||||
land=builder.land(flight.arrival),
|
||||
divert=builder.divert(flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
hold=orbit_location,
|
||||
hold=orbit,
|
||||
hold_duration=timedelta(hours=4),
|
||||
)
|
||||
|
||||
@@ -1151,7 +1130,7 @@ class FlightPlanBuilder:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def anti_ship_targets_for_tgo(tgo: TheaterGroundObject) -> List[StrikeTarget]:
|
||||
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> List[StrikeTarget]:
|
||||
return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups]
|
||||
|
||||
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
|
||||
@@ -1164,12 +1143,9 @@ class FlightPlanBuilder:
|
||||
|
||||
from game.transfers import CargoShip
|
||||
|
||||
if isinstance(location, ControlPoint):
|
||||
if not location.is_fleet:
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
# The first group generated will be the carrier group itself.
|
||||
targets = self.anti_ship_targets_for_tgo(location.ground_objects[0])
|
||||
elif isinstance(location, TheaterGroundObject):
|
||||
if isinstance(location, NavalControlPoint):
|
||||
targets = self.anti_ship_targets_for_tgo(location.find_main_tgo())
|
||||
elif isinstance(location, NavalGroundObject):
|
||||
targets = self.anti_ship_targets_for_tgo(location)
|
||||
elif isinstance(location, CargoShip):
|
||||
targets = [StrikeTarget(location.name, location)]
|
||||
@@ -1191,21 +1167,28 @@ class FlightPlanBuilder:
|
||||
if isinstance(location, FrontLine):
|
||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||
|
||||
start, end = self.racetrack_for_objective(location, barcap=True)
|
||||
patrol_alt = meters(
|
||||
random.randint(
|
||||
int(self.doctrine.min_patrol_altitude.meters),
|
||||
int(self.doctrine.max_patrol_altitude.meters),
|
||||
)
|
||||
start_pos, end_pos = self.racetrack_for_objective(location, barcap=True)
|
||||
|
||||
preferred_alt = flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
|
||||
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
|
||||
logging.debug(
|
||||
f"BARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start, end = builder.race_track(start, end, patrol_alt)
|
||||
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
|
||||
|
||||
return BarCapFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
patrol_speed=patrol_speed,
|
||||
engagement_distance=self.doctrine.cap_engagement_range,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
@@ -1231,10 +1214,12 @@ class FlightPlanBuilder:
|
||||
target = self.package.target.position
|
||||
|
||||
heading = self.package.waypoints.join.heading_between_point(target)
|
||||
start = target.point_from_heading(heading, -self.doctrine.sweep_distance.meters)
|
||||
start_pos = target.point_from_heading(
|
||||
heading, -self.doctrine.sweep_distance.meters
|
||||
)
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
start, end = builder.sweep(start, target, self.doctrine.ingress_altitude)
|
||||
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
|
||||
|
||||
hold = builder.hold(self._hold_point(flight))
|
||||
|
||||
@@ -1428,11 +1413,15 @@ class FlightPlanBuilder:
|
||||
"""
|
||||
location = self.package.target
|
||||
|
||||
patrol_alt = meters(
|
||||
random.randint(
|
||||
int(self.doctrine.min_patrol_altitude.meters),
|
||||
int(self.doctrine.max_patrol_altitude.meters),
|
||||
)
|
||||
preferred_alt = flight.unit_type.preferred_patrol_altitude
|
||||
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||
patrol_alt = max(
|
||||
self.doctrine.min_patrol_altitude,
|
||||
min(self.doctrine.max_patrol_altitude, randomized_alt),
|
||||
)
|
||||
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
|
||||
logging.debug(
|
||||
f"TARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
|
||||
)
|
||||
|
||||
# Create points
|
||||
@@ -1455,6 +1444,7 @@ class FlightPlanBuilder:
|
||||
# requests an escort the CAP flight will remain on station for the
|
||||
# duration of the escorted mission, or until it is winchester/bingo.
|
||||
patrol_duration=self.doctrine.cap_duration,
|
||||
patrol_speed=patrol_speed,
|
||||
engagement_distance=self.doctrine.cap_engagement_range,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
|
||||
@@ -1623,16 +1613,33 @@ class FlightPlanBuilder:
|
||||
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
|
||||
# 2021-08-02: patrol_speed will currently have no effect because
|
||||
# CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected
|
||||
# to have patrol_speed
|
||||
is_helo = flight.unit_type.dcs_unit_type.helicopter
|
||||
ingress_egress_altitude = (
|
||||
self.doctrine.ingress_altitude if not is_helo else meters(50)
|
||||
)
|
||||
patrol_speed = flight.unit_type.preferred_patrol_speed(ingress_egress_altitude)
|
||||
use_agl_ingress_egress = is_helo
|
||||
|
||||
return CasFlightPlan(
|
||||
package=self.package,
|
||||
flight=flight,
|
||||
patrol_duration=self.doctrine.cas_duration,
|
||||
patrol_speed=patrol_speed,
|
||||
takeoff=builder.takeoff(flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
flight.departure.position, ingress, self.doctrine.ingress_altitude
|
||||
flight.departure.position,
|
||||
ingress,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
egress, flight.arrival.position, self.doctrine.ingress_altitude
|
||||
egress,
|
||||
flight.arrival.position,
|
||||
ingress_egress_altitude,
|
||||
use_agl_ingress_egress,
|
||||
),
|
||||
patrol_start=builder.ingress(
|
||||
FlightWaypointType.INGRESS_CAS, ingress, location
|
||||
@@ -1680,31 +1687,18 @@ class FlightPlanBuilder:
|
||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||
|
||||
tanker_type = flight.unit_type
|
||||
if tanker_type is KC_135:
|
||||
# ~300 knots IAS.
|
||||
speed = knots(445)
|
||||
altitude = feet(24000)
|
||||
elif tanker_type is KC135MPRS:
|
||||
# ~300 knots IAS.
|
||||
speed = knots(440)
|
||||
altitude = feet(23000)
|
||||
elif tanker_type is KC130:
|
||||
# ~210 knots IAS, roughly the max for the KC-130 at altitude.
|
||||
speed = knots(370)
|
||||
altitude = feet(22000)
|
||||
elif tanker_type is S_3B_Tanker:
|
||||
# ~265 knots IAS.
|
||||
speed = knots(320)
|
||||
altitude = feet(12000)
|
||||
elif tanker_type is IL_78M:
|
||||
# ~280 knots IAS.
|
||||
speed = knots(400)
|
||||
altitude = feet(21000)
|
||||
if tanker_type.patrol_altitude is not None:
|
||||
altitude = tanker_type.patrol_altitude
|
||||
else:
|
||||
# ~280 knots IAS.
|
||||
speed = knots(400)
|
||||
altitude = feet(21000)
|
||||
|
||||
# TODO: Could use flight.unit_type.preferred_patrol_speed(altitude) instead.
|
||||
if tanker_type.patrol_speed is not None:
|
||||
speed = tanker_type.patrol_speed
|
||||
else:
|
||||
# ~280 knots IAS at 21000.
|
||||
speed = knots(400)
|
||||
|
||||
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||
|
||||
return RefuelingFlightPlan(
|
||||
@@ -1903,23 +1897,23 @@ class FlightPlanBuilder:
|
||||
return self._retreating_rendezvous_point(attack_transition)
|
||||
return self._advancing_rendezvous_point(attack_transition)
|
||||
|
||||
def _ingress_point(self, heading: int) -> Point:
|
||||
def _ingress_point(self, heading: float) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 + 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _egress_point(self, heading: int) -> Point:
|
||||
def _egress_point(self, heading: float) -> Point:
|
||||
return self.package.target.position.point_from_heading(
|
||||
heading - 180 - 15, self.doctrine.ingress_egress_distance.meters
|
||||
)
|
||||
|
||||
def _target_heading_to_package_airfield(self) -> int:
|
||||
def _target_heading_to_package_airfield(self) -> float:
|
||||
return self._heading_to_package_airfield(self.package.target.position)
|
||||
|
||||
def _heading_to_package_airfield(self, point: Point) -> int:
|
||||
def _heading_to_package_airfield(self, point: Point) -> float:
|
||||
return self.package_airfield().position.heading_between_point(point)
|
||||
|
||||
def _distance_to_package_airfield(self, point: Point) -> int:
|
||||
def _distance_to_package_airfield(self, point: Point) -> float:
|
||||
return self.package_airfield().position.distance_to_point(point)
|
||||
|
||||
def package_airfield(self) -> ControlPoint:
|
||||
|
||||
@@ -19,7 +19,11 @@ class Loadout:
|
||||
is_custom: bool = False,
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.pylons = {k: v for k, v in pylons.items() if v is not None}
|
||||
# We clear unused pylon entries on initialization, but UI actions can still
|
||||
# cause a pylon to be emptied, so make the optional type explicit.
|
||||
self.pylons: Mapping[int, Optional[Weapon]] = {
|
||||
k: v for k, v in pylons.items() if v is not None
|
||||
}
|
||||
self.date = date
|
||||
self.is_custom = is_custom
|
||||
|
||||
@@ -92,6 +96,7 @@ class Loadout:
|
||||
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
||||
FlightType.STRIKE: ("STRIKE",),
|
||||
FlightType.ANTISHIP: ("ANTISHIP",),
|
||||
FlightType.DEAD: ("DEAD",),
|
||||
FlightType.SEAD: ("SEAD",),
|
||||
FlightType.BAI: ("BAI",),
|
||||
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
|
||||
@@ -133,4 +138,8 @@ class Loadout:
|
||||
)
|
||||
|
||||
# TODO: Try group.load_task_default_loadout(loadout_for_task)
|
||||
return cls.empty_loadout()
|
||||
|
||||
@classmethod
|
||||
def empty_loadout(cls) -> Loadout:
|
||||
return Loadout("Empty", {}, date=None)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
@@ -10,11 +11,12 @@ from typing import (
|
||||
TYPE_CHECKING,
|
||||
Tuple,
|
||||
Union,
|
||||
Any,
|
||||
)
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import Unit
|
||||
from dcs.unitgroup import Group, VehicleGroup
|
||||
from dcs.unitgroup import Group, VehicleGroup, ShipGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -33,7 +35,9 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
|
||||
@dataclass(frozen=True)
|
||||
class StrikeTarget:
|
||||
name: str
|
||||
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, MultiGroupTransport]
|
||||
target: Union[
|
||||
VehicleGroup, TheaterGroundObject[Any], Unit, ShipGroup, MultiGroupTransport
|
||||
]
|
||||
|
||||
|
||||
class WaypointBuilder:
|
||||
@@ -54,7 +58,7 @@ class WaypointBuilder:
|
||||
|
||||
@property
|
||||
def is_helo(self) -> bool:
|
||||
return getattr(self.flight.unit_type, "helicopter", False)
|
||||
return self.flight.unit_type.dcs_unit_type.helicopter
|
||||
|
||||
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
|
||||
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
||||
@@ -441,7 +445,7 @@ class WaypointBuilder:
|
||||
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||
# the escort flights a flight plan including the ingress point, target
|
||||
# area, and egress point.
|
||||
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||
ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||
|
||||
waypoint = FlightWaypoint(
|
||||
FlightWaypointType.TARGET_GROUP_LOC,
|
||||
@@ -455,8 +459,8 @@ class WaypointBuilder:
|
||||
waypoint.description = "Escort the package"
|
||||
waypoint.pretty_name = "Target area"
|
||||
|
||||
egress = self.egress(egress, target)
|
||||
return ingress, waypoint, egress
|
||||
egress_wp = self.egress(egress, target)
|
||||
return ingress_wp, waypoint, egress_wp
|
||||
|
||||
@staticmethod
|
||||
def pickup(control_point: ControlPoint) -> FlightWaypoint:
|
||||
|
||||
@@ -43,7 +43,7 @@ class ForcedOptionsGenerator:
|
||||
if blue.unrestricted_satnav or red.unrestricted_satnav:
|
||||
self.mission.forced_options.unrestricted_satnav = True
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
self._set_options_view()
|
||||
self._set_external_views()
|
||||
self._set_labels()
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater import ControlPoint
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
MAX_COMBAT_GROUP_PER_CP = 10
|
||||
|
||||
|
||||
@@ -52,10 +57,9 @@ class CombatGroup:
|
||||
self.unit_type = unit_type
|
||||
self.size = size
|
||||
self.role = role
|
||||
self.assigned_enemy_cp = None
|
||||
self.start_position = None
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
s = f"ROLE : {self.role}\n"
|
||||
if self.size:
|
||||
s += f"UNITS {self.unit_type} * {self.size}"
|
||||
@@ -63,7 +67,7 @@ class CombatGroup:
|
||||
|
||||
|
||||
class GroundPlanner:
|
||||
def __init__(self, cp: ControlPoint, game):
|
||||
def __init__(self, cp: ControlPoint, game: Game) -> None:
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.connected_enemy_cp = [
|
||||
@@ -83,17 +87,15 @@ class GroundPlanner:
|
||||
self.units_per_cp[cp.id] = []
|
||||
self.reserve: List[CombatGroup] = []
|
||||
|
||||
def plan_groundwar(self):
|
||||
def plan_groundwar(self) -> None:
|
||||
|
||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||
|
||||
remaining_available_frontline_units = ground_unit_limit
|
||||
|
||||
if hasattr(self.cp, "stance"):
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
||||
else:
|
||||
self.cp.stance = CombatStance.DEFENSIVE
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
|
||||
# TODO: Fix to handle the per-front stances.
|
||||
# https://github.com/dcs-liberation/dcs_liberation/issues/1417
|
||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
|
||||
|
||||
# Create combat groups and assign them randomly to each enemy CP
|
||||
for unit_type in self.cp.base.armor:
|
||||
@@ -152,20 +154,9 @@ class GroundPlanner:
|
||||
if len(self.connected_enemy_cp) > 0:
|
||||
enemy_cp = random.choice(self.connected_enemy_cp).id
|
||||
self.units_per_cp[enemy_cp].append(group)
|
||||
group.assigned_enemy_cp = enemy_cp
|
||||
else:
|
||||
self.reserve.append(group)
|
||||
group.assigned_enemy_cp = "__reserve__"
|
||||
collection.append(group)
|
||||
|
||||
if remaining_available_frontline_units == 0:
|
||||
break
|
||||
|
||||
print("------------------")
|
||||
print("Ground Planner : ")
|
||||
print(self.cp.name)
|
||||
print("------------------")
|
||||
for unit_type in self.units_per_cp.keys():
|
||||
print("For : #" + str(unit_type))
|
||||
for group in self.units_per_cp[unit_type]:
|
||||
print(str(group))
|
||||
|
||||
@@ -9,12 +9,23 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
|
||||
from typing import (
|
||||
Dict,
|
||||
Iterator,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
Type,
|
||||
List,
|
||||
TypeVar,
|
||||
Any,
|
||||
Generic,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dcs import Mission, Point, unitgroup
|
||||
from dcs.action import SceneryDestructionZone
|
||||
from dcs.country import Country
|
||||
from dcs.point import StaticPoint
|
||||
from dcs.point import StaticPoint, MovingPoint
|
||||
from dcs.statics import Fortification, fortification_map, warehouse_map
|
||||
from dcs.task import (
|
||||
ActivateBeaconCommand,
|
||||
@@ -26,12 +37,12 @@ from dcs.task import (
|
||||
from dcs.triggers import TriggerStart, TriggerZone
|
||||
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
|
||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
||||
from dcs.unittype import StaticType, UnitType
|
||||
from dcs.unittype import StaticType, UnitType, ShipType, VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game import db
|
||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
||||
from game.db import unit_type_from_name
|
||||
from game.db import unit_type_from_name, ship_type_from_name, vehicle_type_from_name
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
@@ -56,7 +67,10 @@ FARP_FRONTLINE_DISTANCE = 10000
|
||||
AA_CP_MIN_DISTANCE = 40000
|
||||
|
||||
|
||||
class GenericGroundObjectGenerator:
|
||||
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
|
||||
|
||||
|
||||
class GenericGroundObjectGenerator(Generic[TgoT]):
|
||||
"""An unspecialized ground object generator.
|
||||
|
||||
Currently used only for SAM
|
||||
@@ -64,7 +78,7 @@ class GenericGroundObjectGenerator:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ground_object: TheaterGroundObject,
|
||||
ground_object: TgoT,
|
||||
country: Country,
|
||||
game: Game,
|
||||
mission: Mission,
|
||||
@@ -89,10 +103,7 @@ class GenericGroundObjectGenerator:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
|
||||
|
||||
unit_type = vehicle_type_from_name(group.units[0].type)
|
||||
vg = self.m.vehicle_group(
|
||||
self.country,
|
||||
group.name,
|
||||
@@ -116,24 +127,27 @@ class GenericGroundObjectGenerator:
|
||||
self._register_unit_group(group, vg)
|
||||
|
||||
@staticmethod
|
||||
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
|
||||
if hasattr(unit_type, "eplrs"):
|
||||
if unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
||||
if unit_type.eplrs:
|
||||
group.points[0].tasks.append(EPLRS(group.id))
|
||||
|
||||
def set_alarm_state(self, group: Group) -> None:
|
||||
def set_alarm_state(self, group: Union[ShipGroup, VehicleGroup]) -> None:
|
||||
if self.game.settings.perf_red_alert_state:
|
||||
group.points[0].tasks.append(OptAlarmState(2))
|
||||
else:
|
||||
group.points[0].tasks.append(OptAlarmState(1))
|
||||
|
||||
def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
|
||||
def _register_unit_group(
|
||||
self,
|
||||
persistence_group: Union[ShipGroup, VehicleGroup],
|
||||
miz_group: Union[ShipGroup, VehicleGroup],
|
||||
) -> None:
|
||||
self.unit_map.add_ground_object_units(
|
||||
self.ground_object, persistence_group, miz_group
|
||||
)
|
||||
|
||||
|
||||
class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]):
|
||||
@property
|
||||
def culled(self) -> bool:
|
||||
# Don't cull missile sites - their range is long enough to make them easily
|
||||
@@ -148,7 +162,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
for group in self.ground_object.groups:
|
||||
vg = self.m.find_group(group.name)
|
||||
if vg is not None:
|
||||
targets = self.possible_missile_targets(vg)
|
||||
targets = self.possible_missile_targets()
|
||||
if targets:
|
||||
target = random.choice(targets)
|
||||
real_target = target.point_from_heading(
|
||||
@@ -165,7 +179,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
"Couldn't setup missile site to fire, group was not generated."
|
||||
)
|
||||
|
||||
def possible_missile_targets(self, vg: Group) -> List[Point]:
|
||||
def possible_missile_targets(self) -> List[Point]:
|
||||
"""
|
||||
Find enemy control points in range
|
||||
:param vg: Vehicle group we are searching a target for (There is always only oe group right now)
|
||||
@@ -174,7 +188,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
targets: List[Point] = []
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured != self.ground_object.control_point.captured:
|
||||
distance = cp.position.distance_to_point(vg.position)
|
||||
distance = cp.position.distance_to_point(self.ground_object.position)
|
||||
if distance < self.missile_site_range:
|
||||
targets.append(cp.position)
|
||||
return targets
|
||||
@@ -196,7 +210,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
|
||||
return site_range
|
||||
|
||||
|
||||
class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
|
||||
"""Generator for building sites.
|
||||
|
||||
Building sites are the primary type of non-airbase objective locations that
|
||||
@@ -225,7 +239,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
||||
f"{self.ground_object.dcs_identifier} not found in static maps"
|
||||
)
|
||||
|
||||
def generate_vehicle_group(self, unit_type: Type[UnitType]) -> None:
|
||||
def generate_vehicle_group(self, unit_type: Type[VehicleType]) -> None:
|
||||
if not self.ground_object.is_dead:
|
||||
group = self.m.vehicle_group(
|
||||
country=self.country,
|
||||
@@ -324,7 +338,7 @@ class SceneryGenerator(BuildingSiteGenerator):
|
||||
self.unit_map.add_scenery(scenery)
|
||||
|
||||
|
||||
class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundObject]):
|
||||
"""Base type for carrier group generation.
|
||||
|
||||
Used by both CV(N) groups and LHA groups.
|
||||
@@ -376,13 +390,12 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
|
||||
self._register_unit_group(group, ship_group)
|
||||
|
||||
def get_carrier_type(self, group: Group) -> Type[UnitType]:
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
|
||||
return unit_type
|
||||
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
|
||||
return ship_type_from_name(group.units[0].type)
|
||||
|
||||
def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
|
||||
def configure_carrier(
|
||||
self, group: ShipGroup, atc_channel: RadioFrequency
|
||||
) -> ShipGroup:
|
||||
unit_type = self.get_carrier_type(group)
|
||||
|
||||
ship_group = self.m.ship_group(
|
||||
@@ -474,7 +487,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
||||
class CarrierGenerator(GenericCarrierGenerator):
|
||||
"""Generator for CV(N) groups."""
|
||||
|
||||
def get_carrier_type(self, group: Group) -> UnitType:
|
||||
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
|
||||
unit_type = super().get_carrier_type(group)
|
||||
if self.game.settings.supercarrier:
|
||||
unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
|
||||
@@ -518,7 +531,7 @@ class LhaGenerator(GenericCarrierGenerator):
|
||||
)
|
||||
|
||||
|
||||
class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
class ShipObjectGenerator(GenericGroundObjectGenerator[ShipGroundObject]):
|
||||
"""Generator for non-carrier naval groups."""
|
||||
|
||||
def generate(self) -> None:
|
||||
@@ -529,14 +542,11 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
|
||||
if not group.units:
|
||||
logging.warning(f"Found empty group in {self.ground_object}")
|
||||
continue
|
||||
self.generate_group(group, ship_type_from_name(group.units[0].type))
|
||||
|
||||
unit_type = unit_type_from_name(group.units[0].type)
|
||||
if unit_type is None:
|
||||
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
|
||||
|
||||
self.generate_group(group, unit_type)
|
||||
|
||||
def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
|
||||
def generate_group(
|
||||
self, group_def: ShipGroup, first_unit_type: Type[ShipType]
|
||||
) -> None:
|
||||
group = self.m.ship_group(
|
||||
self.country,
|
||||
group_def.name,
|
||||
@@ -624,7 +634,7 @@ class GroundObjectsGenerator:
|
||||
self.icls_alloc = iter(range(1, 21))
|
||||
self.runways: Dict[str, RunwayData] = {}
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.captured:
|
||||
country_name = self.game.player_country
|
||||
@@ -637,6 +647,7 @@ class GroundObjectsGenerator:
|
||||
).generate()
|
||||
|
||||
for ground_object in cp.ground_objects:
|
||||
generator: GenericGroundObjectGenerator[Any]
|
||||
if isinstance(ground_object, FactoryGroundObject):
|
||||
generator = FactoryGenerator(
|
||||
ground_object, country, self.game, self.m, self.unit_map
|
||||
|
||||
@@ -37,6 +37,7 @@ from tabulate import tabulate
|
||||
from game.data.alic import AlicCodes
|
||||
from game.db import unit_type_from_name
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.savecompat import has_save_compat_for
|
||||
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.utils import meters
|
||||
@@ -63,7 +64,8 @@ class KneeboardPageWriter:
|
||||
else:
|
||||
self.foreground_fill = (15, 15, 15)
|
||||
self.background_fill = (255, 252, 252)
|
||||
self.image = Image.new("RGB", (768, 1024), self.background_fill)
|
||||
self.image_size = (768, 1024)
|
||||
self.image = Image.new("RGB", self.image_size, self.background_fill)
|
||||
# These font sizes create a relatively full page for current sorties. If
|
||||
# we start generating more complicated flight plans, or start including
|
||||
# more information in the comm ladder (the latter of which we should
|
||||
@@ -82,6 +84,7 @@ class KneeboardPageWriter:
|
||||
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
|
||||
)
|
||||
self.draw = ImageDraw.Draw(self.image)
|
||||
self.page_margin = page_margin
|
||||
self.x = page_margin
|
||||
self.y = page_margin
|
||||
self.line_spacing = line_spacing
|
||||
@@ -91,10 +94,24 @@ class KneeboardPageWriter:
|
||||
return self.x, self.y
|
||||
|
||||
def text(
|
||||
self, text: str, font=None, fill: Tuple[int, int, int] = (0, 0, 0)
|
||||
self,
|
||||
text: str,
|
||||
font: Optional[ImageFont.FreeTypeFont] = None,
|
||||
fill: Optional[Tuple[int, int, int]] = None,
|
||||
wrap: bool = False,
|
||||
) -> None:
|
||||
if font is None:
|
||||
font = self.content_font
|
||||
if fill is None:
|
||||
fill = self.foreground_fill
|
||||
|
||||
if wrap:
|
||||
text = "\n".join(
|
||||
self.wrap_line_with_font(
|
||||
line, self.image_size[0] - self.page_margin - self.x, font
|
||||
)
|
||||
for line in text.splitlines()
|
||||
)
|
||||
|
||||
self.draw.text(self.position, text, font=font, fill=fill)
|
||||
width, height = self.draw.textsize(text, font=font)
|
||||
@@ -134,6 +151,24 @@ class KneeboardPageWriter:
|
||||
output = combo
|
||||
return "".join(segments + [output]).strip()
|
||||
|
||||
@staticmethod
|
||||
def wrap_line_with_font(
|
||||
inputstr: str, max_width: int, font: ImageFont.FreeTypeFont
|
||||
) -> str:
|
||||
if font.getsize(inputstr)[0] <= max_width:
|
||||
return inputstr
|
||||
tokens = inputstr.split(" ")
|
||||
output = ""
|
||||
segments = []
|
||||
for token in tokens:
|
||||
combo = output + " " + token
|
||||
if font.getsize(combo)[0] > max_width:
|
||||
segments.append(output + "\n")
|
||||
output = token
|
||||
else:
|
||||
output = combo
|
||||
return "".join(segments + [output]).strip()
|
||||
|
||||
|
||||
class KneeboardPage:
|
||||
"""Base class for all kneeboard pages."""
|
||||
@@ -554,6 +589,24 @@ class StrikeTaskPage(KneeboardPage):
|
||||
]
|
||||
|
||||
|
||||
class NotesPage(KneeboardPage):
|
||||
"""A kneeboard page containing the campaign owner's notes."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
notes: str,
|
||||
dark_kneeboard: bool,
|
||||
) -> None:
|
||||
self.notes = notes
|
||||
self.dark_kneeboard = dark_kneeboard
|
||||
|
||||
def write(self, path: Path) -> None:
|
||||
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
|
||||
writer.title(f"Notes")
|
||||
writer.text(self.notes, wrap=True)
|
||||
writer.write(path)
|
||||
|
||||
|
||||
class KneeboardGenerator(MissionInfoGenerator):
|
||||
"""Creates kneeboard pages for each client flight in the mission."""
|
||||
|
||||
@@ -602,6 +655,7 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
return StrikeTaskPage(flight, self.dark_kneeboard, self.game.theater)
|
||||
return None
|
||||
|
||||
@has_save_compat_for(4)
|
||||
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
||||
"""Returns a list of kneeboard pages for the given flight."""
|
||||
pages: List[KneeboardPage] = [
|
||||
@@ -623,6 +677,10 @@ class KneeboardGenerator(MissionInfoGenerator):
|
||||
),
|
||||
]
|
||||
|
||||
# Only create the notes page if there are notes to show.
|
||||
if notes := getattr(self.game, "notes", ""):
|
||||
pages.append(NotesPage(notes, self.dark_kneeboard))
|
||||
|
||||
if (target_page := self.generate_task_page(flight)) is not None:
|
||||
pages.append(target_page)
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from typing import List
|
||||
|
||||
from gen.locations.preset_locations import PresetLocation
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetControlPointLocations:
|
||||
"""A repository of preset locations for a given control point"""
|
||||
|
||||
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7_Amphibious)
|
||||
ashore_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
|
||||
offshore_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# Possible antiship missiles sites locations (Represented in miz file by Iranian Silkworm missiles)
|
||||
antiship_locations: List[PresetLocation] = field(default_factory=list)
|
||||
|
||||
# List of possible powerplants locations (Represented in miz file by static Workshop A object, USA)
|
||||
powerplant_locations: List[PresetLocation] = field(default_factory=list)
|
||||
@@ -1,21 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from dcs import Point
|
||||
|
||||
|
||||
@dataclass
|
||||
class PresetLocation:
|
||||
"""A preset location"""
|
||||
|
||||
position: Point
|
||||
heading: int
|
||||
id: str
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"-" * 10
|
||||
+ "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(
|
||||
self.position.x, self.position.y, self.heading, self.id
|
||||
)
|
||||
+ "-" * 10
|
||||
)
|
||||
@@ -1,13 +1,20 @@
|
||||
import logging
|
||||
import random
|
||||
from game import db
|
||||
from typing import Optional
|
||||
|
||||
from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game import db, Game
|
||||
from game.theater.theatergroundobject import MissileSiteGroundObject
|
||||
from gen.missiles.scud_site import ScudGenerator
|
||||
from gen.missiles.v1_group import V1GroupGenerator
|
||||
|
||||
MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
|
||||
|
||||
|
||||
def generate_missile_group(game, ground_object, faction_name: str):
|
||||
def generate_missile_group(
|
||||
game: Game, ground_object: MissileSiteGroundObject, faction_name: str
|
||||
) -> Optional[VehicleGroup]:
|
||||
"""
|
||||
This generate a missiles group
|
||||
:return: Nothing, but put the group reference inside the ground object
|
||||
|
||||
@@ -2,15 +2,20 @@ import random
|
||||
|
||||
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import MissileSiteGroundObject
|
||||
from gen.sam.group_generator import VehicleGroupGenerator
|
||||
|
||||
|
||||
class ScudGenerator(GroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
|
||||
) -> None:
|
||||
super(ScudGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
# Scuds
|
||||
self.add_unit(
|
||||
|
||||
@@ -2,15 +2,20 @@ import random
|
||||
|
||||
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import MissileSiteGroundObject
|
||||
from gen.sam.group_generator import VehicleGroupGenerator
|
||||
|
||||
|
||||
class V1GroupGenerator(GroupGenerator):
|
||||
def __init__(self, game, ground_object, faction):
|
||||
class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
|
||||
def __init__(
|
||||
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
|
||||
) -> None:
|
||||
super(V1GroupGenerator, self).__init__(game, ground_object)
|
||||
self.faction = faction
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
# Ramps
|
||||
self.add_unit(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import random
|
||||
import time
|
||||
from typing import List
|
||||
from typing import List, Any
|
||||
|
||||
from dcs.country import Country
|
||||
|
||||
@@ -256,7 +256,7 @@ class NameGenerator:
|
||||
existing_alphas: List[str] = []
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
def reset(cls) -> None:
|
||||
cls.number = 0
|
||||
cls.infantry_number = 0
|
||||
cls.convoy_number = 0
|
||||
@@ -265,7 +265,7 @@ class NameGenerator:
|
||||
cls.existing_alphas = []
|
||||
|
||||
@classmethod
|
||||
def reset_numbers(cls):
|
||||
def reset_numbers(cls) -> None:
|
||||
cls.number = 0
|
||||
cls.infantry_number = 0
|
||||
cls.aircraft_number = 0
|
||||
@@ -273,7 +273,9 @@ class NameGenerator:
|
||||
cls.cargo_ship_number = 0
|
||||
|
||||
@classmethod
|
||||
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
|
||||
def next_aircraft_name(
|
||||
cls, country: Country, parent_base_id: int, flight: Flight
|
||||
) -> str:
|
||||
cls.aircraft_number += 1
|
||||
try:
|
||||
if flight.custom_name:
|
||||
@@ -293,7 +295,9 @@ class NameGenerator:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
|
||||
def next_unit_name(
|
||||
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
|
||||
) -> str:
|
||||
cls.number += 1
|
||||
return "unit|{}|{}|{}|{}|".format(
|
||||
country.id, cls.number, parent_base_id, unit_type.name
|
||||
@@ -301,8 +305,8 @@ class NameGenerator:
|
||||
|
||||
@classmethod
|
||||
def next_infantry_name(
|
||||
cls, country: Country, parent_base_id: int, unit_type: UnitType
|
||||
):
|
||||
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
|
||||
) -> str:
|
||||
cls.infantry_number += 1
|
||||
return "infantry|{}|{}|{}|{}|".format(
|
||||
country.id,
|
||||
@@ -312,17 +316,17 @@ class NameGenerator:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def next_awacs_name(cls, country: Country):
|
||||
def next_awacs_name(cls, country: Country) -> str:
|
||||
cls.number += 1
|
||||
return "awacs|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
@classmethod
|
||||
def next_tanker_name(cls, country: Country, unit_type: AircraftType):
|
||||
def next_tanker_name(cls, country: Country, unit_type: AircraftType) -> str:
|
||||
cls.number += 1
|
||||
return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
|
||||
|
||||
@classmethod
|
||||
def next_carrier_name(cls, country: Country):
|
||||
def next_carrier_name(cls, country: Country) -> str:
|
||||
cls.number += 1
|
||||
return "carrier|{}|{}|0|".format(country.id, cls.number)
|
||||
|
||||
@@ -337,7 +341,7 @@ class NameGenerator:
|
||||
return f"Cargo Ship {cls.cargo_ship_number:03}"
|
||||
|
||||
@classmethod
|
||||
def random_objective_name(cls):
|
||||
def random_objective_name(cls) -> str:
|
||||
if cls.animals:
|
||||
animal = random.choice(cls.animals)
|
||||
cls.animals.remove(animal)
|
||||
|
||||
@@ -15,7 +15,7 @@ class RadioFrequency:
|
||||
#: The frequency in kilohertz.
|
||||
hertz: int
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
if self.hertz >= 1000000:
|
||||
return self.format("MHz", 1000000)
|
||||
return self.format("kHz", 1000)
|
||||
|
||||
@@ -14,25 +14,21 @@ class BoforsGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Bofors AAA"
|
||||
price = 75
|
||||
|
||||
def generate(self):
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(2, 3)
|
||||
|
||||
spacing = random.randint(10, 40)
|
||||
def generate(self) -> None:
|
||||
|
||||
index = 0
|
||||
for i in range(grid_x):
|
||||
for j in range(grid_y):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.Bofors40,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
self.heading,
|
||||
)
|
||||
for i in range(4):
|
||||
spacing_x = random.randint(10, 40)
|
||||
spacing_y = random.randint(10, 40)
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.Bofors40,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing_x * i,
|
||||
self.position.y + spacing_y * i,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -23,31 +23,26 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Flak Site"
|
||||
price = 135
|
||||
|
||||
def generate(self):
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(2, 3)
|
||||
|
||||
spacing = random.randint(20, 35)
|
||||
|
||||
def generate(self) -> None:
|
||||
index = 0
|
||||
mixed = random.choice([True, False])
|
||||
unit_type = random.choice(GFLAK)
|
||||
|
||||
for i in range(grid_x):
|
||||
for j in range(grid_y):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
unit_type,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i + random.randint(1, 5),
|
||||
self.position.y + spacing * j + random.randint(1, 5),
|
||||
self.heading,
|
||||
)
|
||||
for i in range(4):
|
||||
index = index + 1
|
||||
spacing_x = random.randint(10, 40)
|
||||
spacing_y = random.randint(10, 40)
|
||||
self.add_unit(
|
||||
unit_type,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing_x * i + random.randint(1, 5),
|
||||
self.position.y + spacing_y * i + random.randint(1, 5),
|
||||
self.heading,
|
||||
)
|
||||
|
||||
if mixed:
|
||||
unit_type = random.choice(GFLAK)
|
||||
if mixed:
|
||||
unit_type = random.choice(GFLAK)
|
||||
|
||||
# Search lights
|
||||
search_pos = self.get_circular_position(random.randint(2, 3), 80)
|
||||
@@ -86,8 +81,10 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
|
||||
# Some Opel Blitz trucks
|
||||
for i in range(int(max(1, grid_x / 2))):
|
||||
for j in range(int(max(1, grid_x / 2))):
|
||||
index = 0
|
||||
for i in range(int(max(1, 2))):
|
||||
for j in range(int(max(1, 2))):
|
||||
index += 1
|
||||
self.add_unit(
|
||||
Unarmed.Blitz_36_6700A,
|
||||
"BLITZ#" + str(index),
|
||||
|
||||
@@ -14,9 +14,8 @@ class Flak18Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "WW2 Flak Site"
|
||||
price = 40
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
spacing = random.randint(30, 60)
|
||||
index = 0
|
||||
|
||||
@@ -13,12 +13,8 @@ class KS19Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "KS-19 AAA Site"
|
||||
price = 98
|
||||
|
||||
def generate(self):
|
||||
|
||||
spacing = random.randint(10, 40)
|
||||
|
||||
def generate(self) -> None:
|
||||
self.add_unit(
|
||||
highdigitsams.AAA_SON_9_Fire_Can,
|
||||
"TR",
|
||||
@@ -28,16 +24,17 @@ class KS19Generator(AirDefenseGroupGenerator):
|
||||
)
|
||||
|
||||
index = 0
|
||||
for i in range(3):
|
||||
for j in range(3):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
highdigitsams.AAA_100mm_KS_19,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
self.heading,
|
||||
)
|
||||
for i in range(4):
|
||||
spacing_x = random.randint(10, 40)
|
||||
spacing_y = random.randint(10, 40)
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
highdigitsams.AAA_100mm_KS_19,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing_x * i,
|
||||
self.position.y + spacing_y * i,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -14,9 +14,8 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "WW2 Ally Flak Site"
|
||||
price = 140
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
|
||||
for i, position in enumerate(positions):
|
||||
|
||||
@@ -12,10 +12,9 @@ class ZSU57Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "ZSU-57-2 Group"
|
||||
price = 60
|
||||
|
||||
def generate(self):
|
||||
num_launchers = 5
|
||||
def generate(self) -> None:
|
||||
num_launchers = 4
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=110, coverage=360
|
||||
)
|
||||
|
||||
@@ -14,25 +14,20 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Zu-23 Site"
|
||||
price = 56
|
||||
|
||||
def generate(self):
|
||||
grid_x = random.randint(2, 3)
|
||||
grid_y = random.randint(2, 3)
|
||||
|
||||
spacing = random.randint(10, 40)
|
||||
|
||||
def generate(self) -> None:
|
||||
index = 0
|
||||
for i in range(grid_x):
|
||||
for j in range(grid_y):
|
||||
index = index + 1
|
||||
self.add_unit(
|
||||
AirDefence.ZU_23_Closed_Insurgent,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing * i,
|
||||
self.position.y + spacing * j,
|
||||
self.heading,
|
||||
)
|
||||
for i in range(4):
|
||||
index = index + 1
|
||||
spacing_x = random.randint(10, 40)
|
||||
spacing_y = random.randint(10, 40)
|
||||
self.add_unit(
|
||||
AirDefence.ZU_23_Closed_Insurgent,
|
||||
"AAA#" + str(index),
|
||||
self.position.x + spacing_x * i,
|
||||
self.position.y + spacing_y * i,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import Iterator, List
|
||||
@@ -6,36 +8,69 @@ from dcs.unitgroup import VehicleGroup
|
||||
|
||||
from game import Game
|
||||
from game.theater.theatergroundobject import SamGroundObject
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from gen.sam.group_generator import VehicleGroupGenerator
|
||||
|
||||
|
||||
class SkynetRole(Enum):
|
||||
#: A radar SAM that should be controlled by Skynet.
|
||||
Sam = "Sam"
|
||||
|
||||
#: A radar SAM that should be controlled and used as an EWR by Skynet.
|
||||
SamAsEwr = "SamAsEwr"
|
||||
|
||||
#: An air defense unit that should be used as point defense by Skynet.
|
||||
PointDefense = "PD"
|
||||
|
||||
#: All other types of groups that might be present in a SAM TGO. This includes
|
||||
#: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet
|
||||
#: should use this role.
|
||||
NoSkynetBehavior = "NoSkynetBehavior"
|
||||
|
||||
|
||||
class AirDefenseRange(Enum):
|
||||
AAA = "AAA"
|
||||
Short = "short"
|
||||
Medium = "medium"
|
||||
Long = "long"
|
||||
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
|
||||
Short = ("short", SkynetRole.NoSkynetBehavior)
|
||||
Medium = ("medium", SkynetRole.Sam)
|
||||
Long = ("long", SkynetRole.SamAsEwr)
|
||||
|
||||
def __init__(self, description: str, default_role: SkynetRole) -> None:
|
||||
self.range_name = description
|
||||
self.default_role = default_role
|
||||
|
||||
|
||||
class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
||||
class AirDefenseGroupGenerator(VehicleGroupGenerator[SamGroundObject], ABC):
|
||||
"""
|
||||
This is the base for all SAM group generators
|
||||
"""
|
||||
|
||||
price: int
|
||||
|
||||
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
|
||||
ground_object.skynet_capable = True
|
||||
super().__init__(game, ground_object)
|
||||
|
||||
self.vg.name = self.group_name_for_role(self.vg.id, self.primary_group_role())
|
||||
self.auxiliary_groups: List[VehicleGroup] = []
|
||||
self.heading = self.heading_to_conflict()
|
||||
|
||||
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
|
||||
group = VehicleGroup(
|
||||
self.game.next_group_id(), "|".join([self.go.group_name, name_suffix])
|
||||
)
|
||||
def add_auxiliary_group(self, role: SkynetRole) -> VehicleGroup:
|
||||
gid = self.game.next_group_id()
|
||||
group = VehicleGroup(gid, self.group_name_for_role(gid, role))
|
||||
self.auxiliary_groups.append(group)
|
||||
return group
|
||||
|
||||
def group_name_for_role(self, gid: int, role: SkynetRole) -> str:
|
||||
if role is SkynetRole.NoSkynetBehavior:
|
||||
# No special naming needed for air defense groups that don't participate in
|
||||
# Skynet.
|
||||
return f"{self.go.group_name}|{gid}"
|
||||
|
||||
# For those that do, we need a prefix of `$COLOR|SAM| so our Skynet config picks
|
||||
# the group up at all. To support PDs we need to append the ID of the TGO so
|
||||
# that the PD will know which group it's protecting. We then append the role so
|
||||
# our config knows what to do with the group, and finally the GID of *this*
|
||||
# group to ensure no conflicts.
|
||||
return "|".join(
|
||||
[self.go.faction_color, "SAM", str(self.go.group_id), role.value, str(gid)]
|
||||
)
|
||||
|
||||
def get_generated_group(self) -> VehicleGroup:
|
||||
raise RuntimeError(
|
||||
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
|
||||
@@ -52,3 +87,7 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
||||
@abstractmethod
|
||||
def range(cls) -> AirDefenseRange:
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def primary_group_role(cls) -> SkynetRole:
|
||||
return cls.range().default_role
|
||||
|
||||
@@ -17,9 +17,8 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Early Cold War Flak Site"
|
||||
price = 74
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
spacing = random.randint(30, 60)
|
||||
index = 0
|
||||
@@ -90,9 +89,8 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Cold War Flak Site"
|
||||
price = 72
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
spacing = random.randint(30, 60)
|
||||
index = 0
|
||||
|
||||
@@ -18,6 +18,7 @@ from gen.sam.ewrs import (
|
||||
StraightFlushGenerator,
|
||||
TallRackGenerator,
|
||||
EwrGenerator,
|
||||
TinShieldGenerator,
|
||||
)
|
||||
|
||||
EWR_MAP = {
|
||||
@@ -31,6 +32,7 @@ EWR_MAP = {
|
||||
"SnowDriftGenerator": SnowDriftGenerator,
|
||||
"StraightFlushGenerator": StraightFlushGenerator,
|
||||
"HawkEwrGenerator": HawkEwrGenerator,
|
||||
"TinShieldGenerator": TinShieldGenerator,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
from typing import Type
|
||||
|
||||
from dcs.vehicles import AirDefence
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
from gen.sam.group_generator import GroupGenerator
|
||||
from game.theater.theatergroundobject import EwrGroundObject
|
||||
from gen.sam.group_generator import VehicleGroupGenerator
|
||||
|
||||
|
||||
class EwrGenerator(GroupGenerator):
|
||||
class EwrGenerator(VehicleGroupGenerator[EwrGroundObject]):
|
||||
unit_type: Type[VehicleType]
|
||||
|
||||
@classmethod
|
||||
def name(cls) -> str:
|
||||
return cls.unit_type.name
|
||||
|
||||
@staticmethod
|
||||
def price() -> int:
|
||||
# TODO: Differentiate sites.
|
||||
return 20
|
||||
|
||||
def generate(self) -> None:
|
||||
self.add_unit(
|
||||
self.unit_type, "EWR", self.position.x, self.position.y, self.heading
|
||||
self.unit_type,
|
||||
"EWR",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading_to_conflict(),
|
||||
)
|
||||
|
||||
|
||||
@@ -106,3 +106,9 @@ class HawkEwrGenerator(EwrGenerator):
|
||||
"""
|
||||
|
||||
unit_type = AirDefence.Hawk_sr
|
||||
|
||||
|
||||
class TinShieldGenerator(EwrGenerator):
|
||||
"""19ZH6 "Tin Shield" EWR."""
|
||||
|
||||
unit_type = AirDefence.RLS_19J6
|
||||
|
||||
@@ -12,9 +12,8 @@ class FreyaGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Freya EWR Site"
|
||||
price = 60
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
|
||||
# TODO : would be better with the Concrete structure that is supposed to protect it
|
||||
self.add_unit(
|
||||
|
||||
@@ -1,58 +1,116 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import operator
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Type
|
||||
from collections import Iterable
|
||||
from typing import TYPE_CHECKING, Type, TypeVar, Generic, Any
|
||||
|
||||
from dcs import unitgroup
|
||||
from dcs.mapping import Point
|
||||
from dcs.point import PointAction
|
||||
from dcs.unit import Ship, Vehicle
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.unit import Ship, Vehicle, Unit
|
||||
from dcs.unitgroup import ShipGroup, VehicleGroup
|
||||
from dcs.unittype import VehicleType, UnitType, ShipType
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.factions.faction import Faction
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from game.theater import MissionTarget
|
||||
from game.theater.theatergroundobject import TheaterGroundObject, NavalGroundObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.game import Game
|
||||
|
||||
|
||||
GroupT = TypeVar("GroupT", VehicleGroup, ShipGroup)
|
||||
UnitT = TypeVar("UnitT", bound=Unit)
|
||||
UnitTypeT = TypeVar("UnitTypeT", bound=Type[UnitType])
|
||||
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
|
||||
|
||||
|
||||
# TODO: Generate a group description rather than a pydcs group.
|
||||
# It appears that all of this work gets redone at miz generation time (see
|
||||
# groundobjectsgen for an example). We can do less work and include the data we
|
||||
# care about in the format we want if we just generate our own group description
|
||||
# types rather than pydcs groups.
|
||||
class GroupGenerator:
|
||||
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
|
||||
class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]):
|
||||
def __init__(self, game: Game, ground_object: TgoT, group: GroupT) -> None:
|
||||
self.game = game
|
||||
self.go = ground_object
|
||||
self.position = ground_object.position
|
||||
self.heading = random.randint(0, 359)
|
||||
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
|
||||
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
|
||||
wp.ETA_locked = True
|
||||
self.price = 0
|
||||
self.vg: GroupT = group
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_generated_group(self) -> unitgroup.VehicleGroup:
|
||||
def get_generated_group(self) -> GroupT:
|
||||
return self.vg
|
||||
|
||||
def add_unit(
|
||||
self,
|
||||
unit_type: Type[VehicleType],
|
||||
unit_type: UnitTypeT,
|
||||
name: str,
|
||||
pos_x: float,
|
||||
pos_y: float,
|
||||
heading: int,
|
||||
) -> Vehicle:
|
||||
) -> UnitT:
|
||||
return self.add_unit_to_group(
|
||||
self.vg, unit_type, name, Point(pos_x, pos_y), heading
|
||||
)
|
||||
|
||||
def add_unit_to_group(
|
||||
self,
|
||||
group: unitgroup.VehicleGroup,
|
||||
group: GroupT,
|
||||
unit_type: UnitTypeT,
|
||||
name: str,
|
||||
position: Point,
|
||||
heading: int,
|
||||
) -> UnitT:
|
||||
raise NotImplementedError
|
||||
|
||||
def heading_to_conflict(self) -> int:
|
||||
# Heading for a Group to the enemy.
|
||||
# Should be the point between the nearest and the most distant conflict
|
||||
conflicts: dict[MissionTarget, float] = {}
|
||||
|
||||
for conflict in self.game.theater.conflicts():
|
||||
conflicts[conflict] = conflict.distance_to(self.go)
|
||||
|
||||
if len(conflicts) == 0:
|
||||
return self.heading
|
||||
|
||||
closest_conflict = min(conflicts.items(), key=operator.itemgetter(1))[0]
|
||||
most_distant_conflict = max(conflicts.items(), key=operator.itemgetter(1))[0]
|
||||
|
||||
conflict_center = Point(
|
||||
(closest_conflict.position.x + most_distant_conflict.position.x) / 2,
|
||||
(closest_conflict.position.y + most_distant_conflict.position.y) / 2,
|
||||
)
|
||||
|
||||
return int(self.go.position.heading_between_point(conflict_center))
|
||||
|
||||
|
||||
class VehicleGroupGenerator(
|
||||
Generic[TgoT], GroupGenerator[VehicleGroup, Vehicle, Type[VehicleType], TgoT]
|
||||
):
|
||||
def __init__(self, game: Game, ground_object: TgoT) -> None:
|
||||
super().__init__(
|
||||
game,
|
||||
ground_object,
|
||||
unitgroup.VehicleGroup(game.next_group_id(), ground_object.group_name),
|
||||
)
|
||||
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
|
||||
wp.ETA_locked = True
|
||||
|
||||
def generate(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def add_unit_to_group(
|
||||
self,
|
||||
group: VehicleGroup,
|
||||
unit_type: Type[VehicleType],
|
||||
name: str,
|
||||
position: Point,
|
||||
@@ -62,9 +120,19 @@ class GroupGenerator:
|
||||
unit.position = position
|
||||
unit.heading = heading
|
||||
group.add_unit(unit)
|
||||
|
||||
# get price of unit to calculate the real price of the whole group
|
||||
try:
|
||||
ground_unit_type = next(GroundUnitType.for_dcs_type(unit_type))
|
||||
self.price += ground_unit_type.price
|
||||
except StopIteration:
|
||||
logging.error(f"Cannot get price for unit {unit_type.name}")
|
||||
|
||||
return unit
|
||||
|
||||
def get_circular_position(self, num_units, launcher_distance, coverage=90):
|
||||
def get_circular_position(
|
||||
self, num_units: int, launcher_distance: int, coverage: int = 90
|
||||
) -> Iterable[tuple[float, float, int]]:
|
||||
"""
|
||||
Given a position on the map, array a group of units in a circle a uniform distance from the unit
|
||||
:param num_units:
|
||||
@@ -90,39 +158,47 @@ class GroupGenerator:
|
||||
else:
|
||||
current_offset = self.heading
|
||||
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
|
||||
for x in range(1, num_units + 1):
|
||||
positions.append(
|
||||
(
|
||||
self.position.x
|
||||
+ launcher_distance * math.cos(math.radians(current_offset)),
|
||||
self.position.y
|
||||
+ launcher_distance * math.sin(math.radians(current_offset)),
|
||||
current_offset,
|
||||
)
|
||||
for _ in range(1, num_units + 1):
|
||||
x: float = self.position.x + launcher_distance * math.cos(
|
||||
math.radians(current_offset)
|
||||
)
|
||||
y: float = self.position.y + launcher_distance * math.sin(
|
||||
math.radians(current_offset)
|
||||
)
|
||||
heading = current_offset
|
||||
positions.append((x, y, int(heading)))
|
||||
current_offset += outer_offset
|
||||
return positions
|
||||
|
||||
|
||||
class ShipGroupGenerator(GroupGenerator):
|
||||
class ShipGroupGenerator(
|
||||
GroupGenerator[ShipGroup, Ship, Type[ShipType], NavalGroundObject]
|
||||
):
|
||||
"""Abstract class for other ship generator classes"""
|
||||
|
||||
def __init__(
|
||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
||||
):
|
||||
self.game = game
|
||||
self.go = ground_object
|
||||
self.position = ground_object.position
|
||||
self.heading = random.randint(0, 359)
|
||||
def __init__(self, game: Game, ground_object: NavalGroundObject, faction: Faction):
|
||||
super().__init__(
|
||||
game,
|
||||
ground_object,
|
||||
unitgroup.ShipGroup(game.next_group_id(), ground_object.group_name),
|
||||
)
|
||||
self.faction = faction
|
||||
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.go.group_name)
|
||||
wp = self.vg.add_waypoint(self.position, 0)
|
||||
wp.ETA_locked = True
|
||||
|
||||
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
|
||||
def generate(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def add_unit_to_group(
|
||||
self,
|
||||
group: ShipGroup,
|
||||
unit_type: Type[ShipType],
|
||||
name: str,
|
||||
position: Point,
|
||||
heading: int,
|
||||
) -> Ship:
|
||||
unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
|
||||
unit.position.x = pos_x
|
||||
unit.position.y = pos_y
|
||||
unit.position = position
|
||||
unit.heading = heading
|
||||
self.vg.add_unit(unit)
|
||||
group.add_unit(unit)
|
||||
return unit
|
||||
|
||||
@@ -14,10 +14,9 @@ class AvengerGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Avenger Group"
|
||||
price = 62
|
||||
|
||||
def generate(self):
|
||||
num_launchers = random.randint(2, 3)
|
||||
def generate(self) -> None:
|
||||
num_launchers = 2
|
||||
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
|
||||
@@ -14,10 +14,9 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Chaparral Group"
|
||||
price = 66
|
||||
|
||||
def generate(self):
|
||||
num_launchers = random.randint(2, 4)
|
||||
def generate(self) -> None:
|
||||
num_launchers = 2
|
||||
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
|
||||
@@ -14,23 +14,20 @@ class GepardGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Gepard Group"
|
||||
price = 50
|
||||
|
||||
def generate(self):
|
||||
self.add_unit(
|
||||
AirDefence.Gepard,
|
||||
"SPAAA",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
def generate(self) -> None:
|
||||
num_launchers = 2
|
||||
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=120, coverage=180
|
||||
)
|
||||
if random.randint(0, 1) == 1:
|
||||
for i, position in enumerate(positions):
|
||||
self.add_unit(
|
||||
AirDefence.Gepard,
|
||||
"SPAAA2",
|
||||
self.position.x,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
"SPAA#" + str(i),
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
)
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
|
||||
@@ -28,6 +28,7 @@ from gen.sam.sam_gepard import GepardGenerator
|
||||
from gen.sam.sam_hawk import HawkGenerator
|
||||
from gen.sam.sam_hq7 import HQ7Generator
|
||||
from gen.sam.sam_linebacker import LinebackerGenerator
|
||||
from gen.sam.sam_nasams import NasamBGenerator, NasamCGenerator
|
||||
from gen.sam.sam_patriot import PatriotGenerator
|
||||
from gen.sam.sam_rapier import RapierGenerator
|
||||
from gen.sam.sam_roland import RolandGenerator
|
||||
@@ -100,6 +101,8 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
|
||||
"SA20Generator": SA20Generator,
|
||||
"SA20BGenerator": SA20BGenerator,
|
||||
"SA23Generator": SA23Generator,
|
||||
"NasamBGenerator": NasamBGenerator,
|
||||
"NasamCGenerator": NasamCGenerator,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from dcs.vehicles import AirDefence
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
SkynetRole,
|
||||
)
|
||||
|
||||
|
||||
@@ -15,9 +16,8 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Hawk Site"
|
||||
price = 115
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
self.add_unit(
|
||||
AirDefence.Hawk_sr,
|
||||
"SR",
|
||||
@@ -41,7 +41,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
||||
)
|
||||
|
||||
# Triple A for close range defense
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
|
||||
self.add_unit_to_group(
|
||||
aa_group,
|
||||
AirDefence.Vulcan,
|
||||
@@ -50,7 +50,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
||||
self.heading,
|
||||
)
|
||||
|
||||
num_launchers = random.randint(3, 6)
|
||||
num_launchers = 6
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=120, coverage=180
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ from dcs.vehicles import AirDefence
|
||||
from gen.sam.airdefensegroupgenerator import (
|
||||
AirDefenseRange,
|
||||
AirDefenseGroupGenerator,
|
||||
SkynetRole,
|
||||
)
|
||||
|
||||
|
||||
@@ -15,9 +16,8 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "HQ-7 Site"
|
||||
price = 120
|
||||
|
||||
def generate(self):
|
||||
def generate(self) -> None:
|
||||
self.add_unit(
|
||||
AirDefence.HQ_7_STR_SP,
|
||||
"STR",
|
||||
@@ -25,16 +25,9 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
self.add_unit(
|
||||
AirDefence.HQ_7_LN_SP,
|
||||
"LN",
|
||||
self.position.x + 20,
|
||||
self.position.y,
|
||||
self.heading,
|
||||
)
|
||||
|
||||
# Triple A for close range defense
|
||||
aa_group = self.add_auxiliary_group("AA")
|
||||
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
|
||||
self.add_unit_to_group(
|
||||
aa_group,
|
||||
AirDefence.Ural_375_ZU_23,
|
||||
@@ -50,7 +43,7 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
||||
self.heading,
|
||||
)
|
||||
|
||||
num_launchers = random.randint(0, 3)
|
||||
num_launchers = 2
|
||||
if num_launchers > 0:
|
||||
positions = self.get_circular_position(
|
||||
num_launchers, launcher_distance=120, coverage=360
|
||||
|
||||
@@ -14,10 +14,9 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
|
||||
"""
|
||||
|
||||
name = "Linebacker Group"
|
||||
price = 75
|
||||
|
||||
def generate(self):
|
||||
num_launchers = random.randint(2, 4)
|
||||
def generate(self) -> None:
|
||||
num_launchers = 2
|
||||
|
||||
self.add_unit(
|
||||
Unarmed.M_818,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user