mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
171 Commits
8.1.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
|
# 4.0.0
|
||||||
|
|
||||||
Saves from 3.x are not compatible with 4.0.
|
Saves from 3.x are not compatible with 4.0.
|
||||||
|
|
||||||
## Features/Improvements
|
## 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 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]** AI will plan Tanker flights.
|
||||||
* **[Campaign AI]** Removed max distance for AEW&C auto planning.
|
* **[Campaign AI]** Removed max distance for AEW&C auto planning.
|
||||||
* **[Economy]** Adjusted prices for aircraft to balance out some price inconsistencies.
|
* **[Economy]** Adjusted prices for aircraft to balance out some price inconsistencies.
|
||||||
* **[Factions]** Added more tankers to factions.
|
* **[Factions]** Added more tankers to factions.
|
||||||
* **[Flight Planner]** Added ability to plan Tankers.
|
* **[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]** 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.
|
* **[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]** 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]** 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]** Carriers and LHAs now match the colour of airfields, and their destination icons are translucent.
|
||||||
* **[UI]** Updated intel box text for first turn.
|
* **[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
|
## Fixes
|
||||||
|
|
||||||
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
|
* **[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 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 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 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]** Made non-interactive map elements less obstructive.
|
||||||
* **[UI]** Added support for Neutral Dot difficulty label
|
* **[UI]** Added support for Neutral Dot difficulty label
|
||||||
* **[UI]** Clear skies at night no longer described as "Sunny" by the weather widget.
|
* **[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.SNR_75V.id: 126,
|
||||||
AirDefence.HQ_7_LN_SP.id: 127,
|
AirDefence.HQ_7_LN_SP.id: 127,
|
||||||
AirDefence.HQ_7_STR_SP.id: 128,
|
AirDefence.HQ_7_STR_SP.id: 128,
|
||||||
|
AirDefence.RLS_19J6.id: 130,
|
||||||
AirDefence.Roland_ADS.id: 201,
|
AirDefence.Roland_ADS.id: 201,
|
||||||
AirDefence.Patriot_str.id: 202,
|
AirDefence.Patriot_str.id: 202,
|
||||||
AirDefence.Hawk_sr.id: 203,
|
AirDefence.Hawk_sr.id: 203,
|
||||||
@@ -33,6 +34,7 @@ class AlicCodes:
|
|||||||
AirDefence.Hawk_cwar.id: 206,
|
AirDefence.Hawk_cwar.id: 206,
|
||||||
AirDefence.Gepard.id: 207,
|
AirDefence.Gepard.id: 207,
|
||||||
AirDefence.Vulcan.id: 208,
|
AirDefence.Vulcan.id: 208,
|
||||||
|
AirDefence.NASAMS_Radar_MPQ64F1.id: 209,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@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
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
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.unitgroup import FlyingGroup
|
||||||
from dcs.weapons_data import Weapons, weapon_ids
|
from dcs.weapons_data import Weapons, weapon_ids
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
|
|
||||||
PydcsWeapon = Dict[str, Union[int, str]]
|
PydcsWeapon = Any
|
||||||
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
|
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
|
||||||
|
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class Pylon:
|
|||||||
# configuration.
|
# configuration.
|
||||||
return weapon in self.allowed or weapon.cls_id == "<CLEAN>"
|
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):
|
if not self.can_equip(weapon):
|
||||||
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
|
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
|
||||||
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
|
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.ALQ_184): 1989,
|
||||||
Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984,
|
Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984,
|
||||||
# TGP Pods
|
# TGP Pods
|
||||||
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod): 1995,
|
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod): 1999,
|
||||||
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod_): 1995,
|
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod_): 1999,
|
||||||
Weapon.from_pydcs(Weapons.AN_ASQ_228_ATFLIR___Targeting_Pod): 1993,
|
Weapon.from_pydcs(Weapons.AN_ASQ_228_ATFLIR___Targeting_Pod): 2003,
|
||||||
Weapon.from_pydcs(
|
Weapon.from_pydcs(
|
||||||
Weapons.AN_ASQ_173_Laser_Spot_Tracker_Strike_CAMera__LST_SCAM_
|
Weapons.AN_ASQ_173_Laser_Spot_Tracker_Strike_CAMera__LST_SCAM_
|
||||||
): 1993,
|
): 1993,
|
||||||
Weapon.from_pydcs(Weapons.AWW_13_DATALINK_POD): 1967,
|
Weapon.from_pydcs(Weapons.AWW_13_DATALINK_POD): 1967,
|
||||||
Weapon.from_pydcs(Weapons.LANTIRN_Targeting_Pod): 1985,
|
Weapon.from_pydcs(Weapons.LANTIRN_Targeting_Pod): 1990,
|
||||||
Weapon.from_pydcs(Weapons.Lantirn_F_16): 1985,
|
Weapon.from_pydcs(Weapons.Lantirn_F_16): 1990,
|
||||||
Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1985,
|
Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1990,
|
||||||
Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982,
|
Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982,
|
||||||
# BLU-107
|
# BLU-107
|
||||||
Weapon.from_pydcs(Weapons.BLU_107___440lb_Anti_Runway_Penetrator_Bomb): 1983,
|
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,
|
CV_1143_5,
|
||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport
|
from dcs.terrain.terrain import Airport
|
||||||
|
from dcs.unit import Ship
|
||||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||||
from dcs.unittype import UnitType
|
from dcs.unittype import UnitType, FlyingType, ShipType, VehicleType
|
||||||
from dcs.vehicles import (
|
from dcs.vehicles import (
|
||||||
vehicle_map,
|
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.f22a.f22a import F_22A
|
||||||
from pydcs_extensions.hercules.hercules import Hercules
|
from pydcs_extensions.hercules.hercules import Hercules
|
||||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
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
|
from pydcs_extensions.su57.su57 import Su_57
|
||||||
|
|
||||||
plane_map["A-4E-C"] = A_4E_C
|
plane_map["A-4E-C"] = A_4E_C
|
||||||
plane_map["F-22A"] = F_22A
|
plane_map["F-22A"] = F_22A
|
||||||
plane_map["MB-339PAN"] = MB_339PAN
|
|
||||||
plane_map["Su-57"] = Su_57
|
plane_map["Su-57"] = Su_57
|
||||||
plane_map["Hercules"] = Hercules
|
plane_map["Hercules"] = Hercules
|
||||||
plane_map["JAS39Gripen"] = JAS39Gripen
|
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_vert"] = frenchpack.DIM__TOYOTA_GREEN
|
||||||
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
|
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
|
||||||
vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
|
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_SON_9_Fire_Can.id] = highdigitsams.AAA_SON_9_Fire_Can
|
||||||
vehicle_map[highdigitsams.AAA_100mm_KS_19.id] = highdigitsams.AAA_100mm_KS_19
|
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)
|
`Identifier` is aircraft identifier (as used troughout the file) and "LiveryName" (with double quotes)
|
||||||
is livery name as found in mission editor.
|
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
|
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]
|
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 unit == Stennis:
|
||||||
if name == "CVN-71 Theodore Roosevelt":
|
if name == "CVN-71 Theodore Roosevelt":
|
||||||
return CVN_71
|
return CVN_71
|
||||||
@@ -353,7 +360,15 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
|
|||||||
return None
|
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():
|
for k, v in country_dict.items():
|
||||||
if v.name == name:
|
if v.name == name:
|
||||||
return k
|
return k
|
||||||
@@ -366,7 +381,7 @@ class DefaultLiveries:
|
|||||||
|
|
||||||
|
|
||||||
OH_58D.Liveries = DefaultLiveries
|
OH_58D.Liveries = DefaultLiveries
|
||||||
F_16C_50.Liveries = DefaultLiveries
|
F_16C_50.Liveries = DefaultLiveries # type: ignore
|
||||||
P_51D_30_NA.Liveries = DefaultLiveries
|
P_51D_30_NA.Liveries = DefaultLiveries
|
||||||
Ju_88A4.Liveries = DefaultLiveries
|
Ju_88A4.Liveries = DefaultLiveries
|
||||||
B_17G.Liveries = DefaultLiveries
|
B_17G.Liveries = DefaultLiveries
|
||||||
|
|||||||
@@ -29,7 +29,14 @@ from game.radio.channels import (
|
|||||||
ViggenRadioChannelAllocator,
|
ViggenRadioChannelAllocator,
|
||||||
NoOpChannelAllocator,
|
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:
|
if TYPE_CHECKING:
|
||||||
from gen.aircraft import FlightData
|
from gen.aircraft import FlightData
|
||||||
@@ -91,11 +98,34 @@ class RadioConfig:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@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
|
carrier_capable: bool
|
||||||
lha_capable: bool
|
lha_capable: bool
|
||||||
always_keeps_gun: 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
|
max_group_size: int
|
||||||
|
patrol_altitude: Optional[Distance]
|
||||||
|
patrol_speed: Optional[Speed]
|
||||||
intra_flight_radio: Optional[Radio]
|
intra_flight_radio: Optional[Radio]
|
||||||
channel_allocator: Optional[RadioChannelAllocator]
|
channel_allocator: Optional[RadioChannelAllocator]
|
||||||
channel_namer: Type[ChannelNamer]
|
channel_namer: Type[ChannelNamer]
|
||||||
@@ -121,13 +151,86 @@ class AircraftType(UnitType[FlyingType]):
|
|||||||
def max_speed(self) -> Speed:
|
def max_speed(self) -> Speed:
|
||||||
return kph(self.dcs_unit_type.max_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:
|
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:
|
if self.intra_flight_radio is not None:
|
||||||
return radio_registry.alloc_for_radio(self.intra_flight_radio)
|
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:
|
try:
|
||||||
radio_registry.reserve(freq)
|
radio_registry.reserve(freq)
|
||||||
except ChannelInUseError:
|
except ChannelInUseError:
|
||||||
@@ -162,6 +265,8 @@ class AircraftType(UnitType[FlyingType]):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
|
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]
|
yield from cls._by_unit_type[dcs_unit_type]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -183,7 +288,7 @@ class AircraftType(UnitType[FlyingType]):
|
|||||||
logging.warning(f"No data for {aircraft.id}; it will not be available")
|
logging.warning(f"No data for {aircraft.id}; it will not be available")
|
||||||
return
|
return
|
||||||
|
|
||||||
with data_path.open() as data_file:
|
with data_path.open(encoding="utf-8") as data_file:
|
||||||
data = yaml.safe_load(data_file)
|
data = yaml.safe_load(data_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -192,6 +297,7 @@ class AircraftType(UnitType[FlyingType]):
|
|||||||
raise KeyError(f"Missing required price field: {data_path}") from ex
|
raise KeyError(f"Missing required price field: {data_path}") from ex
|
||||||
|
|
||||||
radio_config = RadioConfig.from_data(data.get("radios", {}))
|
radio_config = RadioConfig.from_data(data.get("radios", {}))
|
||||||
|
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
introduction = data["introduced"]
|
introduction = data["introduced"]
|
||||||
@@ -204,7 +310,10 @@ class AircraftType(UnitType[FlyingType]):
|
|||||||
yield AircraftType(
|
yield AircraftType(
|
||||||
dcs_unit_type=aircraft,
|
dcs_unit_type=aircraft,
|
||||||
name=variant,
|
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,
|
year_introduced=introduction,
|
||||||
country_of_origin=data.get("origin", "No data."),
|
country_of_origin=data.get("origin", "No data."),
|
||||||
manufacturer=data.get("manufacturer", "No data."),
|
manufacturer=data.get("manufacturer", "No data."),
|
||||||
@@ -213,7 +322,10 @@ class AircraftType(UnitType[FlyingType]):
|
|||||||
carrier_capable=data.get("carrier_capable", False),
|
carrier_capable=data.get("carrier_capable", False),
|
||||||
lha_capable=data.get("lha_capable", False),
|
lha_capable=data.get("lha_capable", False),
|
||||||
always_keeps_gun=data.get("always_keeps_gun", 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),
|
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,
|
intra_flight_radio=radio_config.intra_flight,
|
||||||
channel_allocator=radio_config.channel_allocator,
|
channel_allocator=radio_config.channel_allocator,
|
||||||
channel_namer=radio_config.channel_namer,
|
channel_namer=radio_config.channel_namer,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from game.dcs.unittype import UnitType
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GroundUnitType(UnitType[VehicleType]):
|
class GroundUnitType(UnitType[Type[VehicleType]]):
|
||||||
unit_class: Optional[GroundUnitClass]
|
unit_class: Optional[GroundUnitClass]
|
||||||
spawn_weight: int
|
spawn_weight: int
|
||||||
|
|
||||||
@@ -45,6 +45,8 @@ class GroundUnitType(UnitType[VehicleType]):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
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]
|
yield from cls._by_unit_type[dcs_unit_type]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -65,7 +67,7 @@ class GroundUnitType(UnitType[VehicleType]):
|
|||||||
logging.warning(f"No data for {vehicle.id}; it will not be available")
|
logging.warning(f"No data for {vehicle.id}; it will not be available")
|
||||||
return
|
return
|
||||||
|
|
||||||
with data_path.open() as data_file:
|
with data_path.open(encoding="utf-8") as data_file:
|
||||||
data = yaml.safe_load(data_file)
|
data = yaml.safe_load(data_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -86,7 +88,10 @@ class GroundUnitType(UnitType[VehicleType]):
|
|||||||
unit_class=unit_class,
|
unit_class=unit_class,
|
||||||
spawn_weight=data.get("spawn_weight", 0),
|
spawn_weight=data.get("spawn_weight", 0),
|
||||||
name=variant,
|
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,
|
year_introduced=introduction,
|
||||||
country_of_origin=data.get("origin", "No data."),
|
country_of_origin=data.get("origin", "No data."),
|
||||||
manufacturer=data.get("manufacturer", "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
|
from dcs.unittype import UnitType as DcsUnitType
|
||||||
|
|
||||||
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
|
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class UnitType(Generic[DcsUnitTypeT]):
|
class UnitType(Generic[DcsUnitTypeT]):
|
||||||
dcs_unit_type: Type[DcsUnitTypeT]
|
dcs_unit_type: DcsUnitTypeT
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
year_introduced: str
|
year_introduced: str
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from typing import (
|
|||||||
Iterator,
|
Iterator,
|
||||||
List,
|
List,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
@@ -77,8 +78,8 @@ class GroundLosses:
|
|||||||
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||||
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||||
|
|
||||||
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
player_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||||
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
|
enemy_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||||
|
|
||||||
player_buildings: List[Building] = field(default_factory=list)
|
player_buildings: List[Building] = field(default_factory=list)
|
||||||
enemy_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.
|
#: Names of vehicle (and ship) units that were killed during the mission.
|
||||||
killed_ground_units: List[str]
|
killed_ground_units: List[str]
|
||||||
|
|
||||||
#: Names of static units that were destroyed during the mission.
|
#: List of descriptions of destroyed statics. Format of each element is a mapping of
|
||||||
destroyed_statics: List[str]
|
#: 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.
|
#: Mangled names of bases that were captured during the mission.
|
||||||
base_capture_events: List[str]
|
base_capture_events: List[str]
|
||||||
@@ -164,7 +166,7 @@ class Debriefing:
|
|||||||
yield from self.ground_losses.enemy_airlifts
|
yield from self.ground_losses.enemy_airlifts
|
||||||
|
|
||||||
@property
|
@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.player_ground_objects
|
||||||
yield from self.ground_losses.enemy_ground_objects
|
yield from self.ground_losses.enemy_ground_objects
|
||||||
|
|
||||||
@@ -370,32 +372,38 @@ class PollDebriefingFileThread(threading.Thread):
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
|
|
||||||
def stop(self):
|
def stop(self) -> None:
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
|
||||||
def stopped(self):
|
def stopped(self) -> bool:
|
||||||
return self._stop_event.is_set()
|
return self._stop_event.is_set()
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
if os.path.isfile("state.json"):
|
if os.path.isfile("state.json"):
|
||||||
last_modified = os.path.getmtime("state.json")
|
last_modified = os.path.getmtime("state.json")
|
||||||
else:
|
else:
|
||||||
last_modified = 0
|
last_modified = 0
|
||||||
while not self.stopped():
|
while not self.stopped():
|
||||||
if (
|
try:
|
||||||
os.path.isfile("state.json")
|
if (
|
||||||
and os.path.getmtime("state.json") > last_modified
|
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)
|
with open("state.json", "r", encoding="utf-8") as json_file:
|
||||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
json_data = json.load(json_file)
|
||||||
self.callback(debriefing)
|
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||||
break
|
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)
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
def wait_for_debriefing(
|
def wait_for_debriefing(
|
||||||
callback: Callable[[Debriefing], None], game: Game, unit_map
|
callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
|
||||||
) -> PollDebriefingFileThread:
|
) -> PollDebriefingFileThread:
|
||||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from .event import Event
|
from .event import Event
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game.theater import ConflictTheater
|
|
||||||
|
|
||||||
|
|
||||||
class AirWarEvent(Event):
|
class AirWarEvent(Event):
|
||||||
"""Event handler for the air battle"""
|
"""Event handler for the air battle"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "AirWar"
|
return "AirWar"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from typing import List, TYPE_CHECKING, Type
|
|||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.task import Task
|
from dcs.task import Task
|
||||||
from dcs.unittype import VehicleType
|
|
||||||
|
|
||||||
from game import persistency
|
from game import persistency
|
||||||
from game.debriefing import AirLosses, Debriefing
|
from game.debriefing import AirLosses, Debriefing
|
||||||
@@ -38,13 +37,13 @@ class Event:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
game,
|
game: Game,
|
||||||
from_cp: ControlPoint,
|
from_cp: ControlPoint,
|
||||||
target_cp: ControlPoint,
|
target_cp: ControlPoint,
|
||||||
location: Point,
|
location: Point,
|
||||||
attacker_name: str,
|
attacker_name: str,
|
||||||
defender_name: str,
|
defender_name: str,
|
||||||
):
|
) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
self.from_cp = from_cp
|
self.from_cp = from_cp
|
||||||
self.to_cp = target_cp
|
self.to_cp = target_cp
|
||||||
@@ -54,7 +53,7 @@ class Event:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_player_attacking(self) -> bool:
|
def is_player_attacking(self) -> bool:
|
||||||
return self.attacker_name == self.game.player_name
|
return self.attacker_name == self.game.player_faction.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tasks(self) -> List[Type[Task]]:
|
def tasks(self) -> List[Type[Task]]:
|
||||||
@@ -220,10 +219,10 @@ class Event:
|
|||||||
for loss in debriefing.ground_object_losses:
|
for loss in debriefing.ground_object_losses:
|
||||||
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
||||||
if not hasattr(loss.group, "units_losts"):
|
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.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:
|
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||||
for loss in debriefing.building_losses:
|
for loss in debriefing.building_losses:
|
||||||
@@ -265,7 +264,7 @@ class Event:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(f"Could not process base capture {captured}")
|
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")
|
logging.info("Committing mission results")
|
||||||
|
|
||||||
self.commit_air_losses(debriefing)
|
self.commit_air_losses(debriefing)
|
||||||
@@ -298,15 +297,16 @@ class Event:
|
|||||||
|
|
||||||
delta = 0.0
|
delta = 0.0
|
||||||
player_won = True
|
player_won = True
|
||||||
|
status_msg: str = ""
|
||||||
ally_casualties = debriefing.casualty_count(cp)
|
ally_casualties = debriefing.casualty_count(cp)
|
||||||
enemy_casualties = debriefing.casualty_count(enemy_cp)
|
enemy_casualties = debriefing.casualty_count(enemy_cp)
|
||||||
ally_units_alive = cp.base.total_armor
|
ally_units_alive = cp.base.total_armor
|
||||||
enemy_units_alive = enemy_cp.base.total_armor
|
enemy_units_alive = enemy_cp.base.total_armor
|
||||||
|
|
||||||
print(ally_units_alive)
|
print(f"Remaining allied units: {ally_units_alive}")
|
||||||
print(enemy_units_alive)
|
print(f"Remaining enemy units: {enemy_units_alive}")
|
||||||
print(ally_casualties)
|
print(f"Allied casualties {ally_casualties}")
|
||||||
print(enemy_casualties)
|
print(f"Enemy casualties {enemy_casualties}")
|
||||||
|
|
||||||
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
||||||
|
|
||||||
@@ -319,24 +319,31 @@ class Event:
|
|||||||
if ally_units_alive == 0:
|
if ally_units_alive == 0:
|
||||||
player_won = False
|
player_won = False
|
||||||
delta = STRONG_DEFEAT_INFLUENCE
|
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:
|
elif enemy_units_alive == 0:
|
||||||
player_won = True
|
player_won = True
|
||||||
delta = STRONG_DEFEAT_INFLUENCE
|
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:
|
elif cp.stances[enemy_cp.id] == CombatStance.RETREAT:
|
||||||
player_won = False
|
player_won = False
|
||||||
delta = STRONG_DEFEAT_INFLUENCE
|
delta = STRONG_DEFEAT_INFLUENCE
|
||||||
|
status_msg = f"Allied forces are retreating along the {cp.name}-{enemy_cp.name} frontline, suffering a strong defeat."
|
||||||
else:
|
else:
|
||||||
if enemy_casualties > ally_casualties:
|
if enemy_casualties > ally_casualties:
|
||||||
player_won = True
|
player_won = True
|
||||||
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
||||||
delta = STRONG_DEFEAT_INFLUENCE
|
delta = STRONG_DEFEAT_INFLUENCE
|
||||||
|
status_msg = f"Allied forces break through the {cp.name}-{enemy_cp.name} frontline, winning a strong victory"
|
||||||
else:
|
else:
|
||||||
if ratio > 3:
|
if ratio > 3:
|
||||||
delta = STRONG_DEFEAT_INFLUENCE
|
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:
|
elif ratio < 1.5:
|
||||||
delta = MINOR_DEFEAT_INFLUENCE
|
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:
|
else:
|
||||||
delta = DEFEAT_INFLUENCE
|
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:
|
elif ally_casualties > enemy_casualties:
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -346,54 +353,66 @@ class Event:
|
|||||||
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
|
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
|
||||||
player_won = True
|
player_won = True
|
||||||
delta = MINOR_DEFEAT_INFLUENCE
|
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 (
|
elif (
|
||||||
ally_units_alive > 3 * enemy_units_alive
|
ally_units_alive > 3 * enemy_units_alive
|
||||||
and player_aggresive
|
and player_aggresive
|
||||||
):
|
):
|
||||||
player_won = True
|
player_won = True
|
||||||
delta = STRONG_DEFEAT_INFLUENCE
|
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:
|
else:
|
||||||
# But is the enemy is not outnumbered, we lose
|
# But if the enemy is not outnumbered, we lose
|
||||||
player_won = False
|
player_won = False
|
||||||
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
||||||
delta = STRONG_DEFEAT_INFLUENCE
|
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:
|
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
|
# No progress with defensive strategies
|
||||||
if player_won and cp.stances[enemy_cp.id] in [
|
if player_won and cp.stances[enemy_cp.id] in [
|
||||||
CombatStance.DEFENSIVE,
|
CombatStance.DEFENSIVE,
|
||||||
CombatStance.AMBUSH,
|
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
|
delta = MINOR_DEFEAT_INFLUENCE
|
||||||
|
|
||||||
if player_won:
|
# Handle the case where there are no casualties at all on either side but both sides still have units
|
||||||
print(cp.name + " won ! factor > " + str(delta))
|
if delta == 0.0:
|
||||||
cp.base.affect_strength(delta)
|
print(status_msg)
|
||||||
enemy_cp.base.affect_strength(-delta)
|
|
||||||
info = Information(
|
info = Information(
|
||||||
"Frontline Report",
|
"Frontline Report",
|
||||||
"Our ground forces from "
|
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.",
|
||||||
+ cp.name
|
|
||||||
+ " are making progress toward "
|
|
||||||
+ enemy_cp.name,
|
|
||||||
self.game.turn,
|
self.game.turn,
|
||||||
)
|
)
|
||||||
self.game.informations.append(info)
|
self.game.informations.append(info)
|
||||||
else:
|
else:
|
||||||
print(cp.name + " lost ! factor > " + str(delta))
|
if player_won:
|
||||||
enemy_cp.base.affect_strength(delta)
|
print(status_msg)
|
||||||
cp.base.affect_strength(-delta)
|
cp.base.affect_strength(delta)
|
||||||
info = Information(
|
enemy_cp.base.affect_strength(-delta)
|
||||||
"Frontline Report",
|
info = Information(
|
||||||
"Our ground forces from "
|
"Frontline Report",
|
||||||
+ cp.name
|
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
|
||||||
+ " are losing ground against the enemy forces from "
|
self.game.turn,
|
||||||
+ enemy_cp.name,
|
)
|
||||||
self.game.turn,
|
self.game.informations.append(info)
|
||||||
)
|
else:
|
||||||
self.game.informations.append(info)
|
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:
|
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||||
""" "
|
""" "
|
||||||
|
|||||||
@@ -8,5 +8,5 @@ class FrontlineAttackEvent(Event):
|
|||||||
future unique Event handling
|
future unique Event handling
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "Frontline attack"
|
return "Frontline attack"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass, field
|
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
|
import dcs
|
||||||
from dcs.countries import country_dict
|
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.aircrafttype import AircraftType
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.theater.start_generator import ModSettings
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Faction:
|
class Faction:
|
||||||
@@ -81,10 +84,10 @@ class Faction:
|
|||||||
requirements: Dict[str, str] = field(default_factory=dict)
|
requirements: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
# possible aircraft carrier units
|
# 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
|
# 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
|
# Possible carrier names
|
||||||
carrier_names: List[str] = field(default_factory=list)
|
carrier_names: List[str] = field(default_factory=list)
|
||||||
@@ -257,6 +260,83 @@ class Faction:
|
|||||||
if unit.unit_class is unit_class:
|
if unit.unit_class is unit_class:
|
||||||
yield unit
|
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]]:
|
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||||
if (ship := getattr(dcs.ships, name, None)) is not None:
|
if (ship := getattr(dcs.ships, name, None)) is not None:
|
||||||
@@ -265,7 +345,7 @@ def load_ship(name: str) -> Optional[Type[ShipType]]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_all_ships(data) -> List[Type[ShipType]]:
|
def load_all_ships(data: list[str]) -> List[Type[ShipType]]:
|
||||||
items = []
|
items = []
|
||||||
for name in data:
|
for name in data:
|
||||||
item = load_ship(name)
|
item = load_ship(name)
|
||||||
|
|||||||
298
game/game.py
298
game/game.py
@@ -1,10 +1,11 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List
|
from typing import Any, List, Type, Union, cast
|
||||||
|
|
||||||
from dcs.action import Coalition
|
from dcs.action import Coalition
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
@@ -12,7 +13,6 @@ from dcs.task import CAP, CAS, PinpointStrike
|
|||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
|
|
||||||
from game import db
|
|
||||||
from game.inventory import GlobalAircraftInventory
|
from game.inventory import GlobalAircraftInventory
|
||||||
from game.models.game_stats import GameStats
|
from game.models.game_stats import GameStats
|
||||||
from game.plugins import LuaPluginManager
|
from game.plugins import LuaPluginManager
|
||||||
@@ -35,7 +35,7 @@ from .procurement import AircraftProcurementRequest, ProcurementAi
|
|||||||
from .profiling import logged_duration
|
from .profiling import logged_duration
|
||||||
from .settings import Settings, AutoAtoBehavior
|
from .settings import Settings, AutoAtoBehavior
|
||||||
from .squadrons import AirWing
|
from .squadrons import AirWing
|
||||||
from .theater import ConflictTheater
|
from .theater import ConflictTheater, ControlPoint
|
||||||
from .theater.bullseye import Bullseye
|
from .theater.bullseye import Bullseye
|
||||||
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||||
from .threatzones import ThreatZones
|
from .threatzones import ThreatZones
|
||||||
@@ -86,8 +86,8 @@ class TurnState(Enum):
|
|||||||
class Game:
|
class Game:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
player_name: str,
|
player_faction: Faction,
|
||||||
enemy_name: str,
|
enemy_faction: Faction,
|
||||||
theater: ConflictTheater,
|
theater: ConflictTheater,
|
||||||
start_date: datetime,
|
start_date: datetime,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
@@ -97,23 +97,23 @@ class Game:
|
|||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.events: List[Event] = []
|
self.events: List[Event] = []
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
self.player_name = player_name
|
self.player_faction = player_faction
|
||||||
self.player_country = db.FACTIONS[player_name].country
|
self.player_country = player_faction.country
|
||||||
self.enemy_name = enemy_name
|
self.enemy_faction = enemy_faction
|
||||||
self.enemy_country = db.FACTIONS[enemy_name].country
|
self.enemy_country = enemy_faction.country
|
||||||
# pass_turn() will be called when initialization is complete which will
|
# pass_turn() will be called when initialization is complete which will
|
||||||
# increment this to turn 0 before it reaches the player.
|
# increment this to turn 0 before it reaches the player.
|
||||||
self.turn = -1
|
self.turn = -1
|
||||||
# NB: This is the *start* date. It is never updated.
|
# NB: This is the *start* date. It is never updated.
|
||||||
self.date = date(start_date.year, start_date.month, start_date.day)
|
self.date = date(start_date.year, start_date.month, start_date.day)
|
||||||
self.game_stats = GameStats()
|
self.game_stats = GameStats()
|
||||||
self.game_stats.update(self)
|
self.notes = ""
|
||||||
self.ground_planners: Dict[int, GroundPlanner] = {}
|
self.ground_planners: dict[int, GroundPlanner] = {}
|
||||||
self.informations = []
|
self.informations = []
|
||||||
self.informations.append(Information("Game Start", "-" * 40, 0))
|
self.informations.append(Information("Game Start", "-" * 40, 0))
|
||||||
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
|
||||||
self.__culling_zones: List[Point] = []
|
self.__culling_zones: List[Point] = []
|
||||||
self.__destroyed_units: List[str] = []
|
self.__destroyed_units: list[dict[str, Union[float, str]]] = []
|
||||||
self.savepath = ""
|
self.savepath = ""
|
||||||
self.budget = player_budget
|
self.budget = player_budget
|
||||||
self.enemy_budget = enemy_budget
|
self.enemy_budget = enemy_budget
|
||||||
@@ -149,7 +149,7 @@ class Game:
|
|||||||
|
|
||||||
self.on_load(game_still_initializing=True)
|
self.on_load(game_still_initializing=True)
|
||||||
|
|
||||||
def __getstate__(self) -> Dict[str, Any]:
|
def __getstate__(self) -> dict[str, Any]:
|
||||||
state = self.__dict__.copy()
|
state = self.__dict__.copy()
|
||||||
# Avoid persisting any volatile types that can be deterministically
|
# Avoid persisting any volatile types that can be deterministically
|
||||||
# recomputed on load for the sake of save compatibility.
|
# recomputed on load for the sake of save compatibility.
|
||||||
@@ -161,7 +161,7 @@ class Game:
|
|||||||
del state["red_faker"]
|
del state["red_faker"]
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def __setstate__(self, state: Dict[str, Any]) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
self.__dict__.update(state)
|
self.__dict__.update(state)
|
||||||
# Regenerate any state that was not persisted.
|
# Regenerate any state that was not persisted.
|
||||||
self.on_load()
|
self.on_load()
|
||||||
@@ -188,7 +188,7 @@ class Game:
|
|||||||
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
|
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
|
Make sure the opposing factions are using different countries
|
||||||
:return:
|
:return:
|
||||||
@@ -201,14 +201,6 @@ class Game:
|
|||||||
else:
|
else:
|
||||||
self.enemy_country = "Russia"
|
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:
|
def faction_for(self, player: bool) -> Faction:
|
||||||
if player:
|
if player:
|
||||||
return self.player_faction
|
return self.player_faction
|
||||||
@@ -234,26 +226,21 @@ class Game:
|
|||||||
return self.blue_bullseye
|
return self.blue_bullseye
|
||||||
return self.red_bullseye
|
return self.red_bullseye
|
||||||
|
|
||||||
def _roll(self, prob, mult):
|
def _generate_player_event(
|
||||||
if self.settings.version == "dev":
|
self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint
|
||||||
# always generate all events for dev
|
) -> None:
|
||||||
return 100
|
|
||||||
else:
|
|
||||||
return random.randint(1, 100) <= prob * mult
|
|
||||||
|
|
||||||
def _generate_player_event(self, event_class, player_cp, enemy_cp):
|
|
||||||
self.events.append(
|
self.events.append(
|
||||||
event_class(
|
event_class(
|
||||||
self,
|
self,
|
||||||
player_cp,
|
player_cp,
|
||||||
enemy_cp,
|
enemy_cp,
|
||||||
enemy_cp.position,
|
enemy_cp.position,
|
||||||
self.player_name,
|
self.player_faction.name,
|
||||||
self.enemy_name,
|
self.enemy_faction.name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _generate_events(self):
|
def _generate_events(self) -> None:
|
||||||
for front_line in self.theater.conflicts():
|
for front_line in self.theater.conflicts():
|
||||||
self._generate_player_event(
|
self._generate_player_event(
|
||||||
FrontlineAttackEvent,
|
FrontlineAttackEvent,
|
||||||
@@ -267,21 +254,22 @@ class Game:
|
|||||||
else:
|
else:
|
||||||
self.enemy_budget += amount
|
self.enemy_budget += amount
|
||||||
|
|
||||||
def process_player_income(self):
|
def process_player_income(self) -> None:
|
||||||
self.budget += Income(self, player=True).total
|
self.budget += Income(self, player=True).total
|
||||||
|
|
||||||
def process_enemy_income(self):
|
def process_enemy_income(self) -> None:
|
||||||
# TODO: Clean up save compat.
|
# TODO: Clean up save compat.
|
||||||
if not hasattr(self, "enemy_budget"):
|
if not hasattr(self, "enemy_budget"):
|
||||||
self.enemy_budget = 0
|
self.enemy_budget = 0
|
||||||
self.enemy_budget += Income(self, player=False).total
|
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
|
# assert event in self.events
|
||||||
logging.info("Generating {} (regular)".format(event))
|
logging.info("Generating {} (regular)".format(event))
|
||||||
return event.generate()
|
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))
|
logging.info("Finishing event {}".format(event))
|
||||||
event.commit(debriefing)
|
event.commit(debriefing)
|
||||||
|
|
||||||
@@ -290,16 +278,6 @@ class Game:
|
|||||||
else:
|
else:
|
||||||
logging.info("finish_event: event not in the events!")
|
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:
|
def on_load(self, game_still_initializing: bool = False) -> None:
|
||||||
if not hasattr(self, "name_generator"):
|
if not hasattr(self, "name_generator"):
|
||||||
self.name_generator = naming.namegen
|
self.name_generator = naming.namegen
|
||||||
@@ -322,6 +300,33 @@ class Game:
|
|||||||
self.red_ato.clear()
|
self.red_ato.clear()
|
||||||
|
|
||||||
def finish_turn(self, skipped: bool = False) -> None:
|
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(
|
self.informations.append(
|
||||||
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
Information("End of turn #" + str(self.turn), "-" * 40, 0)
|
||||||
)
|
)
|
||||||
@@ -344,10 +349,10 @@ class Game:
|
|||||||
self.blue_air_wing.replenish()
|
self.blue_air_wing.replenish()
|
||||||
self.red_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():
|
for cp in self.theater.player_points():
|
||||||
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
|
||||||
else:
|
elif self.turn > 1:
|
||||||
for cp in self.theater.player_points():
|
for cp in self.theater.player_points():
|
||||||
if not cp.is_carrier and not cp.is_lha:
|
if not cp.is_carrier and not cp.is_lha:
|
||||||
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
|
||||||
@@ -358,10 +363,18 @@ class Game:
|
|||||||
self.process_player_income()
|
self.process_player_income()
|
||||||
|
|
||||||
def begin_turn_0(self) -> None:
|
def begin_turn_0(self) -> None:
|
||||||
|
"""Initialization for the first turn of the game."""
|
||||||
self.turn = 0
|
self.turn = 0
|
||||||
self.initialize_turn()
|
self.initialize_turn()
|
||||||
|
|
||||||
def pass_turn(self, no_action: bool = False) -> None:
|
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")
|
logging.info("Pass turn")
|
||||||
with logged_duration("Turn finalization"):
|
with logged_duration("Turn finalization"):
|
||||||
self.finish_turn(no_action)
|
self.finish_turn(no_action)
|
||||||
@@ -371,7 +384,7 @@ class Game:
|
|||||||
# Autosave progress
|
# Autosave progress
|
||||||
persistency.autosave(self)
|
persistency.autosave(self)
|
||||||
|
|
||||||
def check_win_loss(self):
|
def check_win_loss(self) -> TurnState:
|
||||||
player_airbases = {
|
player_airbases = {
|
||||||
cp for cp in self.theater.player_points() if cp.runway_is_operational()
|
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.blue_bullseye = Bullseye(enemy_cp.position)
|
||||||
self.red_bullseye = Bullseye(player_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.events = []
|
||||||
self._generate_events()
|
self._generate_events()
|
||||||
|
|
||||||
self.set_bullseye()
|
self.set_bullseye()
|
||||||
|
|
||||||
# Update statistics
|
# Update statistics
|
||||||
self.game_stats.update(self)
|
self.game_stats.update(self)
|
||||||
|
|
||||||
self.blue_air_wing.reset()
|
|
||||||
self.red_air_wing.reset()
|
|
||||||
self.aircraft_inventory.reset()
|
|
||||||
for cp in self.theater.controlpoints:
|
|
||||||
self.aircraft_inventory.set_from_control_point(cp)
|
|
||||||
|
|
||||||
# Check for win or loss condition
|
# Check for win or loss condition
|
||||||
turn_state = self.check_win_loss()
|
turn_state = self.check_win_loss()
|
||||||
if turn_state in (TurnState.LOSS, TurnState.WIN):
|
if turn_state in (TurnState.LOSS, TurnState.WIN):
|
||||||
return self.process_win_loss(turn_state)
|
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
|
# Plan flights & combat for next turn
|
||||||
with logged_duration("Computing conflict positions"):
|
with logged_duration("Computing conflict positions"):
|
||||||
self.compute_conflicts_position()
|
self.compute_conflicts_position()
|
||||||
@@ -420,55 +497,48 @@ class Game:
|
|||||||
self.compute_transit_networks()
|
self.compute_transit_networks()
|
||||||
self.ground_planners = {}
|
self.ground_planners = {}
|
||||||
|
|
||||||
self.blue_procurement_requests.clear()
|
self.procurement_requests_for(player).clear()
|
||||||
self.red_procurement_requests.clear()
|
|
||||||
|
|
||||||
with logged_duration("Procurement of airlift assets"):
|
with logged_duration("Procurement of airlift assets"):
|
||||||
self.transfers.order_airlift_assets()
|
self.transfers.order_airlift_assets()
|
||||||
with logged_duration("Transport planning"):
|
with logged_duration("Transport planning"):
|
||||||
self.transfers.plan_transports()
|
self.transfers.plan_transports()
|
||||||
|
|
||||||
with logged_duration("Blue mission planning"):
|
if not player or (
|
||||||
if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
|
player and self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
|
||||||
blue_planner = CoalitionMissionPlanner(self, is_player=True)
|
):
|
||||||
blue_planner.plan_missions()
|
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"):
|
self.plan_procurement_for(player)
|
||||||
red_planner = CoalitionMissionPlanner(self, is_player=False)
|
|
||||||
red_planner.plan_missions()
|
|
||||||
|
|
||||||
for cp in self.theater.controlpoints:
|
def plan_procurement_for(self, for_player: bool) -> None:
|
||||||
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:
|
|
||||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
|
# 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
|
# gets much more of the budget that turn. Otherwise budget (after
|
||||||
# repairs) is split evenly between air and ground. For the default
|
# repairs) is split evenly between air and ground. For the default
|
||||||
# starting budget of 2000 this gives 600 to ground forces and 1400 to
|
# starting budget of 2000 this gives 600 to ground forces and 1400 to
|
||||||
# aircraft. After that the budget will be spend proportionally based on how much is already invested
|
# aircraft. After that the budget will be spend proportionally based on how much is already invested
|
||||||
|
|
||||||
self.budget = ProcurementAi(
|
if for_player:
|
||||||
self,
|
self.budget = ProcurementAi(
|
||||||
for_player=True,
|
self,
|
||||||
faction=self.player_faction,
|
for_player=True,
|
||||||
manage_runways=self.settings.automate_runway_repair,
|
faction=self.player_faction,
|
||||||
manage_front_line=self.settings.automate_front_line_reinforcements,
|
manage_runways=self.settings.automate_runway_repair,
|
||||||
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
manage_front_line=self.settings.automate_front_line_reinforcements,
|
||||||
).spend_budget(self.budget)
|
manage_aircraft=self.settings.automate_aircraft_reinforcements,
|
||||||
|
).spend_budget(self.budget)
|
||||||
self.enemy_budget = ProcurementAi(
|
else:
|
||||||
self,
|
self.enemy_budget = ProcurementAi(
|
||||||
for_player=False,
|
self,
|
||||||
faction=self.enemy_faction,
|
for_player=False,
|
||||||
manage_runways=True,
|
faction=self.enemy_faction,
|
||||||
manage_front_line=True,
|
manage_runways=True,
|
||||||
manage_aircraft=True,
|
manage_front_line=True,
|
||||||
).spend_budget(self.enemy_budget)
|
manage_aircraft=True,
|
||||||
|
).spend_budget(self.enemy_budget)
|
||||||
|
|
||||||
def message(self, text: str) -> None:
|
def message(self, text: str) -> None:
|
||||||
self.informations.append(Information(text, turn=self.turn))
|
self.informations.append(Information(text, turn=self.turn))
|
||||||
@@ -481,14 +551,14 @@ class Game:
|
|||||||
def current_day(self) -> date:
|
def current_day(self) -> date:
|
||||||
return self.date + timedelta(days=self.turn // 4)
|
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
|
Next unit id for pre-generated units
|
||||||
"""
|
"""
|
||||||
self.current_unit_id += 1
|
self.current_unit_id += 1
|
||||||
return self.current_unit_id
|
return self.current_unit_id
|
||||||
|
|
||||||
def next_group_id(self):
|
def next_group_id(self) -> int:
|
||||||
"""
|
"""
|
||||||
Next unit id for pre-generated units
|
Next unit id for pre-generated units
|
||||||
"""
|
"""
|
||||||
@@ -522,7 +592,7 @@ class Game:
|
|||||||
return self.blue_navmesh
|
return self.blue_navmesh
|
||||||
return self.red_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
|
Compute the current conflict center position(s), mainly used for culling calculation
|
||||||
:return: List of points of interests
|
: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 there is no conflict take the center point between the two nearest opposing bases
|
||||||
if len(zones) == 0:
|
if len(zones) == 0:
|
||||||
cpoint = None
|
cpoint = None
|
||||||
min_distance = sys.maxsize
|
min_distance = math.inf
|
||||||
for cp in self.theater.player_points():
|
for cp in self.theater.player_points():
|
||||||
for cp2 in self.theater.enemy_points():
|
for cp2 in self.theater.enemy_points():
|
||||||
d = cp.position.distance_to_point(cp2.position)
|
d = cp.position.distance_to_point(cp2.position)
|
||||||
@@ -581,15 +651,15 @@ class Game:
|
|||||||
|
|
||||||
self.__culling_zones = zones
|
self.__culling_zones = zones
|
||||||
|
|
||||||
def add_destroyed_units(self, data):
|
def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None:
|
||||||
pos = Point(data["x"], data["z"])
|
pos = Point(cast(float, data["x"]), cast(float, data["z"]))
|
||||||
if self.theater.is_on_land(pos):
|
if self.theater.is_on_land(pos):
|
||||||
self.__destroyed_units.append(data)
|
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
|
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
|
Check if unit can be generated at given position depending on culling performance settings
|
||||||
:param pos: Position you are tryng to spawn stuff at
|
:param pos: Position you are tryng to spawn stuff at
|
||||||
@@ -602,7 +672,7 @@ class Game:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_culling_zones(self):
|
def get_culling_zones(self) -> list[Point]:
|
||||||
"""
|
"""
|
||||||
Check culling points
|
Check culling points
|
||||||
:return: List of culling zones
|
:return: List of culling zones
|
||||||
@@ -610,30 +680,28 @@ class Game:
|
|||||||
return self.__culling_zones
|
return self.__culling_zones
|
||||||
|
|
||||||
# 1 = red, 2 = blue
|
# 1 = red, 2 = blue
|
||||||
def get_player_coalition_id(self):
|
def get_player_coalition_id(self) -> int:
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
def get_enemy_coalition_id(self):
|
def get_enemy_coalition_id(self) -> int:
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
def get_player_coalition(self):
|
def get_player_coalition(self) -> Coalition:
|
||||||
return Coalition.Blue
|
return Coalition.Blue
|
||||||
|
|
||||||
def get_enemy_coalition(self):
|
def get_enemy_coalition(self) -> Coalition:
|
||||||
return Coalition.Red
|
return Coalition.Red
|
||||||
|
|
||||||
def get_player_color(self):
|
def get_player_color(self) -> str:
|
||||||
return "blue"
|
return "blue"
|
||||||
|
|
||||||
def get_enemy_color(self):
|
def get_enemy_color(self) -> str:
|
||||||
return "red"
|
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:
|
if turn_state is TurnState.WIN:
|
||||||
return self.message(
|
self.message(
|
||||||
"Congratulations, you are victorious! Start a new campaign to continue."
|
"Congratulations, you are victorious! Start a new campaign to continue."
|
||||||
)
|
)
|
||||||
elif turn_state is TurnState.LOSS:
|
elif turn_state is TurnState.LOSS:
|
||||||
return self.message(
|
self.message("Game Over, you lose. Start a new campaign to continue.")
|
||||||
"Game Over, you lose. Start a new campaign to continue."
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import datetime
|
|||||||
|
|
||||||
|
|
||||||
class Information:
|
class Information:
|
||||||
def __init__(self, title="", text="", turn=0):
|
def __init__(self, title: str = "", text: str = "", turn: int = 0) -> None:
|
||||||
self.title = title
|
self.title = title
|
||||||
self.text = text
|
self.text = text
|
||||||
self.turn = turn
|
self.turn = turn
|
||||||
self.timestamp = datetime.datetime.now()
|
self.timestamp = datetime.datetime.now()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "[{}][{}] {} {}".format(
|
return "[{}][{}] {} {}".format(
|
||||||
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
if self.timestamp is not None
|
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:
|
class FactionTurnMetadata:
|
||||||
@@ -10,7 +15,7 @@ class FactionTurnMetadata:
|
|||||||
vehicles_count: int = 0
|
vehicles_count: int = 0
|
||||||
sam_count: int = 0
|
sam_count: int = 0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.aircraft_count = 0
|
self.aircraft_count = 0
|
||||||
self.vehicles_count = 0
|
self.vehicles_count = 0
|
||||||
self.sam_count = 0
|
self.sam_count = 0
|
||||||
@@ -24,7 +29,7 @@ class GameTurnMetadata:
|
|||||||
allied_units: FactionTurnMetadata
|
allied_units: FactionTurnMetadata
|
||||||
enemy_units: FactionTurnMetadata
|
enemy_units: FactionTurnMetadata
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.allied_units = FactionTurnMetadata()
|
self.allied_units = FactionTurnMetadata()
|
||||||
self.enemy_units = FactionTurnMetadata()
|
self.enemy_units = FactionTurnMetadata()
|
||||||
|
|
||||||
@@ -34,15 +39,19 @@ class GameStats:
|
|||||||
Store statistics for the current game
|
Store statistics for the current game
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.data_per_turn: List[GameTurnMetadata] = []
|
self.data_per_turn: List[GameTurnMetadata] = []
|
||||||
|
|
||||||
def update(self, game):
|
def update(self, game: Game) -> None:
|
||||||
"""
|
"""
|
||||||
Save data for current turn
|
Save data for current turn
|
||||||
:param game: Game we want to save the data about
|
: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()
|
turn_data = GameTurnMetadata()
|
||||||
|
|
||||||
for cp in game.theater.controlpoints:
|
for cp in game.theater.controlpoints:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
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 import Mission
|
||||||
from dcs.action import DoScript, DoScriptFile
|
from dcs.action import DoScript, DoScriptFile
|
||||||
@@ -62,28 +62,14 @@ class Operation:
|
|||||||
plugin_scripts: List[str] = []
|
plugin_scripts: List[str] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare(cls, game: Game):
|
def prepare(cls, game: Game) -> None:
|
||||||
with open("resources/default_options.lua", "r") as f:
|
with open("resources/default_options.lua", "r", encoding="utf-8") as f:
|
||||||
options_dict = loads(f.read())["options"]
|
options_dict = loads(f.read())["options"]
|
||||||
cls._set_mission(Mission(game.theater.terrain))
|
cls._set_mission(Mission(game.theater.terrain))
|
||||||
cls.game = game
|
cls.game = game
|
||||||
cls._setup_mission_coalitions()
|
cls._setup_mission_coalitions()
|
||||||
cls.current_mission.options.load_from_dict(options_dict)
|
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
|
@classmethod
|
||||||
def air_conflict(cls) -> Conflict:
|
def air_conflict(cls) -> Conflict:
|
||||||
assert cls.game
|
assert cls.game
|
||||||
@@ -95,10 +81,10 @@ class Operation:
|
|||||||
return Conflict(
|
return Conflict(
|
||||||
cls.game.theater,
|
cls.game.theater,
|
||||||
FrontLine(player_cp, enemy_cp),
|
FrontLine(player_cp, enemy_cp),
|
||||||
cls.game.player_name,
|
cls.game.player_faction.name,
|
||||||
cls.game.enemy_name,
|
cls.game.enemy_faction.name,
|
||||||
cls.game.player_country,
|
cls.current_mission.country(cls.game.player_country),
|
||||||
cls.game.enemy_country,
|
cls.current_mission.country(cls.game.enemy_country),
|
||||||
mid_point,
|
mid_point,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,7 +93,7 @@ class Operation:
|
|||||||
cls.current_mission = mission
|
cls.current_mission = mission
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _setup_mission_coalitions(cls):
|
def _setup_mission_coalitions(cls) -> None:
|
||||||
cls.current_mission.coalition["blue"] = Coalition(
|
cls.current_mission.coalition["blue"] = Coalition(
|
||||||
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
|
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
|
||||||
)
|
)
|
||||||
@@ -163,7 +149,7 @@ class Operation:
|
|||||||
airsupportgen: AirSupportConflictGenerator,
|
airsupportgen: AirSupportConflictGenerator,
|
||||||
jtacs: List[JtacInfo],
|
jtacs: List[JtacInfo],
|
||||||
airgen: AircraftConflictGenerator,
|
airgen: AircraftConflictGenerator,
|
||||||
):
|
) -> None:
|
||||||
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
|
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
|
||||||
|
|
||||||
gens: List[MissionInfoGenerator] = [
|
gens: List[MissionInfoGenerator] = [
|
||||||
@@ -251,7 +237,7 @@ class Operation:
|
|||||||
# beacon list.
|
# beacon list.
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _generate_ground_units(cls):
|
def _generate_ground_units(cls) -> None:
|
||||||
cls.groundobjectgen = GroundObjectsGenerator(
|
cls.groundobjectgen = GroundObjectsGenerator(
|
||||||
cls.current_mission,
|
cls.current_mission,
|
||||||
cls.game,
|
cls.game,
|
||||||
@@ -266,11 +252,16 @@ class Operation:
|
|||||||
"""Add destroyed units to the Mission"""
|
"""Add destroyed units to the Mission"""
|
||||||
for d in cls.game.get_destroyed_units():
|
for d in cls.game.get_destroyed_units():
|
||||||
try:
|
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:
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
pos = Point(d["x"], d["z"])
|
pos = Point(cast(float, d["x"]), cast(float, d["z"]))
|
||||||
if (
|
if (
|
||||||
utype is not None
|
utype is not None
|
||||||
and not cls.game.position_culled(pos)
|
and not cls.game.position_culled(pos)
|
||||||
@@ -389,8 +380,8 @@ class Operation:
|
|||||||
player_cp = front_line.blue_cp
|
player_cp = front_line.blue_cp
|
||||||
enemy_cp = front_line.red_cp
|
enemy_cp = front_line.red_cp
|
||||||
conflict = Conflict.frontline_cas_conflict(
|
conflict = Conflict.frontline_cas_conflict(
|
||||||
cls.game.player_name,
|
cls.game.player_faction.name,
|
||||||
cls.game.enemy_name,
|
cls.game.enemy_faction.name,
|
||||||
cls.current_mission.country(cls.game.player_country),
|
cls.current_mission.country(cls.game.player_country),
|
||||||
cls.current_mission.country(cls.game.enemy_country),
|
cls.current_mission.country(cls.game.enemy_country),
|
||||||
front_line,
|
front_line,
|
||||||
@@ -418,7 +409,7 @@ class Operation:
|
|||||||
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
|
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reset_naming_ids(cls):
|
def reset_naming_ids(cls) -> None:
|
||||||
namegen.reset_numbers()
|
namegen.reset_numbers()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -439,8 +430,8 @@ class Operation:
|
|||||||
"BlueAA": {},
|
"BlueAA": {},
|
||||||
} # type: ignore
|
} # type: ignore
|
||||||
|
|
||||||
for tanker in airsupportgen.air_support.tankers:
|
for i, tanker in enumerate(airsupportgen.air_support.tankers):
|
||||||
luaData["Tankers"][tanker.callsign] = {
|
luaData["Tankers"][i] = {
|
||||||
"dcsGroupName": tanker.group_name,
|
"dcsGroupName": tanker.group_name,
|
||||||
"callsign": tanker.callsign,
|
"callsign": tanker.callsign,
|
||||||
"variant": tanker.variant,
|
"variant": tanker.variant,
|
||||||
@@ -448,23 +439,22 @@ class Operation:
|
|||||||
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
|
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
if airsupportgen.air_support.awacs:
|
for i, awacs in enumerate(airsupportgen.air_support.awacs):
|
||||||
for awacs in airsupportgen.air_support.awacs:
|
luaData["AWACs"][i] = {
|
||||||
luaData["AWACs"][awacs.callsign] = {
|
"dcsGroupName": awacs.group_name,
|
||||||
"dcsGroupName": awacs.group_name,
|
"callsign": awacs.callsign,
|
||||||
"callsign": awacs.callsign,
|
"radio": awacs.freq.mhz,
|
||||||
"radio": awacs.freq.mhz,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for jtac in jtacs:
|
for i, jtac in enumerate(jtacs):
|
||||||
luaData["JTACs"][jtac.callsign] = {
|
luaData["JTACs"][i] = {
|
||||||
"dcsGroupName": jtac.group_name,
|
"dcsGroupName": jtac.group_name,
|
||||||
"callsign": jtac.callsign,
|
"callsign": jtac.callsign,
|
||||||
"zone": jtac.region,
|
"zone": jtac.region,
|
||||||
"dcsUnit": jtac.unit_name,
|
"dcsUnit": jtac.unit_name,
|
||||||
"laserCode": jtac.code,
|
"laserCode": jtac.code,
|
||||||
}
|
}
|
||||||
|
flight_count = 0
|
||||||
for flight in airgen.flights:
|
for flight in airgen.flights:
|
||||||
if flight.friendly and flight.flight_type in [
|
if flight.friendly and flight.flight_type in [
|
||||||
FlightType.ANTISHIP,
|
FlightType.ANTISHIP,
|
||||||
@@ -485,7 +475,7 @@ class Operation:
|
|||||||
elif hasattr(flightTarget, "name"):
|
elif hasattr(flightTarget, "name"):
|
||||||
flightTargetName = flightTarget.name
|
flightTargetName = flightTarget.name
|
||||||
flightTargetType = flightType + " TGT (Airbase)"
|
flightTargetType = flightType + " TGT (Airbase)"
|
||||||
luaData["TargetPoints"][flightTargetName] = {
|
luaData["TargetPoints"][flight_count] = {
|
||||||
"name": flightTargetName,
|
"name": flightTargetName,
|
||||||
"type": flightTargetType,
|
"type": flightTargetType,
|
||||||
"position": {
|
"position": {
|
||||||
@@ -493,6 +483,7 @@ class Operation:
|
|||||||
"y": flightTarget.position.y,
|
"y": flightTarget.position.y,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
flight_count += 1
|
||||||
|
|
||||||
for cp in cls.game.theater.controlpoints:
|
for cp in cls.game.theater.controlpoints:
|
||||||
for ground_object in cp.ground_objects:
|
for ground_object in cp.ground_objects:
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import shutil
|
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
|
_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
|
global _dcs_saved_game_folder
|
||||||
_dcs_saved_game_folder = user_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:
|
def base_path() -> str:
|
||||||
@@ -20,19 +26,23 @@ def base_path() -> str:
|
|||||||
return _dcs_saved_game_folder
|
return _dcs_saved_game_folder
|
||||||
|
|
||||||
|
|
||||||
|
def save_dir() -> Path:
|
||||||
|
return Path(base_path()) / "Liberation" / "Saves"
|
||||||
|
|
||||||
|
|
||||||
def _temporary_save_file() -> str:
|
def _temporary_save_file() -> str:
|
||||||
return os.path.join(base_path(), "tmpsave.liberation")
|
return str(save_dir() / "tmpsave.liberation")
|
||||||
|
|
||||||
|
|
||||||
def _autosave_path() -> str:
|
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:
|
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:
|
with open(path, "rb") as f:
|
||||||
try:
|
try:
|
||||||
save = pickle.load(f)
|
save = pickle.load(f)
|
||||||
@@ -43,7 +53,7 @@ def load_game(path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def save_game(game) -> bool:
|
def save_game(game: Game) -> bool:
|
||||||
try:
|
try:
|
||||||
with open(_temporary_save_file(), "wb") as f:
|
with open(_temporary_save_file(), "wb") as f:
|
||||||
pickle.dump(game, f)
|
pickle.dump(game, f)
|
||||||
@@ -54,7 +64,7 @@ def save_game(game) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def autosave(game) -> bool:
|
def autosave(game: Game) -> bool:
|
||||||
"""
|
"""
|
||||||
Autosave to the autosave location
|
Autosave to the autosave location
|
||||||
:param game: Game to save
|
:param game: Game to save
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class PluginSettings:
|
|||||||
self.settings = Settings()
|
self.settings = Settings()
|
||||||
self.initialize_settings()
|
self.initialize_settings()
|
||||||
|
|
||||||
def set_settings(self, settings: Settings):
|
def set_settings(self, settings: Settings) -> None:
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.initialize_settings()
|
self.initialize_settings()
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ class LuaPlugin(PluginSettings):
|
|||||||
|
|
||||||
return cls(definition)
|
return cls(definition)
|
||||||
|
|
||||||
def set_settings(self, settings: Settings):
|
def set_settings(self, settings: Settings) -> None:
|
||||||
super().set_settings(settings)
|
super().set_settings(settings)
|
||||||
for option in self.definition.options:
|
for option in self.definition.options:
|
||||||
option.set_settings(self.settings)
|
option.set_settings(self.settings)
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dcs import Point
|
from dcs import Point
|
||||||
|
|
||||||
|
|
||||||
class PointWithHeading(Point):
|
class PointWithHeading(Point):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super(PointWithHeading, self).__init__(0, 0)
|
super(PointWithHeading, self).__init__(0, 0)
|
||||||
self.heading = 0
|
self.heading = 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_point(point: Point, heading: int):
|
def from_point(point: Point, heading: int) -> PointWithHeading:
|
||||||
p = PointWithHeading()
|
p = PointWithHeading()
|
||||||
p.x = point.x
|
p.x = point.x
|
||||||
p.y = point.y
|
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 collections import defaultdict
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Iterator
|
from types import TracebackType
|
||||||
|
from typing import Iterator, Optional, Type
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -23,7 +24,12 @@ class MultiEventTracer:
|
|||||||
def __enter__(self) -> MultiEventTracer:
|
def __enter__(self) -> MultiEventTracer:
|
||||||
return self
|
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():
|
for event, duration in self.events.items():
|
||||||
logging.debug("%s took %s", event, duration)
|
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 dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional, Any
|
||||||
|
|
||||||
from dcs.forcedoptions import ForcedOptions
|
from dcs.forcedoptions import ForcedOptions
|
||||||
|
|
||||||
@@ -34,11 +34,14 @@ class Settings:
|
|||||||
player_income_multiplier: float = 1.0
|
player_income_multiplier: float = 1.0
|
||||||
enemy_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 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
|
#: 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
|
#: squadron will not be removed if the limit is lowered and pilots will not be
|
||||||
#: immediately created if the limit is raised.
|
#: 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.
|
#: The number of pilots a squadron can replace per turn.
|
||||||
squadron_replenishment_rate: int = 4
|
squadron_replenishment_rate: int = 4
|
||||||
@@ -101,7 +104,7 @@ class Settings:
|
|||||||
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
|
||||||
self.plugins[self.plugin_settings_key(identifier)] = enabled
|
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
|
# __setstate__ is called with the dict of the object being unpickled. We
|
||||||
# can provide save compatibility for new settings options (which
|
# can provide save compatibility for new settings options (which
|
||||||
# normally would not be present in the unpickled object) by creating a
|
# normally would not be present in the unpickled object) by creating a
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from typing import (
|
|||||||
Optional,
|
Optional,
|
||||||
Iterator,
|
Iterator,
|
||||||
Sequence,
|
Sequence,
|
||||||
|
Any,
|
||||||
)
|
)
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
@@ -112,9 +113,19 @@ class Squadron:
|
|||||||
return self.name
|
return self.name
|
||||||
return f'{self.name} "{self.nickname}"'
|
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]:
|
def claim_available_pilot(self) -> Optional[Pilot]:
|
||||||
if not self.available_pilots:
|
if not self.available_pilots:
|
||||||
return None
|
return self.claim_new_pilot_if_allowed()
|
||||||
|
|
||||||
# For opfor, so player/AI option is irrelevant.
|
# For opfor, so player/AI option is irrelevant.
|
||||||
if not self.player:
|
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
|
# If they only *prefer* players and we're out of players, just return an AI
|
||||||
# pilot.
|
# pilot.
|
||||||
if not prefer_players:
|
if not prefer_players:
|
||||||
return None
|
return self.claim_new_pilot_if_allowed()
|
||||||
return self.available_pilots.pop()
|
return self.available_pilots.pop()
|
||||||
|
|
||||||
def claim_pilot(self, pilot: Pilot) -> None:
|
def claim_pilot(self, pilot: Pilot) -> None:
|
||||||
@@ -169,9 +180,12 @@ class Squadron:
|
|||||||
self.available_pilots.extend(new_pilots)
|
self.available_pilots.extend(new_pilots)
|
||||||
|
|
||||||
def replenish_lost_pilots(self) -> None:
|
def replenish_lost_pilots(self) -> None:
|
||||||
|
if not self.pilot_limits_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
replenish_count = min(
|
replenish_count = min(
|
||||||
self.game.settings.squadron_replenishment_rate,
|
self.game.settings.squadron_replenishment_rate,
|
||||||
self.number_of_unfilled_pilot_slots,
|
self._number_of_unfilled_pilot_slots,
|
||||||
)
|
)
|
||||||
if replenish_count > 0:
|
if replenish_count > 0:
|
||||||
self._recruit_pilots(replenish_count)
|
self._recruit_pilots(replenish_count)
|
||||||
@@ -183,7 +197,7 @@ class Squadron:
|
|||||||
def send_on_leave(pilot: Pilot) -> None:
|
def send_on_leave(pilot: Pilot) -> None:
|
||||||
pilot.send_on_leave()
|
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:
|
if not self.has_unfilled_pilot_slots:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Cannot return {pilot} from leave because {self} is full"
|
f"Cannot return {pilot} from leave because {self} is full"
|
||||||
@@ -213,20 +227,23 @@ class Squadron:
|
|||||||
return len(self.current_roster)
|
return len(self.current_roster)
|
||||||
|
|
||||||
@property
|
@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)
|
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def number_of_available_pilots(self) -> int:
|
def number_of_available_pilots(self) -> int:
|
||||||
return len(self.available_pilots)
|
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
|
@property
|
||||||
def has_available_pilots(self) -> bool:
|
def has_available_pilots(self) -> bool:
|
||||||
return bool(self.available_pilots)
|
return not self.pilot_limits_enabled or bool(self.available_pilots)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_unfilled_pilot_slots(self) -> bool:
|
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:
|
def can_auto_assign(self, task: FlightType) -> bool:
|
||||||
return task in self.auto_assignable_mission_types
|
return task in self.auto_assignable_mission_types
|
||||||
@@ -274,7 +291,7 @@ class Squadron:
|
|||||||
player=player,
|
player=player,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __setstate__(self, state) -> None:
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||||
# TODO: Remove save compat.
|
# TODO: Remove save compat.
|
||||||
if "auto_assignable_mission_types" not in state:
|
if "auto_assignable_mission_types" not in state:
|
||||||
state["auto_assignable_mission_types"] = set(state["mission_types"])
|
state["auto_assignable_mission_types"] = set(state["mission_types"])
|
||||||
@@ -368,6 +385,13 @@ class AirWing:
|
|||||||
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
|
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
|
||||||
return self.squadrons[aircraft]
|
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]:
|
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
|
||||||
for squadron in self.iter_squadrons():
|
for squadron in self.iter_squadrons():
|
||||||
if squadron.can_auto_assign(task):
|
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.groundunittype import GroundUnitType
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
|
|
||||||
BASE_MAX_STRENGTH = 1
|
BASE_MAX_STRENGTH = 1.0
|
||||||
BASE_MIN_STRENGTH = 0
|
BASE_MIN_STRENGTH = 0.0
|
||||||
|
|
||||||
|
|
||||||
class Base:
|
class Base:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.aircraft: dict[AircraftType, int] = {}
|
self.aircraft: dict[AircraftType, int] = {}
|
||||||
self.armor: dict[GroundUnitType, int] = {}
|
self.armor: dict[GroundUnitType, int] = {}
|
||||||
self.strength = 1
|
self.strength = 1.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_aircraft(self) -> int:
|
def total_aircraft(self) -> int:
|
||||||
@@ -31,7 +31,7 @@ class Base:
|
|||||||
total += unit_type.price * count
|
total += unit_type.price * count
|
||||||
return total
|
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(
|
return sum(
|
||||||
[
|
[
|
||||||
c
|
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():
|
for unit_type, unit_count in units.items():
|
||||||
if unit_count <= 0:
|
if unit_count <= 0:
|
||||||
continue
|
continue
|
||||||
@@ -56,7 +56,7 @@ class Base:
|
|||||||
|
|
||||||
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
|
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():
|
for unit_type, count in units_lost.items():
|
||||||
target_dict: dict[Any, int]
|
target_dict: dict[Any, int]
|
||||||
if unit_type in self.aircraft:
|
if unit_type in self.aircraft:
|
||||||
@@ -75,7 +75,7 @@ class Base:
|
|||||||
if target_dict[unit_type] == 0:
|
if target_dict[unit_type] == 0:
|
||||||
del target_dict[unit_type]
|
del target_dict[unit_type]
|
||||||
|
|
||||||
def affect_strength(self, amount):
|
def affect_strength(self, amount: float) -> None:
|
||||||
self.strength += amount
|
self.strength += amount
|
||||||
if self.strength > BASE_MAX_STRENGTH:
|
if self.strength > BASE_MAX_STRENGTH:
|
||||||
self.strength = BASE_MAX_STRENGTH
|
self.strength = BASE_MAX_STRENGTH
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import math
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from pathlib import Path
|
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 import Mission
|
||||||
from dcs.countries import (
|
from dcs.countries import (
|
||||||
@@ -29,14 +29,14 @@ from dcs.terrain import (
|
|||||||
persiangulf,
|
persiangulf,
|
||||||
syria,
|
syria,
|
||||||
thechannel,
|
thechannel,
|
||||||
|
marianaislands,
|
||||||
)
|
)
|
||||||
from dcs.terrain.terrain import Airport, Terrain
|
from dcs.terrain.terrain import Airport, Terrain
|
||||||
from dcs.unitgroup import (
|
from dcs.unitgroup import (
|
||||||
FlyingGroup,
|
|
||||||
Group,
|
|
||||||
ShipGroup,
|
ShipGroup,
|
||||||
StaticGroup,
|
StaticGroup,
|
||||||
VehicleGroup,
|
VehicleGroup,
|
||||||
|
PlaneGroup,
|
||||||
)
|
)
|
||||||
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
|
||||||
from pyproj import CRS, Transformer
|
from pyproj import CRS, Transformer
|
||||||
@@ -56,10 +56,14 @@ from .landmap import Landmap, load_landmap, poly_contains
|
|||||||
from .latlon import LatLon
|
from .latlon import LatLon
|
||||||
from .projections import TransverseMercator
|
from .projections import TransverseMercator
|
||||||
from ..point_with_heading import PointWithHeading
|
from ..point_with_heading import PointWithHeading
|
||||||
|
from ..positioned import Positioned
|
||||||
from ..profiling import logged_duration
|
from ..profiling import logged_duration
|
||||||
from ..scenery_group import SceneryGroup
|
from ..scenery_group import SceneryGroup
|
||||||
from ..utils import Distance, meters
|
from ..utils import Distance, meters
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import TheaterGroundObject
|
||||||
|
|
||||||
SIZE_TINY = 150
|
SIZE_TINY = 150
|
||||||
SIZE_SMALL = 600
|
SIZE_SMALL = 600
|
||||||
SIZE_REGULAR = 1000
|
SIZE_REGULAR = 1000
|
||||||
@@ -181,7 +185,7 @@ class MizCampaignLoader:
|
|||||||
def red(self) -> Country:
|
def red(self) -> Country:
|
||||||
return self.country(blue=False)
|
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:
|
for group in self.country(blue).plane_group:
|
||||||
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
||||||
yield group
|
yield group
|
||||||
@@ -305,26 +309,26 @@ class MizCampaignLoader:
|
|||||||
control_point.captured = blue
|
control_point.captured = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = group.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for group in self.carriers(blue):
|
for ship in self.carriers(blue):
|
||||||
# TODO: Name the carrier.
|
# TODO: Name the carrier.
|
||||||
control_point = 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 = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = ship.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for group in self.lhas(blue):
|
for ship in self.lhas(blue):
|
||||||
# TODO: Name the LHA.db
|
# 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 = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = ship.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
for group in self.fobs(blue):
|
for fob in self.fobs(blue):
|
||||||
control_point = Fob(
|
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 = blue
|
||||||
control_point.captured_invert = group.late_activation
|
control_point.captured_invert = fob.late_activation
|
||||||
control_points[control_point.id] = control_point
|
control_points[control_point.id] = control_point
|
||||||
|
|
||||||
return control_points
|
return control_points
|
||||||
@@ -385,22 +389,22 @@ class MizCampaignLoader:
|
|||||||
origin, list(reversed(waypoints))
|
origin, list(reversed(waypoints))
|
||||||
)
|
)
|
||||||
|
|
||||||
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
|
def objective_info(self, near: Positioned) -> Tuple[ControlPoint, Distance]:
|
||||||
closest = self.theater.closest_control_point(group.position)
|
closest = self.theater.closest_control_point(near.position)
|
||||||
distance = meters(closest.position.distance_to_point(group.position))
|
distance = meters(closest.position.distance_to_point(near.position))
|
||||||
return closest, distance
|
return closest, distance
|
||||||
|
|
||||||
def add_preset_locations(self) -> None:
|
def add_preset_locations(self) -> None:
|
||||||
for group in self.offshore_strike_targets:
|
for static in self.offshore_strike_targets:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.preset_locations.offshore_strike_locations.append(
|
closest.preset_locations.offshore_strike_locations.append(
|
||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(static.position, static.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.ships:
|
for ship in self.ships:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(ship)
|
||||||
closest.preset_locations.ships.append(
|
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:
|
for group in self.missile_sites:
|
||||||
@@ -451,33 +455,33 @@ class MizCampaignLoader:
|
|||||||
PointWithHeading.from_point(group.position, group.units[0].heading)
|
PointWithHeading.from_point(group.position, group.units[0].heading)
|
||||||
)
|
)
|
||||||
|
|
||||||
for group in self.helipads:
|
for static in self.helipads:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.helipads.append(
|
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:
|
for static in self.factories:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.preset_locations.factories.append(
|
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:
|
for static in self.ammunition_depots:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.preset_locations.ammunition_depots.append(
|
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:
|
for static in self.strike_targets:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(static)
|
||||||
closest.preset_locations.strike_locations.append(
|
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:
|
for scenery_group in self.scenery:
|
||||||
closest, distance = self.objective_info(group)
|
closest, distance = self.objective_info(scenery_group)
|
||||||
closest.preset_locations.scenery.append(group)
|
closest.preset_locations.scenery.append(scenery_group)
|
||||||
|
|
||||||
def populate_theater(self) -> None:
|
def populate_theater(self) -> None:
|
||||||
for control_point in self.control_points.values():
|
for control_point in self.control_points.values():
|
||||||
@@ -504,7 +508,7 @@ class ConflictTheater:
|
|||||||
"""
|
"""
|
||||||
daytime_map: Dict[str, Tuple[int, int]]
|
daytime_map: Dict[str, Tuple[int, int]]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.controlpoints: List[ControlPoint] = []
|
self.controlpoints: List[ControlPoint] = []
|
||||||
self.point_to_ll_transformer = Transformer.from_crs(
|
self.point_to_ll_transformer = Transformer.from_crs(
|
||||||
self.projection_parameters.to_crs(), CRS("WGS84")
|
self.projection_parameters.to_crs(), CRS("WGS84")
|
||||||
@@ -536,10 +540,12 @@ class ConflictTheater:
|
|||||||
CRS("WGS84"), self.projection_parameters.to_crs()
|
CRS("WGS84"), self.projection_parameters.to_crs()
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_controlpoint(self, point: ControlPoint):
|
def add_controlpoint(self, point: ControlPoint) -> None:
|
||||||
self.controlpoints.append(point)
|
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 = []
|
found = []
|
||||||
for cp in self.controlpoints:
|
for cp in self.controlpoints:
|
||||||
for g in cp.ground_objects:
|
for g in cp.ground_objects:
|
||||||
@@ -581,12 +587,12 @@ class ConflictTheater:
|
|||||||
|
|
||||||
return True
|
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
|
"""Returns the nearest point inside a land exclusion zone from point
|
||||||
`extend_dist` determines how far inside the zone the point should be placed"""
|
`extend_dist` determines how far inside the zone the point should be placed"""
|
||||||
if self.is_on_land(point):
|
if self.is_on_land(near):
|
||||||
return point
|
return near
|
||||||
point = geometry.Point(point.x, point.y)
|
point = geometry.Point(near.x, near.y)
|
||||||
nearest_points = []
|
nearest_points = []
|
||||||
if not self.landmap:
|
if not self.landmap:
|
||||||
raise RuntimeError("Landmap not initialized")
|
raise RuntimeError("Landmap not initialized")
|
||||||
@@ -698,6 +704,7 @@ class ConflictTheater:
|
|||||||
"Normandy": NormandyTheater,
|
"Normandy": NormandyTheater,
|
||||||
"The Channel": TheChannelTheater,
|
"The Channel": TheChannelTheater,
|
||||||
"Syria": SyriaTheater,
|
"Syria": SyriaTheater,
|
||||||
|
"MarianaIslands": MarianaIslandsTheater,
|
||||||
}
|
}
|
||||||
theater = theaters[data["theater"]]
|
theater = theaters[data["theater"]]
|
||||||
t = theater()
|
t = theater()
|
||||||
@@ -856,3 +863,22 @@ class SyriaTheater(ConflictTheater):
|
|||||||
from .syria import PARAMETERS
|
from .syria import PARAMETERS
|
||||||
|
|
||||||
return 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 (
|
from .theatergroundobject import (
|
||||||
GenericCarrierGroundObject,
|
GenericCarrierGroundObject,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
|
NavalGroundObject,
|
||||||
)
|
)
|
||||||
from ..dcs.aircrafttype import AircraftType
|
from ..dcs.aircrafttype import AircraftType
|
||||||
from ..dcs.groundunittype import GroundUnitType
|
from ..dcs.groundunittype import GroundUnitType
|
||||||
@@ -290,15 +291,15 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
at: db.StartingPosition,
|
at: db.StartingPosition,
|
||||||
size: int,
|
size: int,
|
||||||
importance: float,
|
importance: float,
|
||||||
has_frontline=True,
|
has_frontline: bool = True,
|
||||||
cptype=ControlPointType.AIRBASE,
|
cptype: ControlPointType = ControlPointType.AIRBASE,
|
||||||
):
|
) -> None:
|
||||||
super().__init__(name, position)
|
super().__init__(name, position)
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
self.id = cp_id
|
self.id = cp_id
|
||||||
self.full_name = name
|
self.full_name = name
|
||||||
self.at = at
|
self.at = at
|
||||||
self.connected_objectives: List[TheaterGroundObject] = []
|
self.connected_objectives: List[TheaterGroundObject[Any]] = []
|
||||||
self.preset_locations = PresetLocations()
|
self.preset_locations = PresetLocations()
|
||||||
self.helipads: List[PointWithHeading] = []
|
self.helipads: List[PointWithHeading] = []
|
||||||
|
|
||||||
@@ -322,11 +323,11 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
self.target_position: Optional[Point] = None
|
self.target_position: Optional[Point] = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<{__class__}: {self.name}>"
|
return f"<{self.__class__}: {self.name}>"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ground_objects(self) -> List[TheaterGroundObject]:
|
def ground_objects(self) -> List[TheaterGroundObject[Any]]:
|
||||||
return list(self.connected_objectives)
|
return list(self.connected_objectives)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -334,11 +335,11 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
def heading(self) -> int:
|
def heading(self) -> int:
|
||||||
...
|
...
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_global(self):
|
def is_global(self) -> bool:
|
||||||
return not self.connected_points
|
return not self.connected_points
|
||||||
|
|
||||||
def transitive_connected_friendly_points(
|
def transitive_connected_friendly_points(
|
||||||
@@ -405,21 +406,21 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_carrier(self):
|
def is_carrier(self) -> bool:
|
||||||
"""
|
"""
|
||||||
:return: Whether this control point is an aircraft carrier
|
:return: Whether this control point is an aircraft carrier
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_fleet(self):
|
def is_fleet(self) -> bool:
|
||||||
"""
|
"""
|
||||||
:return: Whether this control point is a boat (mobile)
|
:return: Whether this control point is a boat (mobile)
|
||||||
"""
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_lha(self):
|
def is_lha(self) -> bool:
|
||||||
"""
|
"""
|
||||||
:return: Whether this control point is an LHA
|
:return: Whether this control point is an LHA
|
||||||
"""
|
"""
|
||||||
@@ -439,7 +440,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def total_aircraft_parking(self):
|
def total_aircraft_parking(self) -> int:
|
||||||
"""
|
"""
|
||||||
:return: The maximum number of aircraft that can be stored in this
|
:return: The maximum number of aircraft that can be stored in this
|
||||||
control point
|
control point
|
||||||
@@ -471,7 +472,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
...
|
...
|
||||||
|
|
||||||
# TODO: Should be naval specific.
|
# 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
|
Get the carrier group name if the airbase is a carrier
|
||||||
:return: Carrier group name
|
:return: Carrier group name
|
||||||
@@ -497,10 +498,12 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# TODO: Should be Airbase specific.
|
# TODO: Should be Airbase specific.
|
||||||
def is_connected(self, to) -> bool:
|
def is_connected(self, to: ControlPoint) -> bool:
|
||||||
return to in self.connected_points
|
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 = []
|
found = []
|
||||||
for g in self.ground_objects:
|
for g in self.ground_objects:
|
||||||
if g.obj_name == obj_name:
|
if g.obj_name == obj_name:
|
||||||
@@ -522,7 +525,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
f"vehicles have been captured and sold for ${total}M."
|
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
|
# When there are multiple valid destinations, deliver units to whichever
|
||||||
# base is least defended first. The closest approximation of unit
|
# base is least defended first. The closest approximation of unit
|
||||||
# strength we have is price
|
# 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"])
|
return len([obj for obj in self.connected_objectives if obj.category == "ammo"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -764,8 +767,8 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
|
|
||||||
class Airfield(ControlPoint):
|
class Airfield(ControlPoint):
|
||||||
def __init__(
|
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__(
|
super().__init__(
|
||||||
airport.id,
|
airport.id,
|
||||||
airport.name,
|
airport.name,
|
||||||
@@ -879,9 +882,12 @@ class NavalControlPoint(ControlPoint, ABC):
|
|||||||
def heading(self) -> int:
|
def heading(self) -> int:
|
||||||
return 0 # TODO compute heading
|
return 0 # TODO compute heading
|
||||||
|
|
||||||
def find_main_tgo(self) -> TheaterGroundObject:
|
def find_main_tgo(self) -> GenericCarrierGroundObject:
|
||||||
for g in self.ground_objects:
|
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
|
return g
|
||||||
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
|
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
|
||||||
|
|
||||||
@@ -960,7 +966,7 @@ class Carrier(NavalControlPoint):
|
|||||||
raise RuntimeError("Carriers cannot be captured")
|
raise RuntimeError("Carriers cannot be captured")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_carrier(self):
|
def is_carrier(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def can_operate(self, aircraft: AircraftType) -> bool:
|
def can_operate(self, aircraft: AircraftType) -> bool:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Iterator, List, Tuple
|
from typing import Iterator, List, Tuple, Any
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
|
|
||||||
@@ -66,7 +66,15 @@ class FrontLine(MissionTarget):
|
|||||||
self.segments: List[FrontLineSegment] = [
|
self.segments: List[FrontLineSegment] = [
|
||||||
FrontLineSegment(a, b) for a, b in pairwise(route)
|
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:
|
def control_point_hostile_to(self, player: bool) -> ControlPoint:
|
||||||
if player:
|
if player:
|
||||||
@@ -87,14 +95,6 @@ class FrontLine(MissionTarget):
|
|||||||
]
|
]
|
||||||
yield from super().mission_types(for_player)
|
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
|
@property
|
||||||
def points(self) -> Iterator[Point]:
|
def points(self) -> Iterator[Point]:
|
||||||
yield self.segments[0].point_a
|
yield self.segments[0].point_a
|
||||||
@@ -107,12 +107,12 @@ class FrontLine(MissionTarget):
|
|||||||
return self.blue_cp, self.red_cp
|
return self.blue_cp, self.red_cp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attack_distance(self):
|
def attack_distance(self) -> float:
|
||||||
"""The total distance of all segments"""
|
"""The total distance of all segments"""
|
||||||
return sum(i.attack_distance for i in self.segments)
|
return sum(i.attack_distance for i in self.segments)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attack_heading(self):
|
def attack_heading(self) -> float:
|
||||||
"""The heading of the active attack segment from player to enemy control point"""
|
"""The heading of the active attack segment from player to enemy control point"""
|
||||||
return self.active_segment.attack_heading
|
return self.active_segment.attack_heading
|
||||||
|
|
||||||
@@ -149,6 +149,9 @@ class FrontLine(MissionTarget):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
remaining_dist -= segment.attack_distance
|
remaining_dist -= segment.attack_distance
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Could not find front line point {distance} from {self.blue_cp}"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _position_distance(self) -> float:
|
def _position_distance(self) -> float:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Landmap:
|
|||||||
exclusion_zones: MultiPolygon
|
exclusion_zones: MultiPolygon
|
||||||
sea_zones: MultiPolygon
|
sea_zones: MultiPolygon
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self) -> None:
|
||||||
if not self.inclusion_zones.is_valid:
|
if not self.inclusion_zones.is_valid:
|
||||||
raise RuntimeError("Inclusion zones not valid")
|
raise RuntimeError("Inclusion zones not valid")
|
||||||
if not self.exclusion_zones.is_valid:
|
if not self.exclusion_zones.is_valid:
|
||||||
@@ -36,13 +36,5 @@ def load_landmap(filename: str) -> Optional[Landmap]:
|
|||||||
return None
|
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))
|
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 __future__ import annotations
|
||||||
|
|
||||||
|
from collections import Sequence
|
||||||
from typing import Iterator, TYPE_CHECKING, List, Union
|
from typing import Iterator, TYPE_CHECKING, List, Union
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
@@ -20,7 +21,7 @@ class MissionTarget:
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.position = position
|
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."""
|
"""Computes the distance to the given mission target."""
|
||||||
return self.position.distance_to_point(other.position)
|
return self.position.distance_to_point(other.position)
|
||||||
|
|
||||||
@@ -45,5 +46,5 @@ class MissionTarget:
|
|||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -78,20 +78,33 @@ class GeneratorSettings:
|
|||||||
no_enemy_navy: bool
|
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:
|
class GameGenerator:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
player: str,
|
player: Faction,
|
||||||
enemy: str,
|
enemy: Faction,
|
||||||
theater: ConflictTheater,
|
theater: ConflictTheater,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
generator_settings: GeneratorSettings,
|
generator_settings: GeneratorSettings,
|
||||||
|
mod_settings: ModSettings,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.player = player
|
self.player = player
|
||||||
self.enemy = enemy
|
self.enemy = enemy
|
||||||
self.theater = theater
|
self.theater = theater
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.generator_settings = generator_settings
|
self.generator_settings = generator_settings
|
||||||
|
self.mod_settings = mod_settings
|
||||||
|
|
||||||
def generate(self) -> Game:
|
def generate(self) -> Game:
|
||||||
with logged_duration("TGO population"):
|
with logged_duration("TGO population"):
|
||||||
@@ -99,8 +112,8 @@ class GameGenerator:
|
|||||||
namegen.reset()
|
namegen.reset()
|
||||||
self.prepare_theater()
|
self.prepare_theater()
|
||||||
game = Game(
|
game = Game(
|
||||||
player_name=self.player,
|
player_faction=self.player.apply_mod_settings(self.mod_settings),
|
||||||
enemy_name=self.enemy,
|
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings),
|
||||||
theater=self.theater,
|
theater=self.theater,
|
||||||
start_date=self.generator_settings.start_date,
|
start_date=self.generator_settings.start_date,
|
||||||
settings=self.settings,
|
settings=self.settings,
|
||||||
@@ -159,9 +172,9 @@ class ControlPointGroundObjectGenerator:
|
|||||||
@property
|
@property
|
||||||
def faction_name(self) -> str:
|
def faction_name(self) -> str:
|
||||||
if self.control_point.captured:
|
if self.control_point.captured:
|
||||||
return self.game.player_name
|
return self.game.player_faction.name
|
||||||
else:
|
else:
|
||||||
return self.game.enemy_name
|
return self.game.enemy_faction.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def faction(self) -> Faction:
|
def faction(self) -> Faction:
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
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.mapping import Point
|
||||||
from dcs.triggers import TriggerZone
|
from dcs.triggers import TriggerZone
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from dcs.unitgroup import Group
|
from dcs.unitgroup import ShipGroup, VehicleGroup
|
||||||
from dcs.unittype import VehicleType
|
|
||||||
|
|
||||||
from .. import db
|
from .. import db
|
||||||
from ..data.radar_db import (
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -66,7 +69,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
self.control_point = control_point
|
self.control_point = control_point
|
||||||
self.dcs_identifier = dcs_identifier
|
self.dcs_identifier = dcs_identifier
|
||||||
self.sea_object = sea_object
|
self.sea_object = sea_object
|
||||||
self.groups: List[Group] = []
|
self.groups: List[GroupT] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_dead(self) -> bool:
|
def is_dead(self) -> bool:
|
||||||
@@ -147,7 +150,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
|
def _max_range_of_type(self, group: GroupT, range_type: str) -> Distance:
|
||||||
if not self.might_have_aa:
|
if not self.might_have_aa:
|
||||||
return meters(0)
|
return meters(0)
|
||||||
|
|
||||||
@@ -168,13 +171,13 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
def max_detection_range(self) -> Distance:
|
def max_detection_range(self) -> Distance:
|
||||||
return max(self.detection_range(g) for g in self.groups)
|
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")
|
return self._max_range_of_type(group, "detection_range")
|
||||||
|
|
||||||
def max_threat_range(self) -> Distance:
|
def max_threat_range(self) -> Distance:
|
||||||
return max(self.threat_range(g) for g in self.groups)
|
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")
|
return self._max_range_of_type(group, "threat_range")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -187,7 +190,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
|
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
|
||||||
return self.units
|
return self.units
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -206,7 +209,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class BuildingGroundObject(TheaterGroundObject):
|
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -217,7 +220,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
|||||||
heading: int,
|
heading: int,
|
||||||
control_point: ControlPoint,
|
control_point: ControlPoint,
|
||||||
dcs_identifier: str,
|
dcs_identifier: str,
|
||||||
is_fob_structure=False,
|
is_fob_structure: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
@@ -253,7 +256,7 @@ class BuildingGroundObject(TheaterGroundObject):
|
|||||||
def kill(self) -> None:
|
def kill(self) -> None:
|
||||||
self._dead = True
|
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:
|
for tgo in self.control_point.ground_objects:
|
||||||
if tgo.obj_name == self.obj_name and not tgo.is_dead:
|
if tgo.obj_name == self.obj_name and not tgo.is_dead:
|
||||||
yield tgo
|
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]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
|
|
||||||
@@ -407,7 +410,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
|
|||||||
return f"{self.faction_color}|EWR|{super().group_name}"
|
return f"{self.faction_color}|EWR|{super().group_name}"
|
||||||
|
|
||||||
|
|
||||||
class MissileSiteGroundObject(TheaterGroundObject):
|
class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
self, name: str, group_id: int, position: Point, control_point: ControlPoint
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -431,14 +434,14 @@ class MissileSiteGroundObject(TheaterGroundObject):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class CoastalSiteGroundObject(TheaterGroundObject):
|
class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
group_id: int,
|
group_id: int,
|
||||||
position: Point,
|
position: Point,
|
||||||
control_point: ControlPoint,
|
control_point: ControlPoint,
|
||||||
heading,
|
heading: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
@@ -460,10 +463,10 @@ class CoastalSiteGroundObject(TheaterGroundObject):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# TODO: Differentiate types.
|
# The SamGroundObject represents all type of AA
|
||||||
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
|
# The TGO can have multiple types of units (AAA,SAM,Support...)
|
||||||
# be split into their own types.
|
# Differentiation can be made during generation with the airdefensegroupgenerator
|
||||||
class SamGroundObject(TheaterGroundObject):
|
class SamGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -481,18 +484,6 @@ class SamGroundObject(TheaterGroundObject):
|
|||||||
dcs_identifier="AA",
|
dcs_identifier="AA",
|
||||||
sea_object=False,
|
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]:
|
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
|
||||||
from gen.flights.flight import FlightType
|
from gen.flights.flight import FlightType
|
||||||
@@ -506,33 +497,25 @@ class SamGroundObject(TheaterGroundObject):
|
|||||||
def might_have_aa(self) -> bool:
|
def might_have_aa(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
|
def threat_range(self, group: VehicleGroup, radar_only: bool = False) -> Distance:
|
||||||
max_non_radar = meters(0)
|
max_non_radar = meters(0)
|
||||||
live_trs = set()
|
live_trs = set()
|
||||||
max_telar_range = meters(0)
|
max_telar_range = meters(0)
|
||||||
launchers = set()
|
launchers = set()
|
||||||
for unit in group.units:
|
for unit in group.units:
|
||||||
unit_type = db.unit_type_from_name(unit.type)
|
unit_type = db.vehicle_type_from_name(unit.type)
|
||||||
if unit_type is None or not issubclass(unit_type, VehicleType):
|
|
||||||
continue
|
|
||||||
if unit_type in TRACK_RADARS:
|
if unit_type in TRACK_RADARS:
|
||||||
live_trs.add(unit_type)
|
live_trs.add(unit_type)
|
||||||
elif unit_type in TELARS:
|
elif unit_type in TELARS:
|
||||||
max_telar_range = max(
|
max_telar_range = max(max_telar_range, meters(unit_type.threat_range))
|
||||||
max_telar_range, meters(getattr(unit_type, "threat_range", 0))
|
|
||||||
)
|
|
||||||
elif unit_type in LAUNCHER_TRACKER_PAIRS:
|
elif unit_type in LAUNCHER_TRACKER_PAIRS:
|
||||||
launchers.add(unit_type)
|
launchers.add(unit_type)
|
||||||
else:
|
else:
|
||||||
max_non_radar = max(
|
max_non_radar = max(max_non_radar, meters(unit_type.threat_range))
|
||||||
max_non_radar, meters(getattr(unit_type, "threat_range", 0))
|
|
||||||
)
|
|
||||||
max_tel_range = meters(0)
|
max_tel_range = meters(0)
|
||||||
for launcher in launchers:
|
for launcher in launchers:
|
||||||
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
|
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
|
||||||
max_tel_range = max(
|
max_tel_range = max(max_tel_range, meters(unit_type.threat_range))
|
||||||
max_tel_range, meters(getattr(launcher, "threat_range"))
|
|
||||||
)
|
|
||||||
if radar_only:
|
if radar_only:
|
||||||
return max(max_tel_range, max_telar_range)
|
return max(max_tel_range, max_telar_range)
|
||||||
else:
|
else:
|
||||||
@@ -547,7 +530,7 @@ class SamGroundObject(TheaterGroundObject):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class VehicleGroupGroundObject(TheaterGroundObject):
|
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -575,7 +558,7 @@ class VehicleGroupGroundObject(TheaterGroundObject):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class EwrGroundObject(TheaterGroundObject):
|
class EwrGroundObject(TheaterGroundObject[VehicleGroup]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ ThreatPoly = Union[MultiPolygon, Polygon]
|
|||||||
|
|
||||||
class ThreatZones:
|
class ThreatZones:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
|
self,
|
||||||
|
airbases: ThreatPoly,
|
||||||
|
air_defenses: ThreatPoly,
|
||||||
|
radar_sam_threats: ThreatPoly,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.airbases = airbases
|
self.airbases = airbases
|
||||||
self.air_defenses = air_defenses
|
self.air_defenses = air_defenses
|
||||||
@@ -44,8 +47,10 @@ class ThreatZones:
|
|||||||
boundary = self.closest_boundary(point)
|
boundary = self.closest_boundary(point)
|
||||||
return meters(boundary.distance_to_point(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
|
@singledispatchmethod
|
||||||
def threatened(self, position) -> bool:
|
def threatened(self, position) -> bool: # type: ignore
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@threatened.register
|
@threatened.register
|
||||||
@@ -61,8 +66,10 @@ class ThreatZones:
|
|||||||
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
|
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
|
@singledispatchmethod
|
||||||
def threatened_by_aircraft(self, target) -> bool:
|
def threatened_by_aircraft(self, target) -> bool: # type: ignore
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@threatened_by_aircraft.register
|
@threatened_by_aircraft.register
|
||||||
@@ -82,8 +89,10 @@ class ThreatZones:
|
|||||||
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
|
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
|
@singledispatchmethod
|
||||||
def threatened_by_air_defense(self, target) -> bool:
|
def threatened_by_air_defense(self, target) -> bool: # type: ignore
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@threatened_by_air_defense.register
|
@threatened_by_air_defense.register
|
||||||
@@ -102,8 +111,10 @@ class ThreatZones:
|
|||||||
self.dcs_to_shapely_point(target.position)
|
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
|
@singledispatchmethod
|
||||||
def threatened_by_radar_sam(self, target) -> bool:
|
def threatened_by_radar_sam(self, target) -> bool: # type: ignore
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@threatened_by_radar_sam.register
|
@threatened_by_radar_sam.register
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from collections import defaultdict
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import singledispatchmethod
|
from functools import singledispatchmethod
|
||||||
from typing import (
|
from typing import (
|
||||||
Dict,
|
|
||||||
Generic,
|
Generic,
|
||||||
Iterator,
|
Iterator,
|
||||||
List,
|
List,
|
||||||
@@ -72,10 +71,18 @@ class TransferOrder:
|
|||||||
player: bool = field(init=False)
|
player: bool = field(init=False)
|
||||||
|
|
||||||
#: The units being transferred.
|
#: The units being transferred.
|
||||||
units: Dict[GroundUnitType, int]
|
units: dict[GroundUnitType, int]
|
||||||
|
|
||||||
transport: Optional[Transport] = field(default=None)
|
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:
|
def __post_init__(self) -> None:
|
||||||
self.position = self.origin
|
self.position = self.origin
|
||||||
self.player = self.origin.is_friendly(to_player=True)
|
self.player = self.origin.is_friendly(to_player=True)
|
||||||
@@ -91,12 +98,15 @@ class TransferOrder:
|
|||||||
|
|
||||||
def kill_unit(self, unit_type: GroundUnitType) -> None:
|
def kill_unit(self, unit_type: GroundUnitType) -> None:
|
||||||
if unit_type not in self.units or not self.units[unit_type]:
|
if unit_type not in self.units or not self.units[unit_type]:
|
||||||
raise KeyError(f"{self.destination} has no {unit_type} remaining")
|
raise KeyError(f"{self} has no {unit_type} remaining")
|
||||||
self.units[unit_type] -= 1
|
if self.units[unit_type] == 1:
|
||||||
|
del self.units[unit_type]
|
||||||
|
else:
|
||||||
|
self.units[unit_type] -= 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
return sum(c for c in self.units.values())
|
return sum(self.units.values())
|
||||||
|
|
||||||
def iter_units(self) -> Iterator[GroundUnitType]:
|
def iter_units(self) -> Iterator[GroundUnitType]:
|
||||||
for unit_type, count in self.units.items():
|
for unit_type, count in self.units.items():
|
||||||
@@ -105,7 +115,7 @@ class TransferOrder:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def completed(self) -> bool:
|
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:
|
def disband_at(self, location: ControlPoint) -> None:
|
||||||
logging.info(f"Units halting at {location}.")
|
logging.info(f"Units halting at {location}.")
|
||||||
@@ -120,22 +130,64 @@ class TransferOrder:
|
|||||||
)
|
)
|
||||||
return self.transport.destination
|
return self.transport.destination
|
||||||
|
|
||||||
def proceed(self) -> None:
|
def find_escape_route(self) -> Optional[ControlPoint]:
|
||||||
if self.transport is None:
|
if self.transport is not None:
|
||||||
return
|
return self.transport.find_escape_route()
|
||||||
|
return None
|
||||||
|
|
||||||
if not self.destination.is_friendly(self.player):
|
def disband(self) -> None:
|
||||||
logging.info(f"Transfer destination {self.destination} was captured.")
|
"""
|
||||||
if self.position.is_friendly(self.player):
|
Disbands the specific transfer at the current position if friendly, at a
|
||||||
self.disband_at(self.position)
|
possible escape route or kills all units if none is possible
|
||||||
elif (escape_route := self.transport.find_escape_route()) is not None:
|
"""
|
||||||
self.disband_at(escape_route)
|
if self.position.is_friendly(self.player):
|
||||||
else:
|
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(
|
logging.info(
|
||||||
f"No escape route available. Units were surrounded and destroyed "
|
f"Current position ({self.position}) "
|
||||||
"during transfer."
|
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
|
return
|
||||||
|
|
||||||
self.position = self.next_stop
|
self.position = self.next_stop
|
||||||
@@ -156,7 +208,7 @@ class Airlift(Transport):
|
|||||||
self.flight = flight
|
self.flight = flight
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def units(self) -> Dict[GroundUnitType, int]:
|
def units(self) -> dict[GroundUnitType, int]:
|
||||||
return self.transfer.units
|
return self.transfer.units
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -261,8 +313,12 @@ class AirliftPlanner:
|
|||||||
required,
|
required,
|
||||||
available_aircraft,
|
available_aircraft,
|
||||||
squadron.aircraft.dcs_unit_type.group_size_max,
|
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
|
capacity = flight_size * capacity_each
|
||||||
|
|
||||||
if capacity < self.transfer.size:
|
if capacity < self.transfer.size:
|
||||||
@@ -334,11 +390,11 @@ class MultiGroupTransport(MissionTarget, Transport):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
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
|
@property
|
||||||
def units(self) -> dict[GroundUnitType, int]:
|
def units(self) -> dict[GroundUnitType, int]:
|
||||||
units: Dict[GroundUnitType, int] = defaultdict(int)
|
units: dict[GroundUnitType, int] = defaultdict(int)
|
||||||
for transfer in self.transfers:
|
for transfer in self.transfers:
|
||||||
for unit_type, count in transfer.units.items():
|
for unit_type, count in transfer.units.items():
|
||||||
units[unit_type] += count
|
units[unit_type] += count
|
||||||
@@ -414,8 +470,8 @@ TransportType = TypeVar("TransportType", bound=MultiGroupTransport)
|
|||||||
class TransportMap(Generic[TransportType]):
|
class TransportMap(Generic[TransportType]):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# Dict of origin -> destination -> transport.
|
# Dict of origin -> destination -> transport.
|
||||||
self.transports: Dict[
|
self.transports: dict[
|
||||||
ControlPoint, Dict[ControlPoint, TransportType]
|
ControlPoint, dict[ControlPoint, TransportType]
|
||||||
] = defaultdict(dict)
|
] = defaultdict(dict)
|
||||||
|
|
||||||
def create_transport(
|
def create_transport(
|
||||||
@@ -469,14 +525,14 @@ class TransportMap(Generic[TransportType]):
|
|||||||
yield from destination_dict.values()
|
yield from destination_dict.values()
|
||||||
|
|
||||||
|
|
||||||
class ConvoyMap(TransportMap):
|
class ConvoyMap(TransportMap[Convoy]):
|
||||||
def create_transport(
|
def create_transport(
|
||||||
self, origin: ControlPoint, destination: ControlPoint
|
self, origin: ControlPoint, destination: ControlPoint
|
||||||
) -> Convoy:
|
) -> Convoy:
|
||||||
return Convoy(origin, destination)
|
return Convoy(origin, destination)
|
||||||
|
|
||||||
|
|
||||||
class CargoShipMap(TransportMap):
|
class CargoShipMap(TransportMap[CargoShip]):
|
||||||
def create_transport(
|
def create_transport(
|
||||||
self, origin: ControlPoint, destination: ControlPoint
|
self, origin: ControlPoint, destination: ControlPoint
|
||||||
) -> CargoShip:
|
) -> CargoShip:
|
||||||
@@ -542,8 +598,14 @@ class PendingTransfers:
|
|||||||
self.pending_transfers.append(new_transfer)
|
self.pending_transfers.append(new_transfer)
|
||||||
return 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
|
@singledispatchmethod
|
||||||
def cancel_transport(self, transport, transfer: TransferOrder) -> None:
|
def cancel_transport( # type: ignore
|
||||||
|
self,
|
||||||
|
transport,
|
||||||
|
transfer: TransferOrder,
|
||||||
|
) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@cancel_transport.register
|
@cancel_transport.register
|
||||||
@@ -576,6 +638,12 @@ class PendingTransfers:
|
|||||||
transfer.origin.base.commission_units(transfer.units)
|
transfer.origin.base.commission_units(transfer.units)
|
||||||
|
|
||||||
def perform_transfers(self) -> None:
|
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 = []
|
incomplete = []
|
||||||
for transfer in self.pending_transfers:
|
for transfer in self.pending_transfers:
|
||||||
transfer.proceed()
|
transfer.proceed()
|
||||||
@@ -586,17 +654,71 @@ class PendingTransfers:
|
|||||||
self.cargo_ships.disband_all()
|
self.cargo_ships.disband_all()
|
||||||
|
|
||||||
def plan_transports(self) -> None:
|
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:
|
for transfer in self.pending_transfers:
|
||||||
if transfer.transport is None:
|
if transfer.transport is None:
|
||||||
self.arrange_transport(transfer)
|
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:
|
def order_airlift_assets(self) -> None:
|
||||||
for control_point in self.game.theater.controlpoints:
|
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(self, control_point: ControlPoint) -> int:
|
||||||
def desired_airlift_capacity(control_point: ControlPoint) -> int:
|
|
||||||
return 4 if control_point.has_factory else 0
|
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:
|
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
|
||||||
inventory = self.game.aircraft_inventory.for_control_point(control_point)
|
inventory = self.game.aircraft_inventory.for_control_point(control_point)
|
||||||
@@ -611,9 +733,16 @@ class PendingTransfers:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
|
||||||
gap = self.desired_airlift_capacity(
|
unclaimed_parking = control_point.unclaimed_parking(self.game)
|
||||||
control_point
|
# Buy a maximum of unclaimed_parking only to prevent that aircraft procurement
|
||||||
) - self.current_airlift_capacity(control_point)
|
# take place at another base
|
||||||
|
gap = min(
|
||||||
|
[
|
||||||
|
self.desired_airlift_capacity(control_point)
|
||||||
|
- self.current_airlift_capacity(control_point),
|
||||||
|
unclaimed_parking,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
if gap <= 0:
|
if gap <= 0:
|
||||||
return
|
return
|
||||||
@@ -623,6 +752,10 @@ class PendingTransfers:
|
|||||||
# aesthetic.
|
# aesthetic.
|
||||||
gap += 1
|
gap += 1
|
||||||
|
|
||||||
|
if gap > unclaimed_parking:
|
||||||
|
# Prevent to buy more aircraft than possible
|
||||||
|
return
|
||||||
|
|
||||||
self.game.procurement_requests_for(player=control_point.captured).append(
|
self.game.procurement_requests_for(player=control_point.captured).append(
|
||||||
AircraftProcurementRequest(
|
AircraftProcurementRequest(
|
||||||
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
|
control_point, nautical_miles(200), FlightType.TRANSPORT, gap
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
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 game.theater import ControlPoint
|
||||||
from .dcs.groundunittype import GroundUnitType
|
from .dcs.groundunittype import GroundUnitType
|
||||||
@@ -28,37 +28,47 @@ class PendingUnitDeliveries:
|
|||||||
self.destination = destination
|
self.destination = destination
|
||||||
|
|
||||||
# Maps unit type to order quantity.
|
# 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:
|
def __str__(self) -> str:
|
||||||
return f"Pending delivery to {self.destination}"
|
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():
|
for k, v in units.items():
|
||||||
self.units[k] += v
|
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():
|
for k, v in units.items():
|
||||||
self.units[k] -= v
|
self.units[k] -= v
|
||||||
|
if self.units[k] == 0:
|
||||||
|
del self.units[k]
|
||||||
|
|
||||||
def refund_all(self, game: Game) -> None:
|
def refund_all(self, game: Game) -> None:
|
||||||
self.refund(game, self.units)
|
self.refund(game, self.units)
|
||||||
self.units = defaultdict(int)
|
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():
|
for unit_type, count in units.items():
|
||||||
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
|
||||||
game.adjust_budget(
|
game.adjust_budget(
|
||||||
unit_type.price * count, player=self.destination.captured
|
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)
|
pending_units = self.units.get(unit_type)
|
||||||
if pending_units is None:
|
if pending_units is None:
|
||||||
pending_units = 0
|
pending_units = 0
|
||||||
return pending_units
|
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)
|
current_units = self.destination.base.total_units_of_type(unit_type)
|
||||||
return self.pending_orders(unit_type) + current_units
|
return self.pending_orders(unit_type) + current_units
|
||||||
|
|
||||||
@@ -69,12 +79,11 @@ class PendingUnitDeliveries:
|
|||||||
f"{self.destination.name} lost its source for ground unit "
|
f"{self.destination.name} lost its source for ground unit "
|
||||||
"reinforcements. Refunding purchase price."
|
"reinforcements. Refunding purchase price."
|
||||||
)
|
)
|
||||||
self.refund_all(game)
|
self.refund_ground_units(game)
|
||||||
return
|
|
||||||
|
|
||||||
bought_units: Dict[UnitType, int] = {}
|
bought_units: dict[UnitType[Any], int] = {}
|
||||||
units_needing_transfer: Dict[GroundUnitType, int] = {}
|
units_needing_transfer: dict[GroundUnitType, int] = {}
|
||||||
sold_units: Dict[UnitType, int] = {}
|
sold_units: dict[UnitType[Any], int] = {}
|
||||||
for unit_type, count in self.units.items():
|
for unit_type, count in self.units.items():
|
||||||
coalition = "Ally" if self.destination.captured else "Enemy"
|
coalition = "Ally" if self.destination.captured else "Enemy"
|
||||||
d: dict[Any, int]
|
d: dict[Any, int]
|
||||||
@@ -102,11 +111,16 @@ class PendingUnitDeliveries:
|
|||||||
self.destination.base.commit_losses(sold_units)
|
self.destination.base.commit_losses(sold_units)
|
||||||
|
|
||||||
if units_needing_transfer:
|
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)
|
ground_unit_source.base.commission_units(units_needing_transfer)
|
||||||
self.create_transfer(game, ground_unit_source, units_needing_transfer)
|
self.create_transfer(game, ground_unit_source, units_needing_transfer)
|
||||||
|
|
||||||
def create_transfer(
|
def create_transfer(
|
||||||
self, game: Game, source: ControlPoint, units: Dict[GroundUnitType, int]
|
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int]
|
||||||
) -> None:
|
) -> None:
|
||||||
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import math
|
import math
|
||||||
from dataclasses import dataclass
|
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.unit import Vehicle, Ship
|
||||||
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
|
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup
|
||||||
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.squadrons import Pilot
|
from game.squadrons import Pilot
|
||||||
@@ -27,11 +27,14 @@ class FrontLineUnit:
|
|||||||
origin: ControlPoint
|
origin: ControlPoint
|
||||||
|
|
||||||
|
|
||||||
|
UnitT = TypeVar("UnitT", Ship, Vehicle)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GroundObjectUnit:
|
class GroundObjectUnit(Generic[UnitT]):
|
||||||
ground_object: TheaterGroundObject
|
ground_object: TheaterGroundObject[Any]
|
||||||
group: Group
|
group: MovingGroup[UnitT]
|
||||||
unit: Unit
|
unit: UnitT
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -56,13 +59,13 @@ class UnitMap:
|
|||||||
self.aircraft: Dict[str, FlyingUnit] = {}
|
self.aircraft: Dict[str, FlyingUnit] = {}
|
||||||
self.airfields: Dict[str, Airfield] = {}
|
self.airfields: Dict[str, Airfield] = {}
|
||||||
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
self.front_line_units: Dict[str, FrontLineUnit] = {}
|
||||||
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
|
self.ground_object_units: Dict[str, GroundObjectUnit[Any]] = {}
|
||||||
self.buildings: Dict[str, Building] = {}
|
self.buildings: Dict[str, Building] = {}
|
||||||
self.convoys: Dict[str, ConvoyUnit] = {}
|
self.convoys: Dict[str, ConvoyUnit] = {}
|
||||||
self.cargo_ships: Dict[str, CargoShip] = {}
|
self.cargo_ships: Dict[str, CargoShip] = {}
|
||||||
self.airlifts: Dict[str, AirliftUnits] = {}
|
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):
|
for pilot, unit in zip(flight.roster.pilots, group.units):
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
@@ -85,7 +88,7 @@ class UnitMap:
|
|||||||
return self.airfields.get(name, None)
|
return self.airfields.get(name, None)
|
||||||
|
|
||||||
def add_front_line_units(
|
def add_front_line_units(
|
||||||
self, group: Group, origin: ControlPoint, unit_type: GroundUnitType
|
self, group: VehicleGroup, origin: ControlPoint, unit_type: GroundUnitType
|
||||||
) -> None:
|
) -> None:
|
||||||
for unit in group.units:
|
for unit in group.units:
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
@@ -100,9 +103,9 @@ class UnitMap:
|
|||||||
|
|
||||||
def add_ground_object_units(
|
def add_ground_object_units(
|
||||||
self,
|
self,
|
||||||
ground_object: TheaterGroundObject,
|
ground_object: TheaterGroundObject[Any],
|
||||||
persistence_group: Group,
|
persistence_group: Union[ShipGroup, VehicleGroup],
|
||||||
miz_group: Group,
|
miz_group: Union[ShipGroup, VehicleGroup],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Adds a group associated with a TGO to the unit map.
|
"""Adds a group associated with a TGO to the unit map.
|
||||||
|
|
||||||
@@ -131,10 +134,10 @@ class UnitMap:
|
|||||||
ground_object, persistence_group, persistent_unit
|
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)
|
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()):
|
for unit, unit_type in zip(group.units, convoy.iter_units()):
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
@@ -146,7 +149,7 @@ class UnitMap:
|
|||||||
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
|
||||||
return self.convoys.get(name, None)
|
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:
|
if len(group.units) > 1:
|
||||||
# Cargo ship "groups" are single units. Killing the one ship kills the whole
|
# 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
|
# 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]:
|
def cargo_ship(self, name: str) -> Optional[CargoShip]:
|
||||||
return self.cargo_ships.get(name, None)
|
return self.cargo_ships.get(name, None)
|
||||||
|
|
||||||
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
|
def add_airlift_units(
|
||||||
|
self, group: FlyingGroup[Any], transfer: TransferOrder
|
||||||
|
) -> None:
|
||||||
capacity_each = math.ceil(transfer.size / len(group.units))
|
capacity_each = math.ceil(transfer.size / len(group.units))
|
||||||
for idx, transport in enumerate(group.units):
|
for idx, transport in enumerate(group.units):
|
||||||
# Slice the units in groups based on the capacity of each unit. Cargo is
|
# 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]:
|
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
|
||||||
return self.airlifts.get(name, None)
|
return self.airlifts.get(name, None)
|
||||||
|
|
||||||
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
|
def add_building(
|
||||||
|
self, ground_object: BuildingGroundObject, group: StaticGroup
|
||||||
|
) -> None:
|
||||||
# The actual name is a String (the pydcs translatable string), which
|
# The actual name is a String (the pydcs translatable string), which
|
||||||
# doesn't define __eq__.
|
# doesn't define __eq__.
|
||||||
# The name of the initiator in the DCS dead event will have " object"
|
# The name of the initiator in the DCS dead event will have " object"
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import math
|
import math
|
||||||
|
from collections import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Union
|
from typing import Union, Any
|
||||||
|
|
||||||
METERS_TO_FEET = 3.28084
|
METERS_TO_FEET = 3.28084
|
||||||
FEET_TO_METERS = 1 / METERS_TO_FEET
|
FEET_TO_METERS = 1 / METERS_TO_FEET
|
||||||
@@ -16,17 +17,12 @@ MS_TO_KPH = 3.6
|
|||||||
KPH_TO_MS = 1 / MS_TO_KPH
|
KPH_TO_MS = 1 / MS_TO_KPH
|
||||||
|
|
||||||
|
|
||||||
def heading_sum(h, a) -> int:
|
def heading_sum(h: int, a: int) -> int:
|
||||||
h += a
|
h += a
|
||||||
if h > 360:
|
return h % 360
|
||||||
return h - 360
|
|
||||||
elif h < 0:
|
|
||||||
return 360 + h
|
|
||||||
else:
|
|
||||||
return h
|
|
||||||
|
|
||||||
|
|
||||||
def opposite_heading(h):
|
def opposite_heading(h: int) -> int:
|
||||||
return heading_sum(h, 180)
|
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)
|
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
|
||||||
|
|
||||||
|
|
||||||
def pairwise(iterable):
|
def pairwise(iterable: Iterable[Any]) -> Iterable[tuple[Any, Any]]:
|
||||||
"""
|
"""
|
||||||
itertools recipe
|
itertools recipe
|
||||||
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
s -> (s0,s1), (s1,s2), (s2, s3), ...
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
MAJOR_VERSION = 4
|
||||||
|
MINOR_VERSION = 1
|
||||||
|
MICRO_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
def _build_version_string() -> str:
|
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")
|
build_number_path = Path("resources/buildnumber")
|
||||||
if build_number_path.exists():
|
if build_number_path.exists():
|
||||||
with build_number_path.open("r") as build_number_file:
|
with build_number_path.open("r", encoding="utf-8") as build_number_file:
|
||||||
components.append(build_number_file.readline())
|
components.append(build_number_file.readline())
|
||||||
|
|
||||||
if not Path("resources/final").exists():
|
if not Path("resources/final").exists():
|
||||||
@@ -90,4 +97,17 @@ VERSION = _build_version_string()
|
|||||||
#:
|
#:
|
||||||
#: Version 6.1
|
#: Version 6.1
|
||||||
#: * Support for new Syrian airfields in DCS 2.7.2.7910.1 (Cyprus update).
|
#: * 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
|
raise NotImplementedError
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def random_wind(minimum: int, maximum) -> WindConditions:
|
def random_wind(minimum: int, maximum: int) -> WindConditions:
|
||||||
wind_direction = random.randint(0, 360)
|
wind_direction = random.randint(0, 360)
|
||||||
at_0m_factor = 1
|
at_0m_factor = 1
|
||||||
at_2000m_factor = 2
|
at_2000m_factor = 2
|
||||||
|
|||||||
169
gen/aircraft.py
169
gen/aircraft.py
@@ -1,11 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from game.savecompat import has_save_compat_for
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import cached_property
|
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 import helicopters
|
||||||
from dcs.action import AITaskPush, ActivateGroup
|
from dcs.action import AITaskPush, ActivateGroup
|
||||||
@@ -26,7 +27,6 @@ from dcs.planes import (
|
|||||||
Su_33,
|
Su_33,
|
||||||
Tu_22M3,
|
Tu_22M3,
|
||||||
)
|
)
|
||||||
from dcs.planes import IL_78M
|
|
||||||
from dcs.point import MovingPoint, PointAction
|
from dcs.point import MovingPoint, PointAction
|
||||||
from dcs.task import (
|
from dcs.task import (
|
||||||
AWACS,
|
AWACS,
|
||||||
@@ -66,7 +66,6 @@ from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
|
|||||||
from dcs.unittype import FlyingType
|
from dcs.unittype import FlyingType
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from game.data.cap_capabilities_db import GUNFIGHTERS
|
|
||||||
from game.data.weapons import Pylon
|
from game.data.weapons import Pylon
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
@@ -83,7 +82,7 @@ from game.theater.missiontarget import MissionTarget
|
|||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
from game.transfers import MultiGroupTransport
|
from game.transfers import MultiGroupTransport
|
||||||
from game.unitmap import UnitMap
|
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.ato import AirTaskingOrder, Package
|
||||||
from gen.callsigns import create_group_callsign_from_unit
|
from gen.callsigns import create_group_callsign_from_unit
|
||||||
from gen.flights.flight import (
|
from gen.flights.flight import (
|
||||||
@@ -323,7 +322,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def livery_from_db(flight: Flight) -> Optional[str]:
|
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]:
|
def livery_from_faction(self, flight: Flight) -> Optional[str]:
|
||||||
faction = self.game.faction_for(player=flight.departure.captured)
|
faction = self.game.faction_for(player=flight.departure.captured)
|
||||||
@@ -344,7 +343,7 @@ class AircraftConflictGenerator:
|
|||||||
return livery
|
return livery
|
||||||
return None
|
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)
|
livery = self.livery_for(flight)
|
||||||
if livery is None:
|
if livery is None:
|
||||||
return
|
return
|
||||||
@@ -353,7 +352,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def _setup_group(
|
def _setup_group(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -385,7 +384,18 @@ class AircraftConflictGenerator:
|
|||||||
channel = self.radio_registry.alloc_uhf()
|
channel = self.radio_registry.alloc_uhf()
|
||||||
else:
|
else:
|
||||||
channel = flight.unit_type.alloc_flight_radio(self.radio_registry)
|
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
|
divert = None
|
||||||
if flight.divert is not None:
|
if flight.divert is not None:
|
||||||
@@ -460,8 +470,8 @@ class AircraftConflictGenerator:
|
|||||||
unit_type: Type[FlyingType],
|
unit_type: Type[FlyingType],
|
||||||
count: int,
|
count: int,
|
||||||
start_type: str,
|
start_type: str,
|
||||||
airport: Optional[Airport] = None,
|
airport: Airport,
|
||||||
) -> FlyingGroup:
|
) -> FlyingGroup[Any]:
|
||||||
assert count > 0
|
assert count > 0
|
||||||
|
|
||||||
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
|
||||||
@@ -478,7 +488,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def _generate_inflight(
|
def _generate_inflight(
|
||||||
self, name: str, side: Country, flight: Flight, origin: ControlPoint
|
self, name: str, side: Country, flight: Flight, origin: ControlPoint
|
||||||
) -> FlyingGroup:
|
) -> FlyingGroup[Any]:
|
||||||
assert flight.count > 0
|
assert flight.count > 0
|
||||||
at = origin.position
|
at = origin.position
|
||||||
|
|
||||||
@@ -523,7 +533,7 @@ class AircraftConflictGenerator:
|
|||||||
count: int,
|
count: int,
|
||||||
start_type: str,
|
start_type: str,
|
||||||
at: Union[ShipGroup, StaticGroup],
|
at: Union[ShipGroup, StaticGroup],
|
||||||
) -> FlyingGroup:
|
) -> FlyingGroup[Any]:
|
||||||
assert count > 0
|
assert count > 0
|
||||||
|
|
||||||
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
|
||||||
@@ -538,34 +548,18 @@ class AircraftConflictGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _add_radio_waypoint(
|
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:
|
) -> MovingPoint:
|
||||||
point = group.add_waypoint(position, altitude.meters, airspeed)
|
point = group.add_waypoint(position, altitude.meters, airspeed)
|
||||||
point.alt_type = "RADIO"
|
point.alt_type = "RADIO"
|
||||||
return point
|
return point
|
||||||
|
|
||||||
def _rtb_for(
|
@staticmethod
|
||||||
self,
|
def _at_position(at: Union[Point, ShipGroup, Type[Airport]]) -> Point:
|
||||||
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:
|
|
||||||
if isinstance(at, Point):
|
if isinstance(at, Point):
|
||||||
return at
|
return at
|
||||||
elif isinstance(at, ShipGroup):
|
elif isinstance(at, ShipGroup):
|
||||||
@@ -575,7 +569,7 @@ class AircraftConflictGenerator:
|
|||||||
else:
|
else:
|
||||||
assert False
|
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:
|
for p in group.units:
|
||||||
p.pylons.clear()
|
p.pylons.clear()
|
||||||
|
|
||||||
@@ -595,7 +589,10 @@ class AircraftConflictGenerator:
|
|||||||
parking_slot.unit_id = None
|
parking_slot.unit_id = None
|
||||||
|
|
||||||
def generate_flights(
|
def generate_flights(
|
||||||
self, country, ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData]
|
self,
|
||||||
|
country: Country,
|
||||||
|
ato: AirTaskingOrder,
|
||||||
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
for package in ato.packages:
|
for package in ato.packages:
|
||||||
@@ -674,7 +671,7 @@ class AircraftConflictGenerator:
|
|||||||
self.unit_map.add_aircraft(group, flight)
|
self.unit_map.add_aircraft(group, flight)
|
||||||
|
|
||||||
def set_activation_time(
|
def set_activation_time(
|
||||||
self, flight: Flight, group: FlyingGroup, delay: timedelta
|
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
|
||||||
) -> None:
|
) -> None:
|
||||||
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
# Note: Late activation causes the waypoint TOTs to look *weird* in the
|
||||||
# mission editor. Waypoint times will be relative to the group
|
# mission editor. Waypoint times will be relative to the group
|
||||||
@@ -693,7 +690,7 @@ class AircraftConflictGenerator:
|
|||||||
self.m.triggerrules.triggers.append(activation_trigger)
|
self.m.triggerrules.triggers.append(activation_trigger)
|
||||||
|
|
||||||
def set_startup_time(
|
def set_startup_time(
|
||||||
self, flight: Flight, group: FlyingGroup, delay: timedelta
|
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
|
||||||
) -> None:
|
) -> None:
|
||||||
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
# Uncontrolled causes the AI unit to spawn, but not begin startup.
|
||||||
group.uncontrolled = True
|
group.uncontrolled = True
|
||||||
@@ -721,7 +718,9 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id))
|
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)
|
name = namegen.next_aircraft_name(country, cp.id, flight)
|
||||||
try:
|
try:
|
||||||
if flight.start_type == "In Flight":
|
if flight.start_type == "In Flight":
|
||||||
@@ -730,13 +729,19 @@ class AircraftConflictGenerator:
|
|||||||
)
|
)
|
||||||
elif isinstance(cp, NavalControlPoint):
|
elif isinstance(cp, NavalControlPoint):
|
||||||
group_name = cp.get_carrier_group_name()
|
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(
|
group = self._generate_at_group(
|
||||||
name=name,
|
name=name,
|
||||||
side=country,
|
side=country,
|
||||||
unit_type=flight.unit_type.dcs_unit_type,
|
unit_type=flight.unit_type.dcs_unit_type,
|
||||||
count=flight.count,
|
count=flight.count,
|
||||||
start_type=flight.start_type,
|
start_type=flight.start_type,
|
||||||
at=self.m.find_group(group_name),
|
at=carrier_group,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if not isinstance(cp, Airfield):
|
if not isinstance(cp, Airfield):
|
||||||
@@ -767,7 +772,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_reduced_fuel(
|
def set_reduced_fuel(
|
||||||
flight: Flight, group: FlyingGroup, unit_type: Type[PlaneType]
|
flight: Flight, group: FlyingGroup[Any], unit_type: Type[FlyingType]
|
||||||
) -> None:
|
) -> None:
|
||||||
if unit_type is Su_33:
|
if unit_type is Su_33:
|
||||||
for unit in group.units:
|
for unit in group.units:
|
||||||
@@ -793,9 +798,9 @@ class AircraftConflictGenerator:
|
|||||||
def configure_behavior(
|
def configure_behavior(
|
||||||
self,
|
self,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
react_on_threat: Optional[OptReactOnThreat.Values] = None,
|
||||||
roe: Optional[OptROE.Values] = None,
|
roe: Optional[int] = None,
|
||||||
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
|
||||||
restrict_jettison: Optional[bool] = None,
|
restrict_jettison: Optional[bool] = None,
|
||||||
mission_uses_gun: bool = True,
|
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
|
# 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
|
@staticmethod
|
||||||
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
|
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
|
||||||
if flight.unit_type.eplrs_capable:
|
if flight.unit_type.eplrs_capable:
|
||||||
group.points[0].tasks.append(EPLRS(group.id))
|
group.points[0].tasks.append(EPLRS(group.id))
|
||||||
|
|
||||||
def configure_cap(
|
def configure_cap(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -840,7 +845,7 @@ class AircraftConflictGenerator:
|
|||||||
group.task = CAP.name
|
group.task = CAP.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
self._setup_group(group, package, flight, dynamic_runways)
|
||||||
|
|
||||||
if flight.unit_type not in GUNFIGHTERS:
|
if not flight.unit_type.gunfighter:
|
||||||
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||||
else:
|
else:
|
||||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||||
@@ -849,7 +854,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_sweep(
|
def configure_sweep(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -857,7 +862,7 @@ class AircraftConflictGenerator:
|
|||||||
group.task = FighterSweep.name
|
group.task = FighterSweep.name
|
||||||
self._setup_group(group, package, flight, dynamic_runways)
|
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
|
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
|
||||||
else:
|
else:
|
||||||
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
|
||||||
@@ -866,7 +871,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_cas(
|
def configure_cas(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -884,7 +889,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_dead(
|
def configure_dead(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -909,7 +914,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_sead(
|
def configure_sead(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -933,7 +938,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_strike(
|
def configure_strike(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -951,7 +956,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_anti_ship(
|
def configure_anti_ship(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -969,7 +974,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_runway_attack(
|
def configure_runway_attack(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -987,7 +992,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_oca_strike(
|
def configure_oca_strike(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -1004,7 +1009,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_awacs(
|
def configure_awacs(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -1032,7 +1037,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_refueling(
|
def configure_refueling(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -1058,7 +1063,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_escort(
|
def configure_escort(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -1074,7 +1079,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_sead_escort(
|
def configure_sead_escort(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -1097,7 +1102,7 @@ class AircraftConflictGenerator:
|
|||||||
|
|
||||||
def configure_transport(
|
def configure_transport(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -1112,13 +1117,13 @@ class AircraftConflictGenerator:
|
|||||||
restrict_jettison=True,
|
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}")
|
logging.error(f"Unhandled flight type: {flight.flight_type}")
|
||||||
self.configure_behavior(flight, group)
|
self.configure_behavior(flight, group)
|
||||||
|
|
||||||
def setup_flight_group(
|
def setup_flight_group(
|
||||||
self,
|
self,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
dynamic_runways: Dict[str, RunwayData],
|
dynamic_runways: Dict[str, RunwayData],
|
||||||
@@ -1162,7 +1167,7 @@ class AircraftConflictGenerator:
|
|||||||
self.configure_eplrs(group, flight)
|
self.configure_eplrs(group, flight)
|
||||||
|
|
||||||
def create_waypoints(
|
def create_waypoints(
|
||||||
self, group: FlyingGroup, package: Package, flight: Flight
|
self, group: FlyingGroup[Any], package: Package, flight: Flight
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
for waypoint in flight.points:
|
for waypoint in flight.points:
|
||||||
@@ -1182,7 +1187,7 @@ class AircraftConflictGenerator:
|
|||||||
# under the current flight plans.
|
# under the current flight plans.
|
||||||
# TODO: Make this smarter, it currently selects a random unit in the group for target,
|
# 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.
|
# 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 = [
|
viggen_target_points = [
|
||||||
(idx, point)
|
(idx, point)
|
||||||
for idx, point in enumerate(filtered_points)
|
for idx, point in enumerate(filtered_points)
|
||||||
@@ -1230,7 +1235,7 @@ class AircraftConflictGenerator:
|
|||||||
waypoint: FlightWaypoint,
|
waypoint: FlightWaypoint,
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
estimator = TotEstimator(package)
|
estimator = TotEstimator(package)
|
||||||
start_time = estimator.mission_start_time(flight)
|
start_time = estimator.mission_start_time(flight)
|
||||||
@@ -1273,7 +1278,7 @@ class PydcsWaypointBuilder:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
waypoint: FlightWaypoint,
|
waypoint: FlightWaypoint,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
@@ -1316,7 +1321,7 @@ class PydcsWaypointBuilder:
|
|||||||
def for_waypoint(
|
def for_waypoint(
|
||||||
cls,
|
cls,
|
||||||
waypoint: FlightWaypoint,
|
waypoint: FlightWaypoint,
|
||||||
group: FlyingGroup,
|
group: FlyingGroup[Any],
|
||||||
package: Package,
|
package: Package,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
@@ -1346,9 +1351,10 @@ class PydcsWaypointBuilder:
|
|||||||
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
|
"""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 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 (
|
if (
|
||||||
self.waypoint.waypoint_type not in TARGET_WAYPOINTS
|
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
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
@@ -1429,7 +1435,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
|
|||||||
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
if isinstance(self.flight.flight_plan, CasFlightPlan):
|
||||||
waypoint.add_task(
|
waypoint.add_task(
|
||||||
EngageTargetsInZone(
|
EngageTargetsInZone(
|
||||||
position=self.flight.flight_plan.target,
|
position=self.flight.flight_plan.target.position,
|
||||||
radius=int(self.flight.flight_plan.engagement_distance.meters),
|
radius=int(self.flight.flight_plan.engagement_distance.meters),
|
||||||
targets=[
|
targets=[
|
||||||
Targets.All.GroundUnits.GroundVehicles,
|
Targets.All.GroundUnits.GroundVehicles,
|
||||||
@@ -1705,6 +1711,7 @@ class CargoStopBuilder(PydcsWaypointBuilder):
|
|||||||
|
|
||||||
|
|
||||||
class RaceTrackBuilder(PydcsWaypointBuilder):
|
class RaceTrackBuilder(PydcsWaypointBuilder):
|
||||||
|
@has_save_compat_for(4)
|
||||||
def build(self) -> MovingPoint:
|
def build(self) -> MovingPoint:
|
||||||
waypoint = super().build()
|
waypoint = super().build()
|
||||||
|
|
||||||
@@ -1739,17 +1746,11 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Set orbit speeds for all race tracks and remove this special case.
|
orbit = OrbitAction(
|
||||||
if isinstance(flight_plan, RefuelingFlightPlan):
|
altitude=waypoint.alt,
|
||||||
orbit = OrbitAction(
|
pattern=OrbitAction.OrbitPattern.RaceTrack,
|
||||||
altitude=waypoint.alt,
|
speed=int(getattr(flight_plan, "patrol_speed", kph(600)).kph),
|
||||||
pattern=OrbitAction.OrbitPattern.RaceTrack,
|
)
|
||||||
speed=int(flight_plan.patrol_speed.kph),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
orbit = OrbitAction(
|
|
||||||
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
|
|
||||||
)
|
|
||||||
|
|
||||||
racetrack = ControlledTask(orbit)
|
racetrack = ControlledTask(orbit)
|
||||||
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
|
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:
|
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
|
||||||
waypoint.add_task(Tanker())
|
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]
|
tanker_info = self.air_support.tankers[-1]
|
||||||
tacan = tanker_info.tacan
|
tacan = tanker_info.tacan
|
||||||
tacan_callsign = {
|
tacan_callsign = {
|
||||||
|
|||||||
@@ -1521,4 +1521,47 @@ AIRFIELD_DATA = {
|
|||||||
runway_length=3953,
|
runway_length=3953,
|
||||||
atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
|
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
|
import logging
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import timedelta
|
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.mission import Mission, StartType
|
||||||
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
|
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType
|
||||||
from dcs.unittype import UnitType
|
|
||||||
from dcs.task import (
|
from dcs.task import (
|
||||||
AWACS,
|
AWACS,
|
||||||
ActivateBeaconCommand,
|
ActivateBeaconCommand,
|
||||||
@@ -14,15 +15,17 @@ from dcs.task import (
|
|||||||
SetImmortalCommand,
|
SetImmortalCommand,
|
||||||
SetInvisibleCommand,
|
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 .callsigns import callsign_for_support_unit
|
||||||
from .conflictgen import Conflict
|
from .conflictgen import Conflict
|
||||||
|
from .flights.ai_flight_planner_db import AEWC_CAPABLE
|
||||||
|
from .naming import namegen
|
||||||
from .radios import RadioFrequency, RadioRegistry
|
from .radios import RadioFrequency, RadioRegistry
|
||||||
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
from .tacan import TacanBand, TacanChannel, TacanRegistry
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
|
||||||
TANKER_DISTANCE = 15000
|
TANKER_DISTANCE = 15000
|
||||||
TANKER_ALT = 4572
|
TANKER_ALT = 4572
|
||||||
@@ -70,7 +73,7 @@ class AirSupportConflictGenerator:
|
|||||||
self,
|
self,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
conflict: Conflict,
|
conflict: Conflict,
|
||||||
game,
|
game: Game,
|
||||||
radio_registry: RadioRegistry,
|
radio_registry: RadioRegistry,
|
||||||
tacan_registry: TacanRegistry,
|
tacan_registry: TacanRegistry,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -95,7 +98,7 @@ class AirSupportConflictGenerator:
|
|||||||
return (TANKER_ALT + 500, 596)
|
return (TANKER_ALT + 500, 596)
|
||||||
return (TANKER_ALT, 574)
|
return (TANKER_ALT, 574)
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
player_cp = (
|
player_cp = (
|
||||||
self.conflict.blue_cp
|
self.conflict.blue_cp
|
||||||
if self.conflict.blue_cp.captured
|
if self.conflict.blue_cp.captured
|
||||||
@@ -108,6 +111,11 @@ class AirSupportConflictGenerator:
|
|||||||
for i, tanker_unit_type in enumerate(
|
for i, tanker_unit_type in enumerate(
|
||||||
self.game.faction_for(player=True).tankers
|
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.
|
# TODO: Make loiter altitude a property of the unit type.
|
||||||
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
|
||||||
freq = self.radio_registry.alloc_uhf()
|
freq = self.radio_registry.alloc_uhf()
|
||||||
@@ -127,7 +135,7 @@ class AirSupportConflictGenerator:
|
|||||||
self.mission.country(self.game.player_country), tanker_unit_type
|
self.mission.country(self.game.player_country), tanker_unit_type
|
||||||
),
|
),
|
||||||
airport=None,
|
airport=None,
|
||||||
plane_type=tanker_unit_type,
|
plane_type=unit_type,
|
||||||
position=tanker_position,
|
position=tanker_position,
|
||||||
altitude=alt,
|
altitude=alt,
|
||||||
race_distance=58000,
|
race_distance=58000,
|
||||||
@@ -177,6 +185,8 @@ class AirSupportConflictGenerator:
|
|||||||
tanker_unit_type.name,
|
tanker_unit_type.name,
|
||||||
freq,
|
freq,
|
||||||
tacan,
|
tacan,
|
||||||
|
start_time=None,
|
||||||
|
end_time=None,
|
||||||
blue=True,
|
blue=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -195,12 +205,17 @@ class AirSupportConflictGenerator:
|
|||||||
awacs_unit = possible_awacs[0]
|
awacs_unit = possible_awacs[0]
|
||||||
freq = self.radio_registry.alloc_uhf()
|
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(
|
awacs_flight = self.mission.awacs_flight(
|
||||||
country=self.mission.country(self.game.player_country),
|
country=self.mission.country(self.game.player_country),
|
||||||
name=namegen.next_awacs_name(
|
name=namegen.next_awacs_name(
|
||||||
self.mission.country(self.game.player_country)
|
self.mission.country(self.game.player_country)
|
||||||
),
|
),
|
||||||
plane_type=awacs_unit,
|
plane_type=unit_type,
|
||||||
altitude=AWACS_ALT,
|
altitude=AWACS_ALT,
|
||||||
airport=None,
|
airport=None,
|
||||||
position=self.conflict.position.random_point_within(
|
position=self.conflict.position.random_point_within(
|
||||||
|
|||||||
48
gen/armor.py
48
gen/armor.py
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||||
@@ -23,7 +24,7 @@ from dcs.task import (
|
|||||||
SetInvisibleCommand,
|
SetInvisibleCommand,
|
||||||
)
|
)
|
||||||
from dcs.triggers import Event, TriggerOnce
|
from dcs.triggers import Event, TriggerOnce
|
||||||
from dcs.unit import Vehicle
|
from dcs.unit import Vehicle, Skill
|
||||||
from dcs.unitgroup import VehicleGroup
|
from dcs.unitgroup import VehicleGroup
|
||||||
|
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.groundunitclass import GroundUnitClass
|
||||||
@@ -97,7 +98,7 @@ class GroundConflictGenerator:
|
|||||||
self.unit_map = unit_map
|
self.unit_map = unit_map
|
||||||
self.jtacs: List[JtacInfo] = []
|
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"""
|
"""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(
|
if len(self.enemy_planned_combat_groups) > len(
|
||||||
self.player_planned_combat_groups
|
self.player_planned_combat_groups
|
||||||
@@ -122,20 +123,11 @@ class GroundConflictGenerator:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
def generate(self) -> None:
|
||||||
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):
|
|
||||||
position = Conflict.frontline_position(
|
position = Conflict.frontline_position(
|
||||||
self.conflict.front_line, self.game.theater
|
self.conflict.front_line, self.game.theater
|
||||||
)
|
)
|
||||||
|
|
||||||
frontline_vector = Conflict.frontline_vector(
|
frontline_vector = Conflict.frontline_vector(
|
||||||
self.conflict.front_line, self.game.theater
|
self.conflict.front_line, self.game.theater
|
||||||
)
|
)
|
||||||
@@ -150,6 +142,13 @@ class GroundConflictGenerator:
|
|||||||
self.enemy_planned_combat_groups, frontline_vector, False
|
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
|
# Plan combat actions for groups
|
||||||
self.plan_action_for_groups(
|
self.plan_action_for_groups(
|
||||||
self.player_stance,
|
self.player_stance,
|
||||||
@@ -174,7 +173,7 @@ class GroundConflictGenerator:
|
|||||||
code = 1688 - len(self.jtacs)
|
code = 1688 - len(self.jtacs)
|
||||||
|
|
||||||
utype = self.game.player_faction.jtac_unit
|
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")
|
utype = AircraftType.named("MQ-9 Reaper")
|
||||||
|
|
||||||
jtac = self.mission.flight_group(
|
jtac = self.mission.flight_group(
|
||||||
@@ -361,7 +360,6 @@ class GroundConflictGenerator:
|
|||||||
self.mission.triggerrules.triggers.append(artillery_fallback)
|
self.mission.triggerrules.triggers.append(artillery_fallback)
|
||||||
|
|
||||||
for u in dcs_group.units:
|
for u in dcs_group.units:
|
||||||
u.initial = True
|
|
||||||
u.heading = forward_heading + random.randint(-5, 5)
|
u.heading = forward_heading + random.randint(-5, 5)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -570,10 +568,10 @@ class GroundConflictGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Fallback task
|
# Fallback task
|
||||||
fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
|
task = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
|
||||||
fallback.enabled = False
|
task.enabled = False
|
||||||
dcs_group.add_trigger_action(Hold())
|
dcs_group.add_trigger_action(Hold())
|
||||||
dcs_group.add_trigger_action(fallback)
|
dcs_group.add_trigger_action(task)
|
||||||
|
|
||||||
# Create trigger
|
# Create trigger
|
||||||
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
|
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
|
||||||
@@ -634,7 +632,7 @@ class GroundConflictGenerator:
|
|||||||
@param enemy_groups Potential enemy groups
|
@param enemy_groups Potential enemy groups
|
||||||
@param n number of nearby groups to take
|
@param n number of nearby groups to take
|
||||||
"""
|
"""
|
||||||
targets = [] # type: List[Optional[VehicleGroup]]
|
targets = [] # type: List[VehicleGroup]
|
||||||
sorted_list = sorted(
|
sorted_list = sorted(
|
||||||
enemy_groups,
|
enemy_groups,
|
||||||
key=lambda group: player_group.points[0].position.distance_to_point(
|
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 group Group for which we should find the nearest ennemy
|
||||||
@param enemy_groups Potential enemy groups
|
@param enemy_groups Potential enemy groups
|
||||||
"""
|
"""
|
||||||
min_distance = 99999999
|
min_distance = math.inf
|
||||||
target = None
|
target = None
|
||||||
for dcs_group, _ in enemy_groups:
|
for dcs_group, _ in enemy_groups:
|
||||||
dist = player_group.points[0].position.distance_to_point(
|
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
|
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]:
|
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
|
||||||
rg = random.randint(
|
rg = random.randint(
|
||||||
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
|
||||||
@@ -716,7 +714,7 @@ class GroundConflictGenerator:
|
|||||||
distance_from_frontline: int,
|
distance_from_frontline: int,
|
||||||
heading: int,
|
heading: int,
|
||||||
spawn_heading: int,
|
spawn_heading: int,
|
||||||
):
|
) -> Optional[Point]:
|
||||||
shifted = conflict_position.point_from_heading(
|
shifted = conflict_position.point_from_heading(
|
||||||
heading, random.randint(0, combat_width)
|
heading, random.randint(0, combat_width)
|
||||||
)
|
)
|
||||||
@@ -766,9 +764,9 @@ class GroundConflictGenerator:
|
|||||||
heading=opposite_heading(spawn_heading),
|
heading=opposite_heading(spawn_heading),
|
||||||
)
|
)
|
||||||
if is_player:
|
if is_player:
|
||||||
g.set_skill(self.game.settings.player_skill)
|
g.set_skill(Skill(self.game.settings.player_skill))
|
||||||
else:
|
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))
|
positioned_groups.append((g, group))
|
||||||
|
|
||||||
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
|
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
|
||||||
@@ -790,7 +788,7 @@ class GroundConflictGenerator:
|
|||||||
count: int,
|
count: int,
|
||||||
at: Point,
|
at: Point,
|
||||||
move_formation: PointAction = PointAction.OffRoad,
|
move_formation: PointAction = PointAction.OffRoad,
|
||||||
heading=0,
|
heading: int = 0,
|
||||||
) -> VehicleGroup:
|
) -> VehicleGroup:
|
||||||
|
|
||||||
if side == self.conflict.attackers_country:
|
if side == self.conflict.attackers_country:
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"""Support for working with DCS group callsigns."""
|
"""Support for working with DCS group callsigns."""
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from dcs.unitgroup import FlyingGroup
|
from dcs.unitgroup import FlyingGroup
|
||||||
from dcs.flyingunit import FlyingUnit
|
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.
|
# Either something like Overlord11 for Western AWACS, or else just a number.
|
||||||
# Convert to either "Overlord" or "Flight 123".
|
# Convert to either "Overlord" or "Flight 123".
|
||||||
lead = group.units[0]
|
lead = group.units[0]
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import random
|
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
|
from gen.coastal.silkworm import SilkwormGenerator
|
||||||
|
|
||||||
COASTAL_MAP = {
|
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
|
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]
|
faction = db.FACTIONS[faction_name]
|
||||||
if len(faction.coastal_defenses) > 0:
|
if len(faction.coastal_defenses) > 0:
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
|
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):
|
class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
|
||||||
def __init__(self, game, ground_object, faction):
|
def __init__(
|
||||||
|
self, game: Game, ground_object: CoastalSiteGroundObject, faction: Faction
|
||||||
|
) -> None:
|
||||||
super(SilkwormGenerator, self).__init__(game, ground_object)
|
super(SilkwormGenerator, self).__init__(game, ground_object)
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
|
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
|
||||||
|
|
||||||
@@ -23,7 +28,7 @@ class SilkwormGenerator(GroupGenerator):
|
|||||||
# Launchers
|
# Launchers
|
||||||
for i, p in enumerate(positions):
|
for i, p in enumerate(positions):
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
MissilesSS.Silkworm_SR,
|
MissilesSS.Hy_launcher,
|
||||||
"Missile#" + str(i),
|
"Missile#" + str(i),
|
||||||
p[0],
|
p[0],
|
||||||
p[1],
|
p[1],
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
@@ -54,13 +56,15 @@ class Conflict:
|
|||||||
def frontline_position(
|
def frontline_position(
|
||||||
cls, frontline: FrontLine, theater: ConflictTheater
|
cls, frontline: FrontLine, theater: ConflictTheater
|
||||||
) -> Tuple[Point, int]:
|
) -> Tuple[Point, int]:
|
||||||
attack_heading = frontline.attack_heading
|
attack_heading = int(frontline.attack_heading)
|
||||||
position = cls.find_ground_position(
|
position = cls.find_ground_position(
|
||||||
frontline.position,
|
frontline.position,
|
||||||
FRONTLINE_LENGTH,
|
FRONTLINE_LENGTH,
|
||||||
heading_sum(attack_heading, 90),
|
heading_sum(attack_heading, 90),
|
||||||
theater,
|
theater,
|
||||||
)
|
)
|
||||||
|
if position is None:
|
||||||
|
raise RuntimeError("Could not find front line position")
|
||||||
return position, opposite_heading(attack_heading)
|
return position, opposite_heading(attack_heading)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -91,7 +95,7 @@ class Conflict:
|
|||||||
defender: Country,
|
defender: Country,
|
||||||
front_line: FrontLine,
|
front_line: FrontLine,
|
||||||
theater: ConflictTheater,
|
theater: ConflictTheater,
|
||||||
):
|
) -> Conflict:
|
||||||
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp)
|
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp)
|
||||||
position, heading, distance = cls.frontline_vector(front_line, theater)
|
position, heading, distance = cls.frontline_vector(front_line, theater)
|
||||||
conflict = cls(
|
conflict = cls(
|
||||||
@@ -138,7 +142,7 @@ class Conflict:
|
|||||||
max_distance: int,
|
max_distance: int,
|
||||||
heading: int,
|
heading: int,
|
||||||
theater: ConflictTheater,
|
theater: ConflictTheater,
|
||||||
coerce=True,
|
coerce: bool = True,
|
||||||
) -> Optional[Point]:
|
) -> Optional[Point]:
|
||||||
"""
|
"""
|
||||||
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
|
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):
|
if theater.is_on_land(pos):
|
||||||
return pos
|
return pos
|
||||||
pos = initial.point_from_heading(opposite_heading(heading), distance)
|
pos = initial.point_from_heading(opposite_heading(heading), distance)
|
||||||
|
if theater.is_on_land(pos):
|
||||||
|
return pos
|
||||||
if coerce:
|
if coerce:
|
||||||
pos = theater.nearest_land_pos(initial)
|
pos = theater.nearest_land_pos(initial)
|
||||||
return pos
|
return pos
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import random
|
import random
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from dcs.unitgroup import VehicleGroup
|
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
|
This generate a group of ground units
|
||||||
:return: Generated group
|
:return: Generated group
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import random
|
|||||||
from game import Game
|
from game import Game
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
game: Game,
|
game: Game,
|
||||||
@@ -35,7 +35,7 @@ class ArmoredGroupGenerator(GroupGenerator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class FixedSizeArmorGroupGenerator(GroupGenerator):
|
class FixedSizeArmorGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
game: Game,
|
game: Game,
|
||||||
@@ -47,7 +47,7 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
|
|||||||
self.unit_type = unit_type
|
self.unit_type = unit_type
|
||||||
self.size = size
|
self.size = size
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
spacing = random.randint(20, 70)
|
spacing = random.randint(20, 70)
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class EnvironmentGenerator:
|
|||||||
def set_fog(self, fog: Optional[Fog]) -> None:
|
def set_fog(self, fog: Optional[Fog]) -> None:
|
||||||
if fog is None:
|
if fog is None:
|
||||||
return
|
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
|
self.mission.weather.fog_thickness = fog.thickness
|
||||||
|
|
||||||
def set_wind(self, wind: WindConditions) -> None:
|
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_2000 = wind.at_2000m
|
||||||
self.mission.weather.wind_at_8000 = wind.at_8000m
|
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.mission.start_time = self.conditions.start_time
|
||||||
self.set_clouds(self.conditions.weather.clouds)
|
self.set_clouds(self.conditions.weather.clouds)
|
||||||
self.set_fog(self.conditions.weather.fog)
|
self.set_fog(self.conditions.weather.fog)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG
|
|||||||
|
|
||||||
|
|
||||||
class CarrierGroupGenerator(ShipGroupGenerator):
|
class CarrierGroupGenerator(ShipGroupGenerator):
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
# Carrier Strike Group 8
|
# Carrier Strike Group 8
|
||||||
if self.faction.carrier_names[0] == "Carrier Strike Group 8":
|
if self.faction.carrier_names[0] == "Carrier Strike Group 8":
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|||||||
import random
|
import random
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
from dcs.ships import (
|
from dcs.ships import (
|
||||||
Type_052C,
|
Type_052C,
|
||||||
Type_052B,
|
Type_052B,
|
||||||
@@ -11,16 +10,16 @@ from dcs.ships import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
|
from game.theater.theatergroundobject import ShipGroundObject
|
||||||
from gen.fleet.dd_group import DDGroupGenerator
|
from gen.fleet.dd_group import DDGroupGenerator
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
|
|
||||||
|
|
||||||
class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
include_frigate = random.choice([True, True, False])
|
include_frigate = random.choice([True, True, False])
|
||||||
include_dd = random.choice([True, False])
|
include_dd = random.choice([True, False])
|
||||||
@@ -65,9 +64,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
|
|||||||
|
|
||||||
|
|
||||||
class Type54GroupGenerator(DDGroupGenerator):
|
class Type54GroupGenerator(DDGroupGenerator):
|
||||||
def __init__(
|
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
|
||||||
):
|
|
||||||
super(Type54GroupGenerator, self).__init__(
|
super(Type54GroupGenerator, self).__init__(
|
||||||
game, ground_object, faction, Type_054A
|
game, ground_object, faction, Type_054A
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Type
|
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.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:
|
if TYPE_CHECKING:
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
@@ -16,14 +17,14 @@ class DDGroupGenerator(ShipGroupGenerator):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
game: Game,
|
game: Game,
|
||||||
ground_object: TheaterGroundObject,
|
ground_object: ShipGroundObject,
|
||||||
faction: Faction,
|
faction: Faction,
|
||||||
ddtype: Type[ShipType],
|
ddtype: Type[ShipType],
|
||||||
):
|
):
|
||||||
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
|
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
|
||||||
self.ddtype = ddtype
|
self.ddtype = ddtype
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
self.ddtype,
|
self.ddtype,
|
||||||
"DD1",
|
"DD1",
|
||||||
@@ -42,18 +43,14 @@ class DDGroupGenerator(ShipGroupGenerator):
|
|||||||
|
|
||||||
|
|
||||||
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
|
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(
|
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
|
||||||
):
|
|
||||||
super(OliverHazardPerryGroupGenerator, self).__init__(
|
super(OliverHazardPerryGroupGenerator, self).__init__(
|
||||||
game, ground_object, faction, PERRY
|
game, ground_object, faction, PERRY
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ArleighBurkeGroupGenerator(DDGroupGenerator):
|
class ArleighBurkeGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(
|
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
|
||||||
):
|
|
||||||
super(ArleighBurkeGroupGenerator, self).__init__(
|
super(ArleighBurkeGroupGenerator, self).__init__(
|
||||||
game, ground_object, faction, USS_Arleigh_Burke_IIa
|
game, ground_object, faction, USS_Arleigh_Burke_IIa
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from dcs.ships import La_Combattante_II
|
from dcs.ships import La_Combattante_II
|
||||||
|
|
||||||
|
from game import Game
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from game.theater import TheaterGroundObject
|
from game.theater.theatergroundobject import ShipGroundObject
|
||||||
from gen.fleet.dd_group import DDGroupGenerator
|
from gen.fleet.dd_group import DDGroupGenerator
|
||||||
|
|
||||||
|
|
||||||
class LaCombattanteIIGroupGenerator(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__(
|
super(LaCombattanteIIGroupGenerator, self).__init__(
|
||||||
game, ground_object, faction, La_Combattante_II
|
game, ground_object, faction, La_Combattante_II
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from gen.sam.group_generator import ShipGroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class LHAGroupGenerator(ShipGroupGenerator):
|
class LHAGroupGenerator(ShipGroupGenerator):
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
# Add carrier
|
# Add carrier
|
||||||
if len(self.faction.helicopter_carrier) > 0:
|
if len(self.faction.helicopter_carrier) > 0:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -12,18 +13,17 @@ from dcs.ships import (
|
|||||||
SOM,
|
SOM,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from game.factions.faction import Faction
|
||||||
|
from game.theater.theatergroundobject import ShipGroundObject
|
||||||
from gen.fleet.dd_group import DDGroupGenerator
|
from gen.fleet.dd_group import DDGroupGenerator
|
||||||
from gen.sam.group_generator import ShipGroupGenerator
|
from gen.sam.group_generator import ShipGroupGenerator
|
||||||
from game.factions.faction import Faction
|
|
||||||
from game.theater.theatergroundobject import TheaterGroundObject
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.game import Game
|
from game.game import Game
|
||||||
|
|
||||||
|
|
||||||
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
class RussianNavyGroupGenerator(ShipGroupGenerator):
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
include_frigate = random.choice([True, True, False])
|
include_frigate = random.choice([True, True, False])
|
||||||
include_dd = random.choice([True, False])
|
include_dd = random.choice([True, False])
|
||||||
@@ -85,32 +85,24 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
|
|||||||
|
|
||||||
|
|
||||||
class GrishaGroupGenerator(DDGroupGenerator):
|
class GrishaGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(
|
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
|
||||||
):
|
|
||||||
super(GrishaGroupGenerator, self).__init__(
|
super(GrishaGroupGenerator, self).__init__(
|
||||||
game, ground_object, faction, ALBATROS
|
game, ground_object, faction, ALBATROS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MolniyaGroupGenerator(DDGroupGenerator):
|
class MolniyaGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(
|
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
|
||||||
):
|
|
||||||
super(MolniyaGroupGenerator, self).__init__(
|
super(MolniyaGroupGenerator, self).__init__(
|
||||||
game, ground_object, faction, MOLNIYA
|
game, ground_object, faction, MOLNIYA
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class KiloSubGroupGenerator(DDGroupGenerator):
|
class KiloSubGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(
|
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
|
||||||
):
|
|
||||||
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, KILO)
|
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, KILO)
|
||||||
|
|
||||||
|
|
||||||
class TangoSubGroupGenerator(DDGroupGenerator):
|
class TangoSubGroupGenerator(DDGroupGenerator):
|
||||||
def __init__(
|
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
|
||||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
|
||||||
):
|
|
||||||
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM)
|
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class SchnellbootGroupGenerator(ShipGroupGenerator):
|
class SchnellbootGroupGenerator(ShipGroupGenerator):
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
for i in range(random.randint(2, 4)):
|
for i in range(random.randint(2, 4)):
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from dcs.unitgroup import ShipGroup
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
|
from game.theater.theatergroundobject import (
|
||||||
|
LhaGroundObject,
|
||||||
|
CarrierGroundObject,
|
||||||
|
ShipGroundObject,
|
||||||
|
)
|
||||||
from gen.fleet.carrier_group import CarrierGroupGenerator
|
from gen.fleet.carrier_group import CarrierGroupGenerator
|
||||||
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
|
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
|
||||||
from gen.fleet.dd_group import (
|
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.uboat import UBoatGroupGenerator
|
||||||
from gen.fleet.ww2lst import WW2LSTGroupGenerator
|
from gen.fleet.ww2lst import WW2LSTGroupGenerator
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
|
||||||
|
|
||||||
SHIP_MAP = {
|
SHIP_MAP = {
|
||||||
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
|
"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
|
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]
|
faction = db.FACTIONS[faction_name]
|
||||||
if len(faction.navy_generators) > 0:
|
if len(faction.navy_generators) > 0:
|
||||||
@@ -61,26 +76,30 @@ def generate_ship_group(game, ground_object, faction_name: str):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def generate_carrier_group(faction: str, game, ground_object):
|
def generate_carrier_group(
|
||||||
"""
|
faction: str, game: Game, ground_object: CarrierGroundObject
|
||||||
This generate a carrier group
|
) -> ShipGroup:
|
||||||
:param parentCp: The parent control point
|
"""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 ground_object: The ground object which will own the ship group
|
||||||
:param country: Owner country
|
:return: The generated group.
|
||||||
:return: Nothing, but put the group reference inside the ground object
|
|
||||||
"""
|
"""
|
||||||
generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
||||||
generator.generate()
|
generator.generate()
|
||||||
return generator.get_generated_group()
|
return generator.get_generated_group()
|
||||||
|
|
||||||
|
|
||||||
def generate_lha_group(faction: str, game, ground_object):
|
def generate_lha_group(
|
||||||
"""
|
faction: str, game: Game, ground_object: LhaGroundObject
|
||||||
This generate a lha carrier group
|
) -> ShipGroup:
|
||||||
:param parentCp: The parent control point
|
"""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 ground_object: The ground object which will own the ship group
|
||||||
:param country: Owner country
|
:return: The generated group.
|
||||||
:return: Nothing, but put the group reference inside the ground object
|
|
||||||
"""
|
"""
|
||||||
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
|
||||||
generator.generate()
|
generator.generate()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class UBoatGroupGenerator(ShipGroupGenerator):
|
class UBoatGroupGenerator(ShipGroupGenerator):
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
for i in range(random.randint(1, 4)):
|
for i in range(random.randint(1, 4)):
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
|
|||||||
|
|
||||||
|
|
||||||
class WW2LSTGroupGenerator(ShipGroupGenerator):
|
class WW2LSTGroupGenerator(ShipGroupGenerator):
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
# Add LS Samuel Chase
|
# Add LS Samuel Chase
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Tuple,
|
Tuple,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
|
Any,
|
||||||
)
|
)
|
||||||
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
@@ -178,7 +179,7 @@ class AircraftAllocator:
|
|||||||
aircraft, task
|
aircraft, task
|
||||||
)
|
)
|
||||||
for squadron in squadrons:
|
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)
|
inventory.remove_aircraft(aircraft, flight.num_aircraft)
|
||||||
return airfield, squadron
|
return airfield, squadron
|
||||||
return None
|
return None
|
||||||
@@ -284,7 +285,7 @@ class ObjectiveFinder:
|
|||||||
self.game = game
|
self.game = game
|
||||||
self.is_player = is_player
|
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."""
|
"""Iterates over all enemy SAM sites."""
|
||||||
doctrine = self.game.faction_for(self.is_player).doctrine
|
doctrine = self.game.faction_for(self.is_player).doctrine
|
||||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||||
@@ -314,14 +315,14 @@ class ObjectiveFinder:
|
|||||||
|
|
||||||
yield ground_object, target_range
|
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.
|
"""Iterates over enemy SAMs in threat range of friendly control points.
|
||||||
|
|
||||||
SAM sites are sorted by their closest proximity to any friendly control
|
SAM sites are sorted by their closest proximity to any friendly control
|
||||||
point (airfield or fleet).
|
point (airfield or fleet).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
target_ranges: list[tuple[TheaterGroundObject, Distance]] = []
|
target_ranges: list[tuple[TheaterGroundObject[Any], Distance]] = []
|
||||||
for target, threat_range in self.enemy_air_defenses():
|
for target, threat_range in self.enemy_air_defenses():
|
||||||
ranges: list[Distance] = []
|
ranges: list[Distance] = []
|
||||||
for cp in self.friendly_control_points():
|
for cp in self.friendly_control_points():
|
||||||
@@ -374,9 +375,9 @@ class ObjectiveFinder:
|
|||||||
def _targets_by_range(
|
def _targets_by_range(
|
||||||
self, targets: Iterable[MissionTargetType]
|
self, targets: Iterable[MissionTargetType]
|
||||||
) -> Iterator[MissionTargetType]:
|
) -> Iterator[MissionTargetType]:
|
||||||
target_ranges: List[Tuple[MissionTargetType, int]] = []
|
target_ranges: list[tuple[MissionTargetType, float]] = []
|
||||||
for target in targets:
|
for target in targets:
|
||||||
ranges: List[int] = []
|
ranges: list[float] = []
|
||||||
for cp in self.friendly_control_points():
|
for cp in self.friendly_control_points():
|
||||||
ranges.append(target.distance_to(cp))
|
ranges.append(target.distance_to(cp))
|
||||||
target_ranges.append((target, min(ranges)))
|
target_ranges.append((target, min(ranges)))
|
||||||
@@ -385,13 +386,13 @@ class ObjectiveFinder:
|
|||||||
for target, _range in target_ranges:
|
for target, _range in target_ranges:
|
||||||
yield target
|
yield target
|
||||||
|
|
||||||
def strike_targets(self) -> Iterator[TheaterGroundObject]:
|
def strike_targets(self) -> Iterator[TheaterGroundObject[Any]]:
|
||||||
"""Iterates over enemy strike targets.
|
"""Iterates over enemy strike targets.
|
||||||
|
|
||||||
Targets are sorted by their closest proximity to any friendly control
|
Targets are sorted by their closest proximity to any friendly control
|
||||||
point (airfield or fleet).
|
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 objectives are made of several individual TGOs (one per
|
||||||
# building).
|
# building).
|
||||||
found_targets: Set[str] = set()
|
found_targets: Set[str] = set()
|
||||||
@@ -430,7 +431,7 @@ class ObjectiveFinder:
|
|||||||
continue
|
continue
|
||||||
if ground_object.name in found_targets:
|
if ground_object.name in found_targets:
|
||||||
continue
|
continue
|
||||||
ranges: List[int] = []
|
ranges: list[float] = []
|
||||||
for friendly_cp in self.friendly_control_points():
|
for friendly_cp in self.friendly_control_points():
|
||||||
ranges.append(ground_object.distance_to(friendly_cp))
|
ranges.append(ground_object.distance_to(friendly_cp))
|
||||||
targets.append((ground_object, min(ranges)))
|
targets.append((ground_object, min(ranges)))
|
||||||
@@ -604,27 +605,35 @@ class CoalitionMissionPlanner:
|
|||||||
also possible for the player to exclude mission types from their squadron
|
also possible for the player to exclude mission types from their squadron
|
||||||
designs.
|
designs.
|
||||||
"""
|
"""
|
||||||
all_compatible = aircraft_for_task(mission_type)
|
return self.game.air_wing_for(self.is_player).can_auto_plan(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
|
|
||||||
|
|
||||||
@property
|
def critical_missions(self) -> Iterator[ProposedMission]:
|
||||||
def oca_aircraft_plannable(self) -> bool:
|
"""Identifies the most important missions to plan this turn.
|
||||||
return (
|
|
||||||
self.air_wing_can_plan(FlightType.OCA_AIRCRAFT)
|
Non-critical missions that cannot be fulfilled will create purchase
|
||||||
and self.game.settings.default_start_type == "Cold"
|
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.
|
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
|
||||||
for cp in self.objective_finder.vulnerable_control_points():
|
for cp in self.objective_finder.vulnerable_control_points():
|
||||||
# Plan CAP in such a way, that it is established during the whole desired
|
# Plan CAP in such a way, that it is established during the whole desired mission length
|
||||||
# mission length.
|
|
||||||
for _ in range(
|
for _ in range(
|
||||||
0,
|
0,
|
||||||
int(self.game.settings.desired_player_mission_duration.total_seconds()),
|
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.
|
# Find front lines, plan CAS.
|
||||||
for front_line in self.objective_finder.front_lines():
|
for front_line in self.objective_finder.front_lines():
|
||||||
flights = [ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE)]
|
yield ProposedMission(
|
||||||
if self.air_wing_can_plan(FlightType.TARCAP):
|
front_line,
|
||||||
# This is *not* an escort because front lines don't create a threat
|
[
|
||||||
# zone. Generating threat zones from front lines causes the front
|
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
|
||||||
# line to push back BARCAPs as it gets closer to the base. While
|
# This is *not* an escort because front lines don't create a threat
|
||||||
# front lines do have the same problem of potentially pulling
|
# zone. Generating threat zones from front lines causes the front
|
||||||
# BARCAPs off bases to engage a front line TARCAP, that's probably
|
# line to push back BARCAPs as it gets closer to the base. While
|
||||||
# the one time where we do want that.
|
# front lines do have the same problem of potentially pulling
|
||||||
#
|
# BARCAPs off bases to engage a front line TARCAP, that's probably
|
||||||
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
|
# the one time where we do want that.
|
||||||
# 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
|
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
|
||||||
# line project a threat zone (so that strike missions will route
|
# We don't have intercept missions yet so this isn't something we
|
||||||
# around it) and instead *not plan* a BARCAP at bases near the
|
# can do today, but we should probably return to having the front
|
||||||
# front, since there isn't a place to put a barrier. Instead, the
|
# line project a threat zone (so that strike missions will route
|
||||||
# aircraft that would have been a BARCAP could be used as additional
|
# around it) and instead *not plan* a BARCAP at bases near the
|
||||||
# interceptors and TARCAPs which will defend the base but won't be
|
# front, since there isn't a place to put a barrier. Instead, the
|
||||||
# trying to avoid front line contacts.
|
# aircraft that would have been a BARCAP could be used as additional
|
||||||
flights.append(ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE))
|
# interceptors and TARCAPs which will defend the base but won't be
|
||||||
yield ProposedMission(front_line, flights)
|
# 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,
|
# Find enemy SAM sites with ranges that cover friendly CPs, front lines,
|
||||||
# or objects, plan DEAD.
|
# or objects, plan DEAD.
|
||||||
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
# Find enemy SAM sites with ranges that extend to within 50 nmi of
|
||||||
@@ -686,10 +700,7 @@ class CoalitionMissionPlanner:
|
|||||||
else:
|
else:
|
||||||
flights.append(
|
flights.append(
|
||||||
ProposedFlight(
|
ProposedFlight(
|
||||||
FlightType.SEAD_ESCORT,
|
FlightType.SEAD_ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.Sead
|
||||||
2,
|
|
||||||
self.MAX_SEAD_RANGE,
|
|
||||||
EscortType.Sead,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# TODO: Max escort range.
|
# TODO: Max escort range.
|
||||||
@@ -700,7 +711,6 @@ class CoalitionMissionPlanner:
|
|||||||
)
|
)
|
||||||
yield ProposedMission(sam, flights)
|
yield ProposedMission(sam, flights)
|
||||||
|
|
||||||
def propose_convoy_interdiction(self) -> Iterator[ProposedMission]:
|
|
||||||
# These will only rarely get planned. When a convoy is travelling multiple legs,
|
# These will only rarely get planned. When a convoy is travelling multiple legs,
|
||||||
# they're targetable after the first leg. The reason for this is that
|
# they're targetable after the first leg. The reason for this is that
|
||||||
# procurement happens *after* mission planning so that the missions that could
|
# 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():
|
for ship in self.objective_finder.cargo_ships():
|
||||||
yield ProposedMission(
|
yield ProposedMission(
|
||||||
ship,
|
ship,
|
||||||
@@ -745,7 +754,6 @@ class CoalitionMissionPlanner:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def propose_naval_strikes(self) -> Iterator[ProposedMission]:
|
|
||||||
for group in self.objective_finder.threatening_ships():
|
for group in self.objective_finder.threatening_ships():
|
||||||
yield ProposedMission(
|
yield ProposedMission(
|
||||||
group,
|
group,
|
||||||
@@ -761,7 +769,6 @@ class CoalitionMissionPlanner:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def propose_bai(self) -> Iterator[ProposedMission]:
|
|
||||||
for group in self.objective_finder.threatening_vehicle_groups():
|
for group in self.objective_finder.threatening_vehicle_groups():
|
||||||
yield ProposedMission(
|
yield ProposedMission(
|
||||||
group,
|
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):
|
for target in self.objective_finder.oca_targets(min_aircraft=20):
|
||||||
flights = []
|
flights = [
|
||||||
if self.air_wing_can_plan(FlightType.OCA_RUNWAY):
|
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE),
|
||||||
flights.append(
|
]
|
||||||
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE)
|
if self.game.settings.default_start_type == "Cold":
|
||||||
)
|
|
||||||
if self.oca_aircraft_plannable:
|
|
||||||
# Only schedule if the default start type is Cold. If the player
|
# Only schedule if the default start type is Cold. If the player
|
||||||
# has set anything else there are no targets to hit.
|
# has set anything else there are no targets to hit.
|
||||||
flights.append(
|
flights.append(
|
||||||
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
|
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(
|
flights.extend(
|
||||||
[
|
[
|
||||||
# TODO: Max escort range.
|
# TODO: Max escort range.
|
||||||
@@ -809,7 +807,7 @@ class CoalitionMissionPlanner:
|
|||||||
)
|
)
|
||||||
yield ProposedMission(target, flights)
|
yield ProposedMission(target, flights)
|
||||||
|
|
||||||
def propose_building_strikes(self) -> Iterator[ProposedMission]:
|
# Plan strike missions.
|
||||||
for target in self.objective_finder.strike_targets():
|
for target in self.objective_finder.strike_targets():
|
||||||
yield ProposedMission(
|
yield ProposedMission(
|
||||||
target,
|
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:
|
def plan_missions(self) -> None:
|
||||||
"""Identifies and plans mission for the turn."""
|
"""Identifies and plans mission for the turn."""
|
||||||
player = "Blue" if self.is_player else "Red"
|
player = "Blue" if self.is_player else "Red"
|
||||||
@@ -878,6 +834,11 @@ class CoalitionMissionPlanner:
|
|||||||
for proposed_mission in self.propose_missions():
|
for proposed_mission in self.propose_missions():
|
||||||
self.plan_mission(proposed_mission, tracer)
|
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"):
|
with logged_duration(f"{player} mission scheduling"):
|
||||||
self.stagger_missions()
|
self.stagger_missions()
|
||||||
|
|
||||||
@@ -1097,7 +1058,7 @@ class CoalitionMissionPlanner:
|
|||||||
# delayed until their takeoff time by AirConflictGenerator.
|
# delayed until their takeoff time by AirConflictGenerator.
|
||||||
package.time_over_target = next(start_time) + tot
|
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.
|
"""Emits a planning message to the player.
|
||||||
|
|
||||||
If the mission planner belongs to the players coalition, this emits a
|
If the mission planner belongs to the players coalition, this emits a
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Type
|
from collections import Sequence
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from dcs.helicopters import (
|
from dcs.helicopters import (
|
||||||
AH_1W,
|
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.f22a.f22a import F_22A
|
||||||
from pydcs_extensions.hercules.hercules import Hercules
|
from pydcs_extensions.hercules.hercules import Hercules
|
||||||
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
|
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
|
from pydcs_extensions.su57.su57 import Su_57
|
||||||
|
|
||||||
# All aircraft lists are in priority order. Aircraft higher in the list will be
|
# 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 = [
|
CAP_CAPABLE = [
|
||||||
Su_57,
|
Su_57,
|
||||||
F_22A,
|
F_22A,
|
||||||
MiG_31,
|
F_15C,
|
||||||
F_14B,
|
F_14B,
|
||||||
F_14A_135_GR,
|
F_14A_135_GR,
|
||||||
MiG_25PD,
|
|
||||||
Su_33,
|
Su_33,
|
||||||
|
J_11A,
|
||||||
Su_30,
|
Su_30,
|
||||||
Su_27,
|
Su_27,
|
||||||
J_11A,
|
|
||||||
F_15C,
|
|
||||||
MiG_29S,
|
MiG_29S,
|
||||||
MiG_29G,
|
|
||||||
MiG_29A,
|
|
||||||
F_16C_50,
|
F_16C_50,
|
||||||
FA_18C_hornet,
|
FA_18C_hornet,
|
||||||
|
JF_17,
|
||||||
|
JAS39Gripen,
|
||||||
F_16A,
|
F_16A,
|
||||||
F_4E,
|
F_4E,
|
||||||
JAS39Gripen,
|
MiG_31,
|
||||||
JF_17,
|
MiG_25PD,
|
||||||
|
MiG_29G,
|
||||||
|
MiG_29A,
|
||||||
MiG_23MLD,
|
MiG_23MLD,
|
||||||
MiG_21Bis,
|
MiG_21Bis,
|
||||||
Mirage_2000_5,
|
Mirage_2000_5,
|
||||||
M_2000C,
|
|
||||||
F_15E,
|
F_15E,
|
||||||
|
M_2000C,
|
||||||
F_5E_3,
|
F_5E_3,
|
||||||
MiG_19P,
|
MiG_19P,
|
||||||
A_4E_C,
|
A_4E_C,
|
||||||
@@ -174,6 +174,7 @@ CAS_CAPABLE = [
|
|||||||
A_10C_2,
|
A_10C_2,
|
||||||
A_10C,
|
A_10C,
|
||||||
Hercules,
|
Hercules,
|
||||||
|
Su_34,
|
||||||
Su_25TM,
|
Su_25TM,
|
||||||
Su_25T,
|
Su_25T,
|
||||||
Su_25,
|
Su_25,
|
||||||
@@ -191,17 +192,16 @@ CAS_CAPABLE = [
|
|||||||
F_14B,
|
F_14B,
|
||||||
F_14A_135_GR,
|
F_14A_135_GR,
|
||||||
AJS37,
|
AJS37,
|
||||||
Su_24MR,
|
|
||||||
Su_24M,
|
Su_24M,
|
||||||
Su_17M4,
|
Su_17M4,
|
||||||
|
Su_33,
|
||||||
F_4E,
|
F_4E,
|
||||||
S_3B,
|
S_3B,
|
||||||
Su_34,
|
|
||||||
Su_30,
|
Su_30,
|
||||||
MiG_19P,
|
|
||||||
MiG_29S,
|
MiG_29S,
|
||||||
MiG_27K,
|
MiG_27K,
|
||||||
MiG_29A,
|
MiG_29A,
|
||||||
|
MiG_21Bis,
|
||||||
AH_64D,
|
AH_64D,
|
||||||
AH_64A,
|
AH_64A,
|
||||||
AH_1W,
|
AH_1W,
|
||||||
@@ -213,14 +213,14 @@ CAS_CAPABLE = [
|
|||||||
Mi_24P,
|
Mi_24P,
|
||||||
Mi_24V,
|
Mi_24V,
|
||||||
Mi_8MT,
|
Mi_8MT,
|
||||||
UH_1H,
|
MiG_19P,
|
||||||
MiG_15bis,
|
MiG_15bis,
|
||||||
M_2000C,
|
M_2000C,
|
||||||
F_5E_3,
|
F_5E_3,
|
||||||
F_86F_Sabre,
|
F_86F_Sabre,
|
||||||
C_101CC,
|
C_101CC,
|
||||||
MB_339PAN,
|
|
||||||
L_39ZA,
|
L_39ZA,
|
||||||
|
UH_1H,
|
||||||
A_20G,
|
A_20G,
|
||||||
Ju_88A4,
|
Ju_88A4,
|
||||||
P_47D_40,
|
P_47D_40,
|
||||||
@@ -301,13 +301,14 @@ STRIKE_CAPABLE = [
|
|||||||
Tornado_GR4,
|
Tornado_GR4,
|
||||||
F_16C_50,
|
F_16C_50,
|
||||||
FA_18C_hornet,
|
FA_18C_hornet,
|
||||||
|
AV8BNA,
|
||||||
|
JF_17,
|
||||||
F_16A,
|
F_16A,
|
||||||
F_14B,
|
F_14B,
|
||||||
F_14A_135_GR,
|
F_14A_135_GR,
|
||||||
JAS39Gripen_AG,
|
JAS39Gripen_AG,
|
||||||
Tornado_IDS,
|
Tornado_IDS,
|
||||||
Su_17M4,
|
Su_17M4,
|
||||||
Su_24MR,
|
|
||||||
Su_24M,
|
Su_24M,
|
||||||
Su_25TM,
|
Su_25TM,
|
||||||
Su_25T,
|
Su_25T,
|
||||||
@@ -319,11 +320,9 @@ STRIKE_CAPABLE = [
|
|||||||
MiG_29S,
|
MiG_29S,
|
||||||
MiG_29G,
|
MiG_29G,
|
||||||
MiG_29A,
|
MiG_29A,
|
||||||
JF_17,
|
|
||||||
F_4E,
|
F_4E,
|
||||||
A_10C_2,
|
A_10C_2,
|
||||||
A_10C,
|
A_10C,
|
||||||
AV8BNA,
|
|
||||||
S_3B,
|
S_3B,
|
||||||
A_4E_C,
|
A_4E_C,
|
||||||
M_2000C,
|
M_2000C,
|
||||||
@@ -332,7 +331,6 @@ STRIKE_CAPABLE = [
|
|||||||
MiG_15bis,
|
MiG_15bis,
|
||||||
F_5E_3,
|
F_5E_3,
|
||||||
F_86F_Sabre,
|
F_86F_Sabre,
|
||||||
MB_339PAN,
|
|
||||||
C_101CC,
|
C_101CC,
|
||||||
L_39ZA,
|
L_39ZA,
|
||||||
B_17G,
|
B_17G,
|
||||||
@@ -378,6 +376,7 @@ RUNWAY_ATTACK_CAPABLE = [
|
|||||||
Su_34,
|
Su_34,
|
||||||
Su_30,
|
Su_30,
|
||||||
Tornado_IDS,
|
Tornado_IDS,
|
||||||
|
M_2000C,
|
||||||
] + STRIKE_CAPABLE
|
] + STRIKE_CAPABLE
|
||||||
|
|
||||||
# For any aircraft that isn't necessarily directly involved in strike
|
# 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)
|
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
|
||||||
if task in cap_missions:
|
if task in cap_missions:
|
||||||
return CAP_CAPABLE
|
return CAP_CAPABLE
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import Enum
|
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.mapping import Point
|
||||||
from dcs.point import MovingPoint, PointAction
|
from dcs.point import MovingPoint, PointAction
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
|
|
||||||
from game import db
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.squadrons import Pilot, Squadron
|
from game.squadrons import Pilot, Squadron
|
||||||
from game.theater.controlpoint import ControlPoint, MissionTarget
|
from game.theater.controlpoint import ControlPoint, MissionTarget
|
||||||
@@ -141,8 +140,8 @@ class FlightWaypoint:
|
|||||||
waypoint_type: The waypoint type.
|
waypoint_type: The waypoint type.
|
||||||
x: X cooidinate of the waypoint.
|
x: X cooidinate of the waypoint.
|
||||||
y: Y coordinate of the waypoint.
|
y: Y coordinate of the waypoint.
|
||||||
alt: Altitude of the waypoint. By default this is AGL, but it can be
|
alt: Altitude of the waypoint. By default this is MSL, but it can be
|
||||||
changed to MSL by setting alt_type to "RADIO".
|
changed to AGL by setting alt_type to "RADIO"
|
||||||
"""
|
"""
|
||||||
self.waypoint_type = waypoint_type
|
self.waypoint_type = waypoint_type
|
||||||
self.x = x
|
self.x = x
|
||||||
@@ -154,7 +153,7 @@ class FlightWaypoint:
|
|||||||
# Only used in the waypoint list in the flight edit page. No sense
|
# Only used in the waypoint list in the flight edit page. No sense
|
||||||
# having three names. A short and long form is enough.
|
# having three names. A short and long form is enough.
|
||||||
self.description = ""
|
self.description = ""
|
||||||
self.targets: List[Union[MissionTarget, Unit]] = []
|
self.targets: Sequence[Union[MissionTarget, Unit]] = []
|
||||||
self.obj_name = ""
|
self.obj_name = ""
|
||||||
self.pretty_name = ""
|
self.pretty_name = ""
|
||||||
self.only_for_player = False
|
self.only_for_player = False
|
||||||
@@ -323,12 +322,12 @@ class Flight:
|
|||||||
def clear_roster(self) -> None:
|
def clear_roster(self) -> None:
|
||||||
self.roster.clear()
|
self.roster.clear()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
if self.custom_name:
|
if self.custom_name:
|
||||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||||
return f"[{self.flight_type}] {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:
|
if self.custom_name:
|
||||||
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
return f"{self.custom_name} {self.count} x {self.unit_type}"
|
||||||
return f"[{self.flight_type}] {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 typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
|
||||||
|
|
||||||
from dcs.mapping import Point
|
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 dcs.unit import Unit
|
||||||
from shapely.geometry import Point as ShapelyPoint
|
from shapely.geometry import Point as ShapelyPoint
|
||||||
|
|
||||||
@@ -38,8 +27,9 @@ from game.theater import (
|
|||||||
MissionTarget,
|
MissionTarget,
|
||||||
SamGroundObject,
|
SamGroundObject,
|
||||||
TheaterGroundObject,
|
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 game.utils import Distance, Speed, feet, meters, nautical_miles, knots
|
||||||
from .closestairfields import ObjectiveDistanceCache
|
from .closestairfields import ObjectiveDistanceCache
|
||||||
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
|
||||||
@@ -229,11 +219,7 @@ class FlightPlan:
|
|||||||
tot_waypoint = self.tot_waypoint
|
tot_waypoint = self.tot_waypoint
|
||||||
if tot_waypoint is None:
|
if tot_waypoint is None:
|
||||||
return None
|
return None
|
||||||
|
return self.tot - self._travel_time_to_waypoint(tot_waypoint)
|
||||||
time = self.tot
|
|
||||||
if time is None:
|
|
||||||
return None
|
|
||||||
return time - self._travel_time_to_waypoint(tot_waypoint)
|
|
||||||
|
|
||||||
def startup_time(self) -> Optional[timedelta]:
|
def startup_time(self) -> Optional[timedelta]:
|
||||||
takeoff_time = self.takeoff_time()
|
takeoff_time = self.takeoff_time()
|
||||||
@@ -404,6 +390,9 @@ class PatrollingFlightPlan(FlightPlan):
|
|||||||
#: Maximum time to remain on station.
|
#: Maximum time to remain on station.
|
||||||
patrol_duration: timedelta
|
patrol_duration: timedelta
|
||||||
|
|
||||||
|
#: Racetrack speed TAS.
|
||||||
|
patrol_speed: Speed
|
||||||
|
|
||||||
#: The engagement range of any Search Then Engage task, or the radius of a
|
#: 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
|
#: 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
|
#: this mission within this range of the flight's current position (or the
|
||||||
@@ -785,9 +774,6 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
|
|||||||
divert: Optional[FlightWaypoint]
|
divert: Optional[FlightWaypoint]
|
||||||
bullseye: FlightWaypoint
|
bullseye: FlightWaypoint
|
||||||
|
|
||||||
#: Racetrack speed.
|
|
||||||
patrol_speed: Speed
|
|
||||||
|
|
||||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||||
yield self.takeoff
|
yield self.takeoff
|
||||||
yield from self.nav_to
|
yield from self.nav_to
|
||||||
@@ -1092,35 +1078,28 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
orbit_location = self.aewc_orbit(location)
|
orbit_location = self.aewc_orbit(location)
|
||||||
|
|
||||||
# As high as possible to maximize detection and on-station time.
|
if flight.unit_type.patrol_altitude is not None:
|
||||||
if flight.unit_type == E_2C:
|
patrol_alt = flight.unit_type.patrol_altitude
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
patrol_alt = feet(25000)
|
patrol_alt = feet(25000)
|
||||||
|
|
||||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
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(
|
return AwacsFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
flight=flight,
|
flight=flight,
|
||||||
takeoff=builder.takeoff(flight.departure),
|
takeoff=builder.takeoff(flight.departure),
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
flight.departure.position, orbit_location.position, patrol_alt
|
flight.departure.position, orbit.position, patrol_alt
|
||||||
),
|
),
|
||||||
nav_from=builder.nav_path(
|
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),
|
land=builder.land(flight.arrival),
|
||||||
divert=builder.divert(flight.divert),
|
divert=builder.divert(flight.divert),
|
||||||
bullseye=builder.bullseye(),
|
bullseye=builder.bullseye(),
|
||||||
hold=orbit_location,
|
hold=orbit,
|
||||||
hold_duration=timedelta(hours=4),
|
hold_duration=timedelta(hours=4),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1151,7 +1130,7 @@ class FlightPlanBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@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]
|
return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups]
|
||||||
|
|
||||||
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
|
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
|
||||||
@@ -1164,12 +1143,9 @@ class FlightPlanBuilder:
|
|||||||
|
|
||||||
from game.transfers import CargoShip
|
from game.transfers import CargoShip
|
||||||
|
|
||||||
if isinstance(location, ControlPoint):
|
if isinstance(location, NavalControlPoint):
|
||||||
if not location.is_fleet:
|
targets = self.anti_ship_targets_for_tgo(location.find_main_tgo())
|
||||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
elif isinstance(location, NavalGroundObject):
|
||||||
# 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):
|
|
||||||
targets = self.anti_ship_targets_for_tgo(location)
|
targets = self.anti_ship_targets_for_tgo(location)
|
||||||
elif isinstance(location, CargoShip):
|
elif isinstance(location, CargoShip):
|
||||||
targets = [StrikeTarget(location.name, location)]
|
targets = [StrikeTarget(location.name, location)]
|
||||||
@@ -1191,21 +1167,28 @@ class FlightPlanBuilder:
|
|||||||
if isinstance(location, FrontLine):
|
if isinstance(location, FrontLine):
|
||||||
raise InvalidObjectiveLocation(flight.flight_type, location)
|
raise InvalidObjectiveLocation(flight.flight_type, location)
|
||||||
|
|
||||||
start, end = self.racetrack_for_objective(location, barcap=True)
|
start_pos, end_pos = self.racetrack_for_objective(location, barcap=True)
|
||||||
patrol_alt = meters(
|
|
||||||
random.randint(
|
preferred_alt = flight.unit_type.preferred_patrol_altitude
|
||||||
int(self.doctrine.min_patrol_altitude.meters),
|
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||||
int(self.doctrine.max_patrol_altitude.meters),
|
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)
|
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(
|
return BarCapFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
flight=flight,
|
flight=flight,
|
||||||
patrol_duration=self.doctrine.cap_duration,
|
patrol_duration=self.doctrine.cap_duration,
|
||||||
|
patrol_speed=patrol_speed,
|
||||||
engagement_distance=self.doctrine.cap_engagement_range,
|
engagement_distance=self.doctrine.cap_engagement_range,
|
||||||
takeoff=builder.takeoff(flight.departure),
|
takeoff=builder.takeoff(flight.departure),
|
||||||
nav_to=builder.nav_path(
|
nav_to=builder.nav_path(
|
||||||
@@ -1231,10 +1214,12 @@ class FlightPlanBuilder:
|
|||||||
target = self.package.target.position
|
target = self.package.target.position
|
||||||
|
|
||||||
heading = self.package.waypoints.join.heading_between_point(target)
|
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)
|
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))
|
hold = builder.hold(self._hold_point(flight))
|
||||||
|
|
||||||
@@ -1428,11 +1413,15 @@ class FlightPlanBuilder:
|
|||||||
"""
|
"""
|
||||||
location = self.package.target
|
location = self.package.target
|
||||||
|
|
||||||
patrol_alt = meters(
|
preferred_alt = flight.unit_type.preferred_patrol_altitude
|
||||||
random.randint(
|
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
|
||||||
int(self.doctrine.min_patrol_altitude.meters),
|
patrol_alt = max(
|
||||||
int(self.doctrine.max_patrol_altitude.meters),
|
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
|
# Create points
|
||||||
@@ -1455,6 +1444,7 @@ class FlightPlanBuilder:
|
|||||||
# requests an escort the CAP flight will remain on station for the
|
# requests an escort the CAP flight will remain on station for the
|
||||||
# duration of the escorted mission, or until it is winchester/bingo.
|
# duration of the escorted mission, or until it is winchester/bingo.
|
||||||
patrol_duration=self.doctrine.cap_duration,
|
patrol_duration=self.doctrine.cap_duration,
|
||||||
|
patrol_speed=patrol_speed,
|
||||||
engagement_distance=self.doctrine.cap_engagement_range,
|
engagement_distance=self.doctrine.cap_engagement_range,
|
||||||
takeoff=builder.takeoff(flight.departure),
|
takeoff=builder.takeoff(flight.departure),
|
||||||
nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
|
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)
|
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(
|
return CasFlightPlan(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
flight=flight,
|
flight=flight,
|
||||||
patrol_duration=self.doctrine.cas_duration,
|
patrol_duration=self.doctrine.cas_duration,
|
||||||
|
patrol_speed=patrol_speed,
|
||||||
takeoff=builder.takeoff(flight.departure),
|
takeoff=builder.takeoff(flight.departure),
|
||||||
nav_to=builder.nav_path(
|
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(
|
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(
|
patrol_start=builder.ingress(
|
||||||
FlightWaypointType.INGRESS_CAS, ingress, location
|
FlightWaypointType.INGRESS_CAS, ingress, location
|
||||||
@@ -1680,31 +1687,18 @@ class FlightPlanBuilder:
|
|||||||
builder = WaypointBuilder(flight, self.game, self.is_player)
|
builder = WaypointBuilder(flight, self.game, self.is_player)
|
||||||
|
|
||||||
tanker_type = flight.unit_type
|
tanker_type = flight.unit_type
|
||||||
if tanker_type is KC_135:
|
if tanker_type.patrol_altitude is not None:
|
||||||
# ~300 knots IAS.
|
altitude = tanker_type.patrol_altitude
|
||||||
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)
|
|
||||||
else:
|
else:
|
||||||
# ~280 knots IAS.
|
|
||||||
speed = knots(400)
|
|
||||||
altitude = feet(21000)
|
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)
|
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
|
||||||
|
|
||||||
return RefuelingFlightPlan(
|
return RefuelingFlightPlan(
|
||||||
@@ -1903,23 +1897,23 @@ class FlightPlanBuilder:
|
|||||||
return self._retreating_rendezvous_point(attack_transition)
|
return self._retreating_rendezvous_point(attack_transition)
|
||||||
return self._advancing_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(
|
return self.package.target.position.point_from_heading(
|
||||||
heading - 180 + 15, self.doctrine.ingress_egress_distance.meters
|
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(
|
return self.package.target.position.point_from_heading(
|
||||||
heading - 180 - 15, self.doctrine.ingress_egress_distance.meters
|
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)
|
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)
|
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)
|
return self.package_airfield().position.distance_to_point(point)
|
||||||
|
|
||||||
def package_airfield(self) -> ControlPoint:
|
def package_airfield(self) -> ControlPoint:
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ class Loadout:
|
|||||||
is_custom: bool = False,
|
is_custom: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = name
|
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.date = date
|
||||||
self.is_custom = is_custom
|
self.is_custom = is_custom
|
||||||
|
|
||||||
@@ -92,6 +96,7 @@ class Loadout:
|
|||||||
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
|
||||||
FlightType.STRIKE: ("STRIKE",),
|
FlightType.STRIKE: ("STRIKE",),
|
||||||
FlightType.ANTISHIP: ("ANTISHIP",),
|
FlightType.ANTISHIP: ("ANTISHIP",),
|
||||||
|
FlightType.DEAD: ("DEAD",),
|
||||||
FlightType.SEAD: ("SEAD",),
|
FlightType.SEAD: ("SEAD",),
|
||||||
FlightType.BAI: ("BAI",),
|
FlightType.BAI: ("BAI",),
|
||||||
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
|
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
|
||||||
@@ -133,4 +138,8 @@ class Loadout:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Try group.load_task_default_loadout(loadout_for_task)
|
# 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)
|
return Loadout("Empty", {}, date=None)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -10,11 +11,12 @@ from typing import (
|
|||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Tuple,
|
Tuple,
|
||||||
Union,
|
Union,
|
||||||
|
Any,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.unit import Unit
|
from dcs.unit import Unit
|
||||||
from dcs.unitgroup import Group, VehicleGroup
|
from dcs.unitgroup import Group, VehicleGroup, ShipGroup
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game import Game
|
from game import Game
|
||||||
@@ -33,7 +35,9 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StrikeTarget:
|
class StrikeTarget:
|
||||||
name: str
|
name: str
|
||||||
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, MultiGroupTransport]
|
target: Union[
|
||||||
|
VehicleGroup, TheaterGroundObject[Any], Unit, ShipGroup, MultiGroupTransport
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class WaypointBuilder:
|
class WaypointBuilder:
|
||||||
@@ -54,7 +58,7 @@ class WaypointBuilder:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_helo(self) -> bool:
|
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:
|
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
|
||||||
"""Create takeoff waypoint for the given arrival airfield or carrier.
|
"""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
|
# description in gen.aircraft.JoinPointBuilder), so instead we give
|
||||||
# the escort flights a flight plan including the ingress point, target
|
# the escort flights a flight plan including the ingress point, target
|
||||||
# area, and egress point.
|
# area, and egress point.
|
||||||
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
|
||||||
|
|
||||||
waypoint = FlightWaypoint(
|
waypoint = FlightWaypoint(
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
@@ -455,8 +459,8 @@ class WaypointBuilder:
|
|||||||
waypoint.description = "Escort the package"
|
waypoint.description = "Escort the package"
|
||||||
waypoint.pretty_name = "Target area"
|
waypoint.pretty_name = "Target area"
|
||||||
|
|
||||||
egress = self.egress(egress, target)
|
egress_wp = self.egress(egress, target)
|
||||||
return ingress, waypoint, egress
|
return ingress_wp, waypoint, egress_wp
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pickup(control_point: ControlPoint) -> FlightWaypoint:
|
def pickup(control_point: ControlPoint) -> FlightWaypoint:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ForcedOptionsGenerator:
|
|||||||
if blue.unrestricted_satnav or red.unrestricted_satnav:
|
if blue.unrestricted_satnav or red.unrestricted_satnav:
|
||||||
self.mission.forced_options.unrestricted_satnav = True
|
self.mission.forced_options.unrestricted_satnav = True
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
self._set_options_view()
|
self._set_options_view()
|
||||||
self._set_external_views()
|
self._set_external_views()
|
||||||
self._set_labels()
|
self._set_labels()
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List
|
from typing import Dict, List, TYPE_CHECKING
|
||||||
|
|
||||||
from game.data.groundunitclass import GroundUnitClass
|
from game.data.groundunitclass import GroundUnitClass
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.theater import ControlPoint
|
from game.theater import ControlPoint
|
||||||
from gen.ground_forces.combat_stance import CombatStance
|
from gen.ground_forces.combat_stance import CombatStance
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
|
||||||
MAX_COMBAT_GROUP_PER_CP = 10
|
MAX_COMBAT_GROUP_PER_CP = 10
|
||||||
|
|
||||||
|
|
||||||
@@ -52,10 +57,9 @@ class CombatGroup:
|
|||||||
self.unit_type = unit_type
|
self.unit_type = unit_type
|
||||||
self.size = size
|
self.size = size
|
||||||
self.role = role
|
self.role = role
|
||||||
self.assigned_enemy_cp = None
|
|
||||||
self.start_position = None
|
self.start_position = None
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
s = f"ROLE : {self.role}\n"
|
s = f"ROLE : {self.role}\n"
|
||||||
if self.size:
|
if self.size:
|
||||||
s += f"UNITS {self.unit_type} * {self.size}"
|
s += f"UNITS {self.unit_type} * {self.size}"
|
||||||
@@ -63,7 +67,7 @@ class CombatGroup:
|
|||||||
|
|
||||||
|
|
||||||
class GroundPlanner:
|
class GroundPlanner:
|
||||||
def __init__(self, cp: ControlPoint, game):
|
def __init__(self, cp: ControlPoint, game: Game) -> None:
|
||||||
self.cp = cp
|
self.cp = cp
|
||||||
self.game = game
|
self.game = game
|
||||||
self.connected_enemy_cp = [
|
self.connected_enemy_cp = [
|
||||||
@@ -83,17 +87,15 @@ class GroundPlanner:
|
|||||||
self.units_per_cp[cp.id] = []
|
self.units_per_cp[cp.id] = []
|
||||||
self.reserve: List[CombatGroup] = []
|
self.reserve: List[CombatGroup] = []
|
||||||
|
|
||||||
def plan_groundwar(self):
|
def plan_groundwar(self) -> None:
|
||||||
|
|
||||||
ground_unit_limit = self.cp.frontline_unit_count_limit
|
ground_unit_limit = self.cp.frontline_unit_count_limit
|
||||||
|
|
||||||
remaining_available_frontline_units = ground_unit_limit
|
remaining_available_frontline_units = ground_unit_limit
|
||||||
|
|
||||||
if hasattr(self.cp, "stance"):
|
# TODO: Fix to handle the per-front stances.
|
||||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
|
# https://github.com/dcs-liberation/dcs_liberation/issues/1417
|
||||||
else:
|
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
|
||||||
self.cp.stance = CombatStance.DEFENSIVE
|
|
||||||
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
|
|
||||||
|
|
||||||
# Create combat groups and assign them randomly to each enemy CP
|
# Create combat groups and assign them randomly to each enemy CP
|
||||||
for unit_type in self.cp.base.armor:
|
for unit_type in self.cp.base.armor:
|
||||||
@@ -152,20 +154,9 @@ class GroundPlanner:
|
|||||||
if len(self.connected_enemy_cp) > 0:
|
if len(self.connected_enemy_cp) > 0:
|
||||||
enemy_cp = random.choice(self.connected_enemy_cp).id
|
enemy_cp = random.choice(self.connected_enemy_cp).id
|
||||||
self.units_per_cp[enemy_cp].append(group)
|
self.units_per_cp[enemy_cp].append(group)
|
||||||
group.assigned_enemy_cp = enemy_cp
|
|
||||||
else:
|
else:
|
||||||
self.reserve.append(group)
|
self.reserve.append(group)
|
||||||
group.assigned_enemy_cp = "__reserve__"
|
|
||||||
collection.append(group)
|
collection.append(group)
|
||||||
|
|
||||||
if remaining_available_frontline_units == 0:
|
if remaining_available_frontline_units == 0:
|
||||||
break
|
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 logging
|
||||||
import random
|
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 import Mission, Point, unitgroup
|
||||||
from dcs.action import SceneryDestructionZone
|
from dcs.action import SceneryDestructionZone
|
||||||
from dcs.country import Country
|
from dcs.country import Country
|
||||||
from dcs.point import StaticPoint
|
from dcs.point import StaticPoint, MovingPoint
|
||||||
from dcs.statics import Fortification, fortification_map, warehouse_map
|
from dcs.statics import Fortification, fortification_map, warehouse_map
|
||||||
from dcs.task import (
|
from dcs.task import (
|
||||||
ActivateBeaconCommand,
|
ActivateBeaconCommand,
|
||||||
@@ -26,12 +37,12 @@ from dcs.task import (
|
|||||||
from dcs.triggers import TriggerStart, TriggerZone
|
from dcs.triggers import TriggerStart, TriggerZone
|
||||||
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
|
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
|
||||||
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
|
||||||
from dcs.unittype import StaticType, UnitType
|
from dcs.unittype import StaticType, UnitType, ShipType, VehicleType
|
||||||
from dcs.vehicles import vehicle_map
|
from dcs.vehicles import vehicle_map
|
||||||
|
|
||||||
from game import db
|
from game import db
|
||||||
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
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 import ControlPoint, TheaterGroundObject
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
BuildingGroundObject,
|
BuildingGroundObject,
|
||||||
@@ -56,7 +67,10 @@ FARP_FRONTLINE_DISTANCE = 10000
|
|||||||
AA_CP_MIN_DISTANCE = 40000
|
AA_CP_MIN_DISTANCE = 40000
|
||||||
|
|
||||||
|
|
||||||
class GenericGroundObjectGenerator:
|
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
|
||||||
|
|
||||||
|
|
||||||
|
class GenericGroundObjectGenerator(Generic[TgoT]):
|
||||||
"""An unspecialized ground object generator.
|
"""An unspecialized ground object generator.
|
||||||
|
|
||||||
Currently used only for SAM
|
Currently used only for SAM
|
||||||
@@ -64,7 +78,7 @@ class GenericGroundObjectGenerator:
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
ground_object: TheaterGroundObject,
|
ground_object: TgoT,
|
||||||
country: Country,
|
country: Country,
|
||||||
game: Game,
|
game: Game,
|
||||||
mission: Mission,
|
mission: Mission,
|
||||||
@@ -89,10 +103,7 @@ class GenericGroundObjectGenerator:
|
|||||||
logging.warning(f"Found empty group in {self.ground_object}")
|
logging.warning(f"Found empty group in {self.ground_object}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
unit_type = unit_type_from_name(group.units[0].type)
|
unit_type = vehicle_type_from_name(group.units[0].type)
|
||||||
if unit_type is None:
|
|
||||||
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
|
|
||||||
|
|
||||||
vg = self.m.vehicle_group(
|
vg = self.m.vehicle_group(
|
||||||
self.country,
|
self.country,
|
||||||
group.name,
|
group.name,
|
||||||
@@ -116,24 +127,27 @@ class GenericGroundObjectGenerator:
|
|||||||
self._register_unit_group(group, vg)
|
self._register_unit_group(group, vg)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
|
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
||||||
if hasattr(unit_type, "eplrs"):
|
if unit_type.eplrs:
|
||||||
if unit_type.eplrs:
|
group.points[0].tasks.append(EPLRS(group.id))
|
||||||
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:
|
if self.game.settings.perf_red_alert_state:
|
||||||
group.points[0].tasks.append(OptAlarmState(2))
|
group.points[0].tasks.append(OptAlarmState(2))
|
||||||
else:
|
else:
|
||||||
group.points[0].tasks.append(OptAlarmState(1))
|
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.unit_map.add_ground_object_units(
|
||||||
self.ground_object, persistence_group, miz_group
|
self.ground_object, persistence_group, miz_group
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MissileSiteGenerator(GenericGroundObjectGenerator):
|
class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]):
|
||||||
@property
|
@property
|
||||||
def culled(self) -> bool:
|
def culled(self) -> bool:
|
||||||
# Don't cull missile sites - their range is long enough to make them easily
|
# 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:
|
for group in self.ground_object.groups:
|
||||||
vg = self.m.find_group(group.name)
|
vg = self.m.find_group(group.name)
|
||||||
if vg is not None:
|
if vg is not None:
|
||||||
targets = self.possible_missile_targets(vg)
|
targets = self.possible_missile_targets()
|
||||||
if targets:
|
if targets:
|
||||||
target = random.choice(targets)
|
target = random.choice(targets)
|
||||||
real_target = target.point_from_heading(
|
real_target = target.point_from_heading(
|
||||||
@@ -165,7 +179,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
|
|||||||
"Couldn't setup missile site to fire, group was not generated."
|
"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
|
Find enemy control points in range
|
||||||
:param vg: Vehicle group we are searching a target for (There is always only oe group right now)
|
: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] = []
|
targets: List[Point] = []
|
||||||
for cp in self.game.theater.controlpoints:
|
for cp in self.game.theater.controlpoints:
|
||||||
if cp.captured != self.ground_object.control_point.captured:
|
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:
|
if distance < self.missile_site_range:
|
||||||
targets.append(cp.position)
|
targets.append(cp.position)
|
||||||
return targets
|
return targets
|
||||||
@@ -196,7 +210,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
|
|||||||
return site_range
|
return site_range
|
||||||
|
|
||||||
|
|
||||||
class BuildingSiteGenerator(GenericGroundObjectGenerator):
|
class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
|
||||||
"""Generator for building sites.
|
"""Generator for building sites.
|
||||||
|
|
||||||
Building sites are the primary type of non-airbase objective locations that
|
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"
|
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:
|
if not self.ground_object.is_dead:
|
||||||
group = self.m.vehicle_group(
|
group = self.m.vehicle_group(
|
||||||
country=self.country,
|
country=self.country,
|
||||||
@@ -324,7 +338,7 @@ class SceneryGenerator(BuildingSiteGenerator):
|
|||||||
self.unit_map.add_scenery(scenery)
|
self.unit_map.add_scenery(scenery)
|
||||||
|
|
||||||
|
|
||||||
class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundObject]):
|
||||||
"""Base type for carrier group generation.
|
"""Base type for carrier group generation.
|
||||||
|
|
||||||
Used by both CV(N) groups and LHA groups.
|
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.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
|
||||||
self._register_unit_group(group, ship_group)
|
self._register_unit_group(group, ship_group)
|
||||||
|
|
||||||
def get_carrier_type(self, group: Group) -> Type[UnitType]:
|
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
|
||||||
unit_type = unit_type_from_name(group.units[0].type)
|
return ship_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 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)
|
unit_type = self.get_carrier_type(group)
|
||||||
|
|
||||||
ship_group = self.m.ship_group(
|
ship_group = self.m.ship_group(
|
||||||
@@ -474,7 +487,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
|
|||||||
class CarrierGenerator(GenericCarrierGenerator):
|
class CarrierGenerator(GenericCarrierGenerator):
|
||||||
"""Generator for CV(N) groups."""
|
"""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)
|
unit_type = super().get_carrier_type(group)
|
||||||
if self.game.settings.supercarrier:
|
if self.game.settings.supercarrier:
|
||||||
unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
|
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."""
|
"""Generator for non-carrier naval groups."""
|
||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
@@ -529,14 +542,11 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
|
|||||||
if not group.units:
|
if not group.units:
|
||||||
logging.warning(f"Found empty group in {self.ground_object}")
|
logging.warning(f"Found empty group in {self.ground_object}")
|
||||||
continue
|
continue
|
||||||
|
self.generate_group(group, ship_type_from_name(group.units[0].type))
|
||||||
|
|
||||||
unit_type = unit_type_from_name(group.units[0].type)
|
def generate_group(
|
||||||
if unit_type is None:
|
self, group_def: ShipGroup, first_unit_type: Type[ShipType]
|
||||||
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
|
) -> None:
|
||||||
|
|
||||||
self.generate_group(group, unit_type)
|
|
||||||
|
|
||||||
def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
|
|
||||||
group = self.m.ship_group(
|
group = self.m.ship_group(
|
||||||
self.country,
|
self.country,
|
||||||
group_def.name,
|
group_def.name,
|
||||||
@@ -624,7 +634,7 @@ class GroundObjectsGenerator:
|
|||||||
self.icls_alloc = iter(range(1, 21))
|
self.icls_alloc = iter(range(1, 21))
|
||||||
self.runways: Dict[str, RunwayData] = {}
|
self.runways: Dict[str, RunwayData] = {}
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
for cp in self.game.theater.controlpoints:
|
for cp in self.game.theater.controlpoints:
|
||||||
if cp.captured:
|
if cp.captured:
|
||||||
country_name = self.game.player_country
|
country_name = self.game.player_country
|
||||||
@@ -637,6 +647,7 @@ class GroundObjectsGenerator:
|
|||||||
).generate()
|
).generate()
|
||||||
|
|
||||||
for ground_object in cp.ground_objects:
|
for ground_object in cp.ground_objects:
|
||||||
|
generator: GenericGroundObjectGenerator[Any]
|
||||||
if isinstance(ground_object, FactoryGroundObject):
|
if isinstance(ground_object, FactoryGroundObject):
|
||||||
generator = FactoryGenerator(
|
generator = FactoryGenerator(
|
||||||
ground_object, country, self.game, self.m, self.unit_map
|
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.data.alic import AlicCodes
|
||||||
from game.db import unit_type_from_name
|
from game.db import unit_type_from_name
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
|
from game.savecompat import has_save_compat_for
|
||||||
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
|
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
|
||||||
from game.theater.bullseye import Bullseye
|
from game.theater.bullseye import Bullseye
|
||||||
from game.utils import meters
|
from game.utils import meters
|
||||||
@@ -63,7 +64,8 @@ class KneeboardPageWriter:
|
|||||||
else:
|
else:
|
||||||
self.foreground_fill = (15, 15, 15)
|
self.foreground_fill = (15, 15, 15)
|
||||||
self.background_fill = (255, 252, 252)
|
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
|
# These font sizes create a relatively full page for current sorties. If
|
||||||
# we start generating more complicated flight plans, or start including
|
# we start generating more complicated flight plans, or start including
|
||||||
# more information in the comm ladder (the latter of which we should
|
# 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
|
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
|
||||||
)
|
)
|
||||||
self.draw = ImageDraw.Draw(self.image)
|
self.draw = ImageDraw.Draw(self.image)
|
||||||
|
self.page_margin = page_margin
|
||||||
self.x = page_margin
|
self.x = page_margin
|
||||||
self.y = page_margin
|
self.y = page_margin
|
||||||
self.line_spacing = line_spacing
|
self.line_spacing = line_spacing
|
||||||
@@ -91,10 +94,24 @@ class KneeboardPageWriter:
|
|||||||
return self.x, self.y
|
return self.x, self.y
|
||||||
|
|
||||||
def text(
|
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:
|
) -> None:
|
||||||
if font is None:
|
if font is None:
|
||||||
font = self.content_font
|
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)
|
self.draw.text(self.position, text, font=font, fill=fill)
|
||||||
width, height = self.draw.textsize(text, font=font)
|
width, height = self.draw.textsize(text, font=font)
|
||||||
@@ -134,6 +151,24 @@ class KneeboardPageWriter:
|
|||||||
output = combo
|
output = combo
|
||||||
return "".join(segments + [output]).strip()
|
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:
|
class KneeboardPage:
|
||||||
"""Base class for all kneeboard pages."""
|
"""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):
|
class KneeboardGenerator(MissionInfoGenerator):
|
||||||
"""Creates kneeboard pages for each client flight in the mission."""
|
"""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 StrikeTaskPage(flight, self.dark_kneeboard, self.game.theater)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@has_save_compat_for(4)
|
||||||
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
|
||||||
"""Returns a list of kneeboard pages for the given flight."""
|
"""Returns a list of kneeboard pages for the given flight."""
|
||||||
pages: List[KneeboardPage] = [
|
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:
|
if (target_page := self.generate_task_page(flight)) is not None:
|
||||||
pages.append(target_page)
|
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 logging
|
||||||
import random
|
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.scud_site import ScudGenerator
|
||||||
from gen.missiles.v1_group import V1GroupGenerator
|
from gen.missiles.v1_group import V1GroupGenerator
|
||||||
|
|
||||||
MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
|
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
|
This generate a missiles group
|
||||||
:return: Nothing, but put the group reference inside the ground object
|
: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 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):
|
class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
|
||||||
def __init__(self, game, ground_object, faction):
|
def __init__(
|
||||||
|
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
|
||||||
|
) -> None:
|
||||||
super(ScudGenerator, self).__init__(game, ground_object)
|
super(ScudGenerator, self).__init__(game, ground_object)
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
# Scuds
|
# Scuds
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ import random
|
|||||||
|
|
||||||
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
|
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):
|
class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
|
||||||
def __init__(self, game, ground_object, faction):
|
def __init__(
|
||||||
|
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
|
||||||
|
) -> None:
|
||||||
super(V1GroupGenerator, self).__init__(game, ground_object)
|
super(V1GroupGenerator, self).__init__(game, ground_object)
|
||||||
self.faction = faction
|
self.faction = faction
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
# Ramps
|
# Ramps
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
from typing import List
|
from typing import List, Any
|
||||||
|
|
||||||
from dcs.country import Country
|
from dcs.country import Country
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ class NameGenerator:
|
|||||||
existing_alphas: List[str] = []
|
existing_alphas: List[str] = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reset(cls):
|
def reset(cls) -> None:
|
||||||
cls.number = 0
|
cls.number = 0
|
||||||
cls.infantry_number = 0
|
cls.infantry_number = 0
|
||||||
cls.convoy_number = 0
|
cls.convoy_number = 0
|
||||||
@@ -265,7 +265,7 @@ class NameGenerator:
|
|||||||
cls.existing_alphas = []
|
cls.existing_alphas = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def reset_numbers(cls):
|
def reset_numbers(cls) -> None:
|
||||||
cls.number = 0
|
cls.number = 0
|
||||||
cls.infantry_number = 0
|
cls.infantry_number = 0
|
||||||
cls.aircraft_number = 0
|
cls.aircraft_number = 0
|
||||||
@@ -273,7 +273,9 @@ class NameGenerator:
|
|||||||
cls.cargo_ship_number = 0
|
cls.cargo_ship_number = 0
|
||||||
|
|
||||||
@classmethod
|
@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
|
cls.aircraft_number += 1
|
||||||
try:
|
try:
|
||||||
if flight.custom_name:
|
if flight.custom_name:
|
||||||
@@ -293,7 +295,9 @@ class NameGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@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
|
cls.number += 1
|
||||||
return "unit|{}|{}|{}|{}|".format(
|
return "unit|{}|{}|{}|{}|".format(
|
||||||
country.id, cls.number, parent_base_id, unit_type.name
|
country.id, cls.number, parent_base_id, unit_type.name
|
||||||
@@ -301,8 +305,8 @@ class NameGenerator:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def next_infantry_name(
|
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
|
cls.infantry_number += 1
|
||||||
return "infantry|{}|{}|{}|{}|".format(
|
return "infantry|{}|{}|{}|{}|".format(
|
||||||
country.id,
|
country.id,
|
||||||
@@ -312,17 +316,17 @@ class NameGenerator:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def next_awacs_name(cls, country: Country):
|
def next_awacs_name(cls, country: Country) -> str:
|
||||||
cls.number += 1
|
cls.number += 1
|
||||||
return "awacs|{}|{}|0|".format(country.id, cls.number)
|
return "awacs|{}|{}|0|".format(country.id, cls.number)
|
||||||
|
|
||||||
@classmethod
|
@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
|
cls.number += 1
|
||||||
return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
|
return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def next_carrier_name(cls, country: Country):
|
def next_carrier_name(cls, country: Country) -> str:
|
||||||
cls.number += 1
|
cls.number += 1
|
||||||
return "carrier|{}|{}|0|".format(country.id, cls.number)
|
return "carrier|{}|{}|0|".format(country.id, cls.number)
|
||||||
|
|
||||||
@@ -337,7 +341,7 @@ class NameGenerator:
|
|||||||
return f"Cargo Ship {cls.cargo_ship_number:03}"
|
return f"Cargo Ship {cls.cargo_ship_number:03}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def random_objective_name(cls):
|
def random_objective_name(cls) -> str:
|
||||||
if cls.animals:
|
if cls.animals:
|
||||||
animal = random.choice(cls.animals)
|
animal = random.choice(cls.animals)
|
||||||
cls.animals.remove(animal)
|
cls.animals.remove(animal)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class RadioFrequency:
|
|||||||
#: The frequency in kilohertz.
|
#: The frequency in kilohertz.
|
||||||
hertz: int
|
hertz: int
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
if self.hertz >= 1000000:
|
if self.hertz >= 1000000:
|
||||||
return self.format("MHz", 1000000)
|
return self.format("MHz", 1000000)
|
||||||
return self.format("kHz", 1000)
|
return self.format("kHz", 1000)
|
||||||
|
|||||||
@@ -14,25 +14,21 @@ class BoforsGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Bofors AAA"
|
name = "Bofors AAA"
|
||||||
price = 75
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
grid_x = random.randint(2, 3)
|
|
||||||
grid_y = random.randint(2, 3)
|
|
||||||
|
|
||||||
spacing = random.randint(10, 40)
|
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
for i in range(grid_x):
|
for i in range(4):
|
||||||
for j in range(grid_y):
|
spacing_x = random.randint(10, 40)
|
||||||
index = index + 1
|
spacing_y = random.randint(10, 40)
|
||||||
self.add_unit(
|
index = index + 1
|
||||||
AirDefence.Bofors40,
|
self.add_unit(
|
||||||
"AAA#" + str(index),
|
AirDefence.Bofors40,
|
||||||
self.position.x + spacing * i,
|
"AAA#" + str(index),
|
||||||
self.position.y + spacing * j,
|
self.position.x + spacing_x * i,
|
||||||
self.heading,
|
self.position.y + spacing_y * i,
|
||||||
)
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -23,31 +23,26 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Flak Site"
|
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
|
index = 0
|
||||||
mixed = random.choice([True, False])
|
mixed = random.choice([True, False])
|
||||||
unit_type = random.choice(GFLAK)
|
unit_type = random.choice(GFLAK)
|
||||||
|
|
||||||
for i in range(grid_x):
|
for i in range(4):
|
||||||
for j in range(grid_y):
|
index = index + 1
|
||||||
index = index + 1
|
spacing_x = random.randint(10, 40)
|
||||||
self.add_unit(
|
spacing_y = random.randint(10, 40)
|
||||||
unit_type,
|
self.add_unit(
|
||||||
"AAA#" + str(index),
|
unit_type,
|
||||||
self.position.x + spacing * i + random.randint(1, 5),
|
"AAA#" + str(index),
|
||||||
self.position.y + spacing * j + random.randint(1, 5),
|
self.position.x + spacing_x * i + random.randint(1, 5),
|
||||||
self.heading,
|
self.position.y + spacing_y * i + random.randint(1, 5),
|
||||||
)
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
if mixed:
|
if mixed:
|
||||||
unit_type = random.choice(GFLAK)
|
unit_type = random.choice(GFLAK)
|
||||||
|
|
||||||
# Search lights
|
# Search lights
|
||||||
search_pos = self.get_circular_position(random.randint(2, 3), 80)
|
search_pos = self.get_circular_position(random.randint(2, 3), 80)
|
||||||
@@ -86,8 +81,10 @@ class FlakGenerator(AirDefenseGroupGenerator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Some Opel Blitz trucks
|
# Some Opel Blitz trucks
|
||||||
for i in range(int(max(1, grid_x / 2))):
|
index = 0
|
||||||
for j in range(int(max(1, grid_x / 2))):
|
for i in range(int(max(1, 2))):
|
||||||
|
for j in range(int(max(1, 2))):
|
||||||
|
index += 1
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
Unarmed.Blitz_36_6700A,
|
Unarmed.Blitz_36_6700A,
|
||||||
"BLITZ#" + str(index),
|
"BLITZ#" + str(index),
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ class Flak18Generator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "WW2 Flak Site"
|
name = "WW2 Flak Site"
|
||||||
price = 40
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
spacing = random.randint(30, 60)
|
spacing = random.randint(30, 60)
|
||||||
index = 0
|
index = 0
|
||||||
|
|||||||
@@ -13,12 +13,8 @@ class KS19Generator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "KS-19 AAA Site"
|
name = "KS-19 AAA Site"
|
||||||
price = 98
|
|
||||||
|
|
||||||
def generate(self):
|
|
||||||
|
|
||||||
spacing = random.randint(10, 40)
|
|
||||||
|
|
||||||
|
def generate(self) -> None:
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
highdigitsams.AAA_SON_9_Fire_Can,
|
highdigitsams.AAA_SON_9_Fire_Can,
|
||||||
"TR",
|
"TR",
|
||||||
@@ -28,16 +24,17 @@ class KS19Generator(AirDefenseGroupGenerator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
index = 0
|
index = 0
|
||||||
for i in range(3):
|
for i in range(4):
|
||||||
for j in range(3):
|
spacing_x = random.randint(10, 40)
|
||||||
index = index + 1
|
spacing_y = random.randint(10, 40)
|
||||||
self.add_unit(
|
index = index + 1
|
||||||
highdigitsams.AAA_100mm_KS_19,
|
self.add_unit(
|
||||||
"AAA#" + str(index),
|
highdigitsams.AAA_100mm_KS_19,
|
||||||
self.position.x + spacing * i,
|
"AAA#" + str(index),
|
||||||
self.position.y + spacing * j,
|
self.position.x + spacing_x * i,
|
||||||
self.heading,
|
self.position.y + spacing_y * i,
|
||||||
)
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "WW2 Ally Flak Site"
|
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)
|
positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
|
||||||
for i, position in enumerate(positions):
|
for i, position in enumerate(positions):
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ class ZSU57Generator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "ZSU-57-2 Group"
|
name = "ZSU-57-2 Group"
|
||||||
price = 60
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
num_launchers = 5
|
num_launchers = 4
|
||||||
positions = self.get_circular_position(
|
positions = self.get_circular_position(
|
||||||
num_launchers, launcher_distance=110, coverage=360
|
num_launchers, launcher_distance=110, coverage=360
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,25 +14,20 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Zu-23 Site"
|
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
|
index = 0
|
||||||
for i in range(grid_x):
|
for i in range(4):
|
||||||
for j in range(grid_y):
|
index = index + 1
|
||||||
index = index + 1
|
spacing_x = random.randint(10, 40)
|
||||||
self.add_unit(
|
spacing_y = random.randint(10, 40)
|
||||||
AirDefence.ZU_23_Closed_Insurgent,
|
self.add_unit(
|
||||||
"AAA#" + str(index),
|
AirDefence.ZU_23_Closed_Insurgent,
|
||||||
self.position.x + spacing * i,
|
"AAA#" + str(index),
|
||||||
self.position.y + spacing * j,
|
self.position.x + spacing_x * i,
|
||||||
self.heading,
|
self.position.y + spacing_y * i,
|
||||||
)
|
self.heading,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
def range(cls) -> AirDefenseRange:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Iterator, List
|
from typing import Iterator, List
|
||||||
@@ -6,36 +8,69 @@ from dcs.unitgroup import VehicleGroup
|
|||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.theater.theatergroundobject import SamGroundObject
|
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):
|
class AirDefenseRange(Enum):
|
||||||
AAA = "AAA"
|
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
|
||||||
Short = "short"
|
Short = ("short", SkynetRole.NoSkynetBehavior)
|
||||||
Medium = "medium"
|
Medium = ("medium", SkynetRole.Sam)
|
||||||
Long = "long"
|
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
|
This is the base for all SAM group generators
|
||||||
"""
|
"""
|
||||||
|
|
||||||
price: int
|
|
||||||
|
|
||||||
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
|
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
|
||||||
ground_object.skynet_capable = True
|
|
||||||
super().__init__(game, ground_object)
|
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.auxiliary_groups: List[VehicleGroup] = []
|
||||||
|
self.heading = self.heading_to_conflict()
|
||||||
|
|
||||||
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
|
def add_auxiliary_group(self, role: SkynetRole) -> VehicleGroup:
|
||||||
group = VehicleGroup(
|
gid = self.game.next_group_id()
|
||||||
self.game.next_group_id(), "|".join([self.go.group_name, name_suffix])
|
group = VehicleGroup(gid, self.group_name_for_role(gid, role))
|
||||||
)
|
|
||||||
self.auxiliary_groups.append(group)
|
self.auxiliary_groups.append(group)
|
||||||
return 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:
|
def get_generated_group(self) -> VehicleGroup:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
|
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
|
||||||
@@ -52,3 +87,7 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def range(cls) -> AirDefenseRange:
|
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"
|
name = "Early Cold War Flak Site"
|
||||||
price = 74
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
spacing = random.randint(30, 60)
|
spacing = random.randint(30, 60)
|
||||||
index = 0
|
index = 0
|
||||||
@@ -90,9 +89,8 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Cold War Flak Site"
|
name = "Cold War Flak Site"
|
||||||
price = 72
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
|
|
||||||
spacing = random.randint(30, 60)
|
spacing = random.randint(30, 60)
|
||||||
index = 0
|
index = 0
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from gen.sam.ewrs import (
|
|||||||
StraightFlushGenerator,
|
StraightFlushGenerator,
|
||||||
TallRackGenerator,
|
TallRackGenerator,
|
||||||
EwrGenerator,
|
EwrGenerator,
|
||||||
|
TinShieldGenerator,
|
||||||
)
|
)
|
||||||
|
|
||||||
EWR_MAP = {
|
EWR_MAP = {
|
||||||
@@ -31,6 +32,7 @@ EWR_MAP = {
|
|||||||
"SnowDriftGenerator": SnowDriftGenerator,
|
"SnowDriftGenerator": SnowDriftGenerator,
|
||||||
"StraightFlushGenerator": StraightFlushGenerator,
|
"StraightFlushGenerator": StraightFlushGenerator,
|
||||||
"HawkEwrGenerator": HawkEwrGenerator,
|
"HawkEwrGenerator": HawkEwrGenerator,
|
||||||
|
"TinShieldGenerator": TinShieldGenerator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from dcs.vehicles import AirDefence
|
|
||||||
from dcs.unittype import VehicleType
|
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]
|
unit_type: Type[VehicleType]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def name(cls) -> str:
|
def name(cls) -> str:
|
||||||
return cls.unit_type.name
|
return cls.unit_type.name
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def price() -> int:
|
|
||||||
# TODO: Differentiate sites.
|
|
||||||
return 20
|
|
||||||
|
|
||||||
def generate(self) -> None:
|
def generate(self) -> None:
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
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
|
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"
|
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
|
# TODO : would be better with the Concrete structure that is supposed to protect it
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
|
|||||||
@@ -1,58 +1,116 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import operator
|
||||||
import random
|
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 import unitgroup
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.point import PointAction
|
from dcs.point import PointAction
|
||||||
from dcs.unit import Ship, Vehicle
|
from dcs.unit import Ship, Vehicle, Unit
|
||||||
from dcs.unittype import VehicleType
|
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.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:
|
if TYPE_CHECKING:
|
||||||
from game.game import Game
|
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.
|
# TODO: Generate a group description rather than a pydcs group.
|
||||||
# It appears that all of this work gets redone at miz generation time (see
|
# 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
|
# 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
|
# care about in the format we want if we just generate our own group description
|
||||||
# types rather than pydcs groups.
|
# types rather than pydcs groups.
|
||||||
class GroupGenerator:
|
class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]):
|
||||||
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
|
def __init__(self, game: Game, ground_object: TgoT, group: GroupT) -> None:
|
||||||
self.game = game
|
self.game = game
|
||||||
self.go = ground_object
|
self.go = ground_object
|
||||||
self.position = ground_object.position
|
self.position = ground_object.position
|
||||||
self.heading = random.randint(0, 359)
|
self.heading = random.randint(0, 359)
|
||||||
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
|
self.price = 0
|
||||||
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
|
self.vg: GroupT = group
|
||||||
wp.ETA_locked = True
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_generated_group(self) -> unitgroup.VehicleGroup:
|
def get_generated_group(self) -> GroupT:
|
||||||
return self.vg
|
return self.vg
|
||||||
|
|
||||||
def add_unit(
|
def add_unit(
|
||||||
self,
|
self,
|
||||||
unit_type: Type[VehicleType],
|
unit_type: UnitTypeT,
|
||||||
name: str,
|
name: str,
|
||||||
pos_x: float,
|
pos_x: float,
|
||||||
pos_y: float,
|
pos_y: float,
|
||||||
heading: int,
|
heading: int,
|
||||||
) -> Vehicle:
|
) -> UnitT:
|
||||||
return self.add_unit_to_group(
|
return self.add_unit_to_group(
|
||||||
self.vg, unit_type, name, Point(pos_x, pos_y), heading
|
self.vg, unit_type, name, Point(pos_x, pos_y), heading
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_unit_to_group(
|
def add_unit_to_group(
|
||||||
self,
|
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],
|
unit_type: Type[VehicleType],
|
||||||
name: str,
|
name: str,
|
||||||
position: Point,
|
position: Point,
|
||||||
@@ -62,9 +120,19 @@ class GroupGenerator:
|
|||||||
unit.position = position
|
unit.position = position
|
||||||
unit.heading = heading
|
unit.heading = heading
|
||||||
group.add_unit(unit)
|
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
|
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
|
Given a position on the map, array a group of units in a circle a uniform distance from the unit
|
||||||
:param num_units:
|
:param num_units:
|
||||||
@@ -90,39 +158,47 @@ class GroupGenerator:
|
|||||||
else:
|
else:
|
||||||
current_offset = self.heading
|
current_offset = self.heading
|
||||||
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
|
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
|
||||||
for x in range(1, num_units + 1):
|
for _ in range(1, num_units + 1):
|
||||||
positions.append(
|
x: float = self.position.x + launcher_distance * math.cos(
|
||||||
(
|
math.radians(current_offset)
|
||||||
self.position.x
|
|
||||||
+ launcher_distance * math.cos(math.radians(current_offset)),
|
|
||||||
self.position.y
|
|
||||||
+ launcher_distance * math.sin(math.radians(current_offset)),
|
|
||||||
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
|
current_offset += outer_offset
|
||||||
return positions
|
return positions
|
||||||
|
|
||||||
|
|
||||||
class ShipGroupGenerator(GroupGenerator):
|
class ShipGroupGenerator(
|
||||||
|
GroupGenerator[ShipGroup, Ship, Type[ShipType], NavalGroundObject]
|
||||||
|
):
|
||||||
"""Abstract class for other ship generator classes"""
|
"""Abstract class for other ship generator classes"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, game: Game, ground_object: NavalGroundObject, faction: Faction):
|
||||||
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
|
super().__init__(
|
||||||
):
|
game,
|
||||||
self.game = game
|
ground_object,
|
||||||
self.go = ground_object
|
unitgroup.ShipGroup(game.next_group_id(), ground_object.group_name),
|
||||||
self.position = ground_object.position
|
)
|
||||||
self.heading = random.randint(0, 359)
|
|
||||||
self.faction = faction
|
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 = self.vg.add_waypoint(self.position, 0)
|
||||||
wp.ETA_locked = True
|
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 = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
|
||||||
unit.position.x = pos_x
|
unit.position = position
|
||||||
unit.position.y = pos_y
|
|
||||||
unit.heading = heading
|
unit.heading = heading
|
||||||
self.vg.add_unit(unit)
|
group.add_unit(unit)
|
||||||
return unit
|
return unit
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ class AvengerGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Avenger Group"
|
name = "Avenger Group"
|
||||||
price = 62
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
num_launchers = random.randint(2, 3)
|
num_launchers = 2
|
||||||
|
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
Unarmed.M_818,
|
Unarmed.M_818,
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Chaparral Group"
|
name = "Chaparral Group"
|
||||||
price = 66
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
num_launchers = random.randint(2, 4)
|
num_launchers = 2
|
||||||
|
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
Unarmed.M_818,
|
Unarmed.M_818,
|
||||||
|
|||||||
@@ -14,23 +14,20 @@ class GepardGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Gepard Group"
|
name = "Gepard Group"
|
||||||
price = 50
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
self.add_unit(
|
num_launchers = 2
|
||||||
AirDefence.Gepard,
|
|
||||||
"SPAAA",
|
positions = self.get_circular_position(
|
||||||
self.position.x,
|
num_launchers, launcher_distance=120, coverage=180
|
||||||
self.position.y,
|
|
||||||
self.heading,
|
|
||||||
)
|
)
|
||||||
if random.randint(0, 1) == 1:
|
for i, position in enumerate(positions):
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
AirDefence.Gepard,
|
AirDefence.Gepard,
|
||||||
"SPAAA2",
|
"SPAA#" + str(i),
|
||||||
self.position.x,
|
position[0],
|
||||||
self.position.y,
|
position[1],
|
||||||
self.heading,
|
position[2],
|
||||||
)
|
)
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
Unarmed.M_818,
|
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_hawk import HawkGenerator
|
||||||
from gen.sam.sam_hq7 import HQ7Generator
|
from gen.sam.sam_hq7 import HQ7Generator
|
||||||
from gen.sam.sam_linebacker import LinebackerGenerator
|
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_patriot import PatriotGenerator
|
||||||
from gen.sam.sam_rapier import RapierGenerator
|
from gen.sam.sam_rapier import RapierGenerator
|
||||||
from gen.sam.sam_roland import RolandGenerator
|
from gen.sam.sam_roland import RolandGenerator
|
||||||
@@ -100,6 +101,8 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
|
|||||||
"SA20Generator": SA20Generator,
|
"SA20Generator": SA20Generator,
|
||||||
"SA20BGenerator": SA20BGenerator,
|
"SA20BGenerator": SA20BGenerator,
|
||||||
"SA23Generator": SA23Generator,
|
"SA23Generator": SA23Generator,
|
||||||
|
"NasamBGenerator": NasamBGenerator,
|
||||||
|
"NasamCGenerator": NasamCGenerator,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from dcs.vehicles import AirDefence
|
|||||||
from gen.sam.airdefensegroupgenerator import (
|
from gen.sam.airdefensegroupgenerator import (
|
||||||
AirDefenseRange,
|
AirDefenseRange,
|
||||||
AirDefenseGroupGenerator,
|
AirDefenseGroupGenerator,
|
||||||
|
SkynetRole,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -15,9 +16,8 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Hawk Site"
|
name = "Hawk Site"
|
||||||
price = 115
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
AirDefence.Hawk_sr,
|
AirDefence.Hawk_sr,
|
||||||
"SR",
|
"SR",
|
||||||
@@ -41,7 +41,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Triple A for close range defense
|
# 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(
|
self.add_unit_to_group(
|
||||||
aa_group,
|
aa_group,
|
||||||
AirDefence.Vulcan,
|
AirDefence.Vulcan,
|
||||||
@@ -50,7 +50,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
|
|||||||
self.heading,
|
self.heading,
|
||||||
)
|
)
|
||||||
|
|
||||||
num_launchers = random.randint(3, 6)
|
num_launchers = 6
|
||||||
positions = self.get_circular_position(
|
positions = self.get_circular_position(
|
||||||
num_launchers, launcher_distance=120, coverage=180
|
num_launchers, launcher_distance=120, coverage=180
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from dcs.vehicles import AirDefence
|
|||||||
from gen.sam.airdefensegroupgenerator import (
|
from gen.sam.airdefensegroupgenerator import (
|
||||||
AirDefenseRange,
|
AirDefenseRange,
|
||||||
AirDefenseGroupGenerator,
|
AirDefenseGroupGenerator,
|
||||||
|
SkynetRole,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -15,9 +16,8 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "HQ-7 Site"
|
name = "HQ-7 Site"
|
||||||
price = 120
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
AirDefence.HQ_7_STR_SP,
|
AirDefence.HQ_7_STR_SP,
|
||||||
"STR",
|
"STR",
|
||||||
@@ -25,16 +25,9 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
|||||||
self.position.y,
|
self.position.y,
|
||||||
self.heading,
|
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
|
# 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(
|
self.add_unit_to_group(
|
||||||
aa_group,
|
aa_group,
|
||||||
AirDefence.Ural_375_ZU_23,
|
AirDefence.Ural_375_ZU_23,
|
||||||
@@ -50,7 +43,7 @@ class HQ7Generator(AirDefenseGroupGenerator):
|
|||||||
self.heading,
|
self.heading,
|
||||||
)
|
)
|
||||||
|
|
||||||
num_launchers = random.randint(0, 3)
|
num_launchers = 2
|
||||||
if num_launchers > 0:
|
if num_launchers > 0:
|
||||||
positions = self.get_circular_position(
|
positions = self.get_circular_position(
|
||||||
num_launchers, launcher_distance=120, coverage=360
|
num_launchers, launcher_distance=120, coverage=360
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = "Linebacker Group"
|
name = "Linebacker Group"
|
||||||
price = 75
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self) -> None:
|
||||||
num_launchers = random.randint(2, 4)
|
num_launchers = 2
|
||||||
|
|
||||||
self.add_unit(
|
self.add_unit(
|
||||||
Unarmed.M_818,
|
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