mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Refactor Templates to Layouts, Review and Cleanup
- Fix tgogenerator - Fix UI for ForceGroup and Layouts - Fix ammo depot handling - Split bigger files in smaller meaningful files (TGO, layouts, forces) - Renamed Template to Layout - Renamed GroundGroup to TheaterGroup and GroundUnit to TheaterUnit - Reorganize Layouts and UnitGroups to a ArmedForces class and ForceGroup similar to the AirWing and Squadron - Reworded the UnitClass, GroupRole, GroupTask (adopted to PEP8) and reworked the connection from Role and Task - added comments - added missing unit classes - added temp workaround for missing classes - add repariable property to TheaterUnit - Review and Cleanup Added serialization for loaded templates Loading the templates from the .miz files takes a lot of computation time and in the future there will be more templates added to the system. Therefore a local pickle serialization for the loaded templates was re-added: - The pickle will be created the first time the TemplateLoader will be accessed - Pickle is stored in Liberation SaveDir - Added UI option to (re-)import templates
This commit is contained in:
parent
1ae6503ceb
commit
2c17a9a52e
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
@ -1,8 +1,17 @@
|
|||||||
# The Template System
|
# ArmedForces and the Layout System
|
||||||
|
|
||||||
The Template System is a complete rework of the generator-based logic to build theater-ground-objects (Liberation Objects).
|
Armed Forces and the Layout System is a complete rework of the generator-based logic to build theater-ground-objects (Liberation Objects which group ground units).
|
||||||
In the original system the generator was written in python and generated a group with a defined and static logic, written in code.
|
This will change underlying parts of the code base which will allow major improvements to the Ground Warfare in upcoming features.
|
||||||
The template sytem will now decouple the alignment / positioning from units and the definition of theire actual type (like Ural-375).
|
|
||||||
|
**Armed Forces**\
|
||||||
|
TODO Describe the introduction of ArmedForces which are similar to the AirWing and Squadrons.
|
||||||
|
The armed forces of each coalition contain multiple ForceGroups. A ForceGroup is a logical set of units (Vehicles, Ships, Statics) and corresponding Layouts for these units.
|
||||||
|
|
||||||
|
TODO Picture / Example to describe what it is... for example with Hawk Battery or S-300 Battery
|
||||||
|
|
||||||
|
**The Layout System**\
|
||||||
|
In the previous system the generator was written in python and generated a group with a defined and static logic, written in code.
|
||||||
|
The layout sytem will now decouple the alignment / positioning from units and the definition of theire actual type (like Ural-375).
|
||||||
The template system allows to define the layout and set which unit types or classes (All logistic units for example) are able to fit into the template.
|
The template system allows to define the layout and set which unit types or classes (All logistic units for example) are able to fit into the template.
|
||||||
Ultimately this will allow to have generalized templates which can be reused by multiple types of units. Best example is the definition of a SAM layout.
|
Ultimately this will allow to have generalized templates which can be reused by multiple types of units. Best example is the definition of a SAM layout.
|
||||||
Previously we had a generator for every different SAM Site, now we can just reuse the alignemnt (e.g. 6 Launchers in a circle alignment) with more generalization.
|
Previously we had a generator for every different SAM Site, now we can just reuse the alignemnt (e.g. 6 Launchers in a circle alignment) with more generalization.
|
||||||
@ -11,14 +20,15 @@ This also allows Users and Designers to easily create or modify templates as the
|
|||||||
In total the new system reduces the complexity and allows to precisely align / orient units as needed and create realistic looking ground units.
|
In total the new system reduces the complexity and allows to precisely align / orient units as needed and create realistic looking ground units.
|
||||||
As the whole ground unit generation and handling was reworked it is now also possible to add static units to a ground object, so even Fortifcation or similar can be added to templates in the future.
|
As the whole ground unit generation and handling was reworked it is now also possible to add static units to a ground object, so even Fortifcation or similar can be added to templates in the future.
|
||||||
|
|
||||||
|
|
||||||
## General Concept
|
## General Concept
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
TODO: Describe the general flow of the Template system
|
TODO: Describe the general flow of the Template system
|
||||||
|
|
||||||
|
TODO: Describe the serialization (Developer Tools: Import Templates)
|
||||||
|
|
||||||
TODO Lifecycle:
|
TODO Lifecycle:
|
||||||
The template will be automatically validated on campaign generation against the player and enemy factions.
|
The template will be automatically validated on campaign generation against the player and enemy factions.
|
||||||
If the factions support the template (based on the unit_types and unit_classes) then it will be added to the game.
|
If the factions support the template (based on the unit_types and unit_classes) then it will be added to the game.
|
||||||
@ -26,13 +36,15 @@ If a faction does not support a group from the template it will be removed if op
|
|||||||
During campaign initialization the start_generator will request unit_groups for the preset locations defined by the campaign designer. The faction will then offer possible groups and the matching template.
|
During campaign initialization the start_generator will request unit_groups for the preset locations defined by the campaign designer. The faction will then offer possible groups and the matching template.
|
||||||
The Liberation Group (TheaterGroundObject) is then being generated from this UnitGroup.
|
The Liberation Group (TheaterGroundObject) is then being generated from this UnitGroup.
|
||||||
|
|
||||||
- GroundWar currently does **not** use the template system
|
- GroundWar (Frontline) currently does **not** use the template system
|
||||||
- User can buy new SAM or ArmorGroup using this template system
|
- User can buy new SAM or ArmorGroup using this template system
|
||||||
|
- Campaign Designers can also define precicsly (if needed) which template or UnitGroup should be placed at a specific location by using TriggerZones with custom properties
|
||||||
|
|
||||||
Example for a customized Ground Object Buy Menu which makes use of Templates and UnitGroups:
|
Example for a customized Ground Object Buy Menu which makes use of Templates and UnitGroups:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
### The template miz
|
### The template miz
|
||||||
|
|
||||||
*Important*: Every unit_type has to be in a separate Group for the template to work.
|
*Important*: Every unit_type has to be in a separate Group for the template to work.
|
||||||
@ -44,7 +56,7 @@ Unit Count per group has to be the amount set with the unit_count property.
|
|||||||
|
|
||||||
During template generation the system will go through all possible units and will assign the respective unit_type to the units up to the maximum allow unit_count from the mapping.
|
During template generation the system will go through all possible units and will assign the respective unit_type to the units up to the maximum allow unit_count from the mapping.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### The template yaml
|
### The template yaml
|
||||||
|
|
||||||
@ -102,9 +114,8 @@ template_file: resources/templates/anti_air/AAA.miz
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Roles, Tasks and Classes
|
### Roles, Tasks and Classes
|
||||||
TODO Improve Naming? Same logic as with the Squadrons.. Also brainstorm if we should rename the UnitGroup which basicly is the equivalent of a "Package"
|
|
||||||
|
|
||||||
Role and Tasking
|
TODO Describe Role, Tasking and Classes
|
||||||
|
|
||||||
[GroupRole and GroupTask](/game/data/groups.py)
|
[GroupRole and GroupTask](/game/data/groups.py)
|
||||||
|
|
||||||
@ -112,12 +123,28 @@ Role and Tasking
|
|||||||
|
|
||||||
## How to add / modify a template
|
## How to add / modify a template
|
||||||
|
|
||||||
template.miz (positioning / alignment) and template.yaml (Mapping)
|
A template consists of two special files:
|
||||||
|
|
||||||
Best practice:
|
- template.miz which defines the actual positioning and alignment of the groups / units
|
||||||
- Copy existing Template and rename the files
|
- template.yaml which defines the necessary information like amount of units, possible types or classes.
|
||||||
- Adjust the .miz and change the group names accordingly
|
|
||||||
- Adjust the .yaml file to the needs and check for the correct group names
|
To add a new template a new yaml has to be created as every yaml can only define exact one template. Best practice is to copy paste an existing template which is similar to the one to be created as starting point. The next step is to create a new .miz file and align Units and statics to the wishes. Even if existing ones can be reused, best practice is to always create a fresh one to prevent side effects.
|
||||||
|
The most important part is to use a new Group for every different Unit Type. It is not possible to mix Unit Types in one group within a template. For example it is not possible to have a logistic truck and a AAA in the same group. The miz file will only be used to get the exact position and orientation of the units, therefore it is irrelevant which type of unit will be used. The unit type will be later defined inside the yaml file.
|
||||||
|
For the next step all Group names have to be added to the yaml file. Take care to that these names match exactly! Assign the unit_types or unit_classes properties to math the needs.
|
||||||
|
|
||||||
|
TODO Improve this with images and more detailed description
|
||||||
|
|
||||||
|
**IMPORTANT**: Due to performance increase the templates get serialized to a pickle file in the save dir as `templates.p`. When templates were modified a manual re-import of all templates has to be triggered.
|
||||||
|
This can be done by either deleting this file or using the Liberation UI. There is a special option in the ToolBar under Tools: Import Templates.
|
||||||
|
|
||||||
|
|
||||||
|
## Import Layouts into Liberation
|
||||||
|
TODO Describe the serialization and import.
|
||||||
|
|
||||||
|
For performance improvements all layouts are serialized to a so called pickle file. Every time changes are made to the layouts this file has to be recreated.
|
||||||
|
It will also be recreated after each Liberation update as it will check the Version Number and recreate it when changes are recognized.
|
||||||
|
|
||||||
|
This file is stored in the save folder
|
||||||
|
|
||||||
|
|
||||||
## Migration from Generators
|
## Migration from Generators
|
||||||
@ -125,9 +152,9 @@ Best practice:
|
|||||||
- All generators removed and migrated to templates
|
- All generators removed and migrated to templates
|
||||||
- These templates will in the next step be generalized
|
- These templates will in the next step be generalized
|
||||||
|
|
||||||
TODO: Update the template_list.md with the changes in Role/Tasking
|
The previous generators were migrated using a script which build a group using the generator. All of these groups were save into one .miz file [original_generator_layouts.miz](/resources/layouts/original_generator_layouts.miz).
|
||||||
|
This miz file can be used to verify the templates and to generalize similar templates to decouple the layout from the actual units. As this is a time-consuming and sphisticated task this will be done over time.
|
||||||
[List of supported templates](template_list.md)
|
With the first step the technical requirements will be fulfilled so that the generalization can happen afterwards the technical pr gets merged.
|
||||||
|
|
||||||
|
|
||||||
### Updates for Factions
|
### Updates for Factions
|
||||||
@ -138,65 +165,53 @@ During migration all default factions were automatically updated, so they will w
|
|||||||
What was changed:
|
What was changed:
|
||||||
- Removed the `ewrs` list. All EWRs are now defined in the list "air_defense_units".
|
- Removed the `ewrs` list. All EWRs are now defined in the list "air_defense_units".
|
||||||
- Added the `air_defense_units` list. All units with the Role AntiAir can be defined here as [GroundUnitType](/game/dcs/groundunittype.py). All possible units are defined in [/resources/units/ground_units](/resources/units/ground_units)
|
- Added the `air_defense_units` list. All units with the Role AntiAir can be defined here as [GroundUnitType](/game/dcs/groundunittype.py). All possible units are defined in [/resources/units/ground_units](/resources/units/ground_units)
|
||||||
- Added `preset_groups`. This list allows to define Preset Groups (described above) like SAM Systems consisting of Launcher, SR, TR and so on instead of adding them each to "air_defense_units". The presets are defined in [/resources/units/unit_groups](/resources/units/unit_groups)
|
- Added `preset_groups`. This list allows to define Preset Groups (described above) like SAM Systems consisting of Launcher, SR, TR and so on instead of adding them each to "air_defense_units". The presets are defined in [/resources/units/unit_groups](/resources/units/groups)
|
||||||
- Migrated `air_defenses` to air_defense_units and preset_sets.
|
- Migrated `air_defenses` to air_defense_units and preset_sets.
|
||||||
- `Missiles` are migrated to GroundUnitTypes instead of Generator names (see air_defense_units for how to use)
|
- `Missiles` are migrated to GroundUnitTypes instead of Generator names (see air_defense_units for how to use)
|
||||||
- Removed `cruisers`, `destroyers` and `naval_generators`. Migrated them to naval_units and preset_groups
|
- Removed `cruisers`, `destroyers` and `naval_generators`. Migrated them to naval_units and preset_groups
|
||||||
- added `naval_units` with the correct ship name found here [/resources/units/ships](/resources/units/ships)
|
- added `naval_units` with the correct ship name found here [/resources/units/ships](/resources/units/ships)
|
||||||
- `aircraft_carrier` and `helicopter_carrier` were moved to `naval_units` as well.
|
- `aircraft_carrier` and `helicopter_carrier` were moved to `naval_units` as well.
|
||||||
|
|
||||||
Possible Preset Groups:
|
|
||||||
|
|
||||||
TODO generate list with old generator name
|
|
||||||
|
|
||||||
Possible EWRs:
|
|
||||||
|
|
||||||
| Name in Faction file |
|
|
||||||
|------------------------------------------------------|
|
|
||||||
| EWR 1L13 |
|
|
||||||
| EWR 55G6 |
|
|
||||||
| MCC-SR Sborka "Dog Ear" SR |
|
|
||||||
| SAM Roland EWR |
|
|
||||||
| SAM P19 "Flat Face" SR (SA-2/3) |
|
|
||||||
| SAM Patriot STR |
|
|
||||||
| SAM SA-10 S-300 "Grumble" Big Bird SR |
|
|
||||||
| SAM SA-11 Buk "Gadfly" Snow Drift SR |
|
|
||||||
| SAM SA-6 Kub "Straight Flush" STR |
|
|
||||||
| SAM Hawk SR (AN/MPQ-50) |
|
|
||||||
| SAM SA-5 S-200 ST-68U "Tin Shield" SR |
|
|
||||||
|
|
||||||
## Unit Groups
|
## Unit Groups
|
||||||
|
|
||||||
|
TODO Explain more
|
||||||
|
|
||||||
- Sum up groups of different units which are used together (like a sam site).
|
- Sum up groups of different units which are used together (like a sam site).
|
||||||
- UnitGroup allows to define this logical group and add this to the faction file.
|
- UnitGroup allows to define this logical group and add this to the faction file.
|
||||||
- UnitGroups can have preferred templates
|
- UnitGroups can have preferred templates
|
||||||
|
|
||||||
# Open Points
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
name: SA-10/S-300PS # The name which will be used in the faction file
|
||||||
|
role: AntiAir # The role of the Group
|
||||||
|
tasks:
|
||||||
|
- LORAD # The task the Group can fulfill
|
||||||
|
ground_units:
|
||||||
|
- SAM SA-10 S-300 "Grumble" Clam Shell SR
|
||||||
|
- SAM SA-10 S-300 "Grumble" Big Bird SR
|
||||||
|
- SAM SA-10 S-300 "Grumble" C2
|
||||||
|
- SAM SA-10 S-300 "Grumble" Flap Lid TR
|
||||||
|
- SAM SA-10 S-300 "Grumble" TEL D
|
||||||
|
- SAM SA-10 S-300 "Grumble" TEL C
|
||||||
|
ship_units:
|
||||||
|
- # Add some naval units here
|
||||||
|
statics:
|
||||||
|
- # Add some statics here
|
||||||
|
templates:
|
||||||
|
- S-300 Site # The template names which should be used by this group
|
||||||
|
```
|
||||||
|
|
||||||
|
A list of all available units is accessible here: [/resources/units/unit_groups](/resources/units/groups)
|
||||||
|
|
||||||
|
|
||||||
|
### Optional Tasks which can be done later
|
||||||
|
- [ ] Complex Presets which allow campaign designer to specify the exact forcegroup or layout which should be used.
|
||||||
|
- [ ] Generalize all layouts (Like MERAD or SHORAD templates)
|
||||||
|
- [ ] Reuse the layouts for the frontline
|
||||||
|
- [ ] Add UI Implementation to choose which templates should be used during new game wizard (like AirWing Config)
|
||||||
|
- [ ] Rework "Names_By_Category" to just use the Tasking instead of a string.
|
||||||
|
- [ ] Add remaining missing classes to the units which currently dont have a class
|
||||||
|
|
||||||
|
|
||||||
- [x] Rework SAM Systems to unit_groups: migrated all `air_defense`
|
|
||||||
- [ ] Review Naming of all classes and so on
|
|
||||||
- [X] Improve UI Display Name for the new ShipUniType
|
|
||||||
- [ ] Verify if the campaign will be generated the same as before
|
|
||||||
- [x] Fix the generation of buildings
|
|
||||||
- [ ] Verify the handling of buildings (ammo and factory for example work different.)
|
|
||||||
- [X] Correct Missiles and Coastal (Counts?)
|
|
||||||
- [X] Faction Group Count (Navy, Missile, Coastal).. used for?? For nothing! removed.
|
|
||||||
- [x] Navy Generators -> Destroyers, Cruisers und Preset_groups Migration
|
|
||||||
- [x] Fix & Generalize Navy Templates (wrong unit number)
|
|
||||||
- [X] Fix Bug: Enemy Navy PLanes spawn at blue Carrier.
|
|
||||||
- [x] Add Naming for all AircraftCarrier and HelicopterCarrier (whats with the SC updated ones?)
|
|
||||||
- [X] Verfiy SuperCarrier Upgrade
|
|
||||||
- [X] Special Handling for Carrier Strike Group 8 (was not even working before..) can now be added as preset group
|
|
||||||
- [X] Validate that all Waypoints and taskings are correct and working (some use the group.name)
|
|
||||||
- [ ] Verify that DEAD Flights are untouched from these changes.. They are missing a attack command
|
|
||||||
- [ ] Finish Documentation
|
|
||||||
- [ ] Naming of created groups -> It uses the template name currently which is not great
|
|
||||||
- [x] Fix the Faction overview site not showing the newly added preset_groups and AA Units
|
|
||||||
- [ ] Add missing classes to the units so that they can be used by the templates
|
|
||||||
- [x] Replace BuyUi implementation from Template to "Preset" by using the available UnitGroups
|
|
||||||
- [x] Fix Buy menu for Armor Groups.
|
|
||||||
- [x] Fix Buy menu not allowing to change the amount when only one unit_type is available.
|
|
||||||
- [x] Fix Group order is not correct (group2 is 0, group1 is 1 if dcs_group id for group2 is smaller)
|
|
||||||
- [ ] Add generalized Templates for Lorad and Merad
|
|
||||||
- [ ] Generalize all Templates
|
|
||||||
|
|
||||||
BIN
doc/layouts/layouts.png
Normal file
BIN
doc/layouts/layouts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
56
doc/templates/template_list.md
vendored
56
doc/templates/template_list.md
vendored
@ -1,56 +0,0 @@
|
|||||||
| Role | Tasks | Template Name | Units |
|
|
||||||
|---------------|------------------------------|----------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| AntiAir | AAA | AAA Site | <ul><li>Classes = [ AAA]</li><li>Classes = [ Logistics]</li></ul> |
|
|
||||||
| AntiAir | AAA | AAA Mobile | <ul><li>Classes = [ AAA]</li><li>Classes = [ Logistics]</li></ul> |
|
|
||||||
| AntiAir | AAA | AAA Radar Site | <ul><li>Classes = [ SearchRadar]</li><li>Classes = [ AAA]</li><li>Classes = [ Logistics]</li></ul> |
|
|
||||||
| AntiAir | AAA | Cold War Flak Site | <ul><li>Classes = [ SearchRadar]</li><li>8.8 cm Flak 18</li><li>S-60 57mm</li><li>Classes = [ AAA]</li><li>Classes = [ Logistics]</li></ul> |
|
|
||||||
| AntiAir | AAA | Flak Site | <ul><li>2 cm Flakvierling 38, 8.8 cm Flak 18, 8.8 cm Flak 36, 8.8 cm Flak 37, 8.8 cm Flak 41, 2 cm Flak 38</li><li>2 cm Flakvierling 38</li><li>8.8 cm Flak 36</li><li>SL Flakscheinwerfer 37</li><li>PU Maschinensatz_33</li><li>AAA SP Kdo.G.40</li><li>LUV Kubelwagen 82</li><li>Truck Opel Blitz</li></ul> |
|
|
||||||
| AntiAir | AAA | WW2 Flak Site | <ul><li>8.8 cm Flak 18</li><li>Truck Opel Blitz</li></ul> |
|
|
||||||
| AntiAir | AAA | WW2 Ally Flak Site | <ul><li>QF 3.7-inch AA Gun</li><li>M1 37mm Gun</li><li>M45 Quadmount</li><li>Willys Jeep</li><li>M30 Cargo Carrier</li><li>M4 High-Speed Tractor</li><li>Truck Bedford</li></ul> |
|
|
||||||
| AntiAir | MERAD | Hawk Site | <ul><li>SAM Hawk SR (AN/MPQ-50)</li><li>SAM Hawk Platoon Command Post (PCP)</li><li>SAM Hawk TR (AN/MPQ-46)</li><li>SAM Hawk LN M192</li><li>M163 Vulcan Air Defense System</li></ul> |
|
|
||||||
| AntiAir | MERAD | SA-2/S-75 Site | <ul><li>SAM P19 "Flat Face" SR (SA-2/3)</li><li>SAM SA-2 S-75 "Fan Song" TR</li><li>SAM SA-2 S-75 "Guideline" LN</li></ul> |
|
|
||||||
| AntiAir | MERAD | SA-3/S-125 Site | <ul><li>SAM P19 "Flat Face" SR (SA-2/3)</li><li>SAM SA-3 S-125 "Low Blow" TR</li><li>SAM SA-3 S-125 "Goa" LN</li></ul> |
|
|
||||||
| AntiAir | MERAD | SA-6 Kub Site | <ul><li>SAM SA-6 Kub "Straight Flush" STR</li><li>SAM SA-6 Kub "Gainful" TEL</li></ul> |
|
|
||||||
| AntiAir | MERAD | SA-11 Buk Battery | <ul><li>SAM SA-11 Buk "Gadfly" Snow Drift SR</li><li>SAM SA-11 Buk "Gadfly" C2</li><li>SAM SA-11 Buk "Gadfly" Fire Dome TEL</li></ul> |
|
|
||||||
| AntiAir | MERAD | SA-17 Grizzly Battery | <ul><li>SAM SA-11 Buk "Gadfly" Snow Drift SR</li><li>SAM SA-11 Buk "Gadfly" C2</li><li>SAM SA-17 Buk M1-2 LN 9A310M1-2</li></ul> |
|
|
||||||
| AntiAir | MERAD | NASAMS AIM-120B | <ul><li>SAM NASAMS SR MPQ64F1</li><li>SAM NASAMS C2</li><li>SAM NASAMS LN AIM-120B</li></ul> |
|
|
||||||
| AntiAir | MERAD | NASAMS AIM-120C | <ul><li>SAM NASAMS SR MPQ64F1</li><li>SAM NASAMS C2</li><li>SAM NASAMS LN AIM-120C</li></ul> |
|
|
||||||
| AntiAir | SHORAD | Rapier AA Site | <ul><li>SAM Rapier Blindfire TR</li><li>SAM Rapier Tracker</li><li>SAM Rapier LN</li></ul> |
|
|
||||||
| AntiAir | SHORAD | Roland Site | <ul><li>SAM Roland EWR</li><li>Roland 2 (Marder Chassis)</li><li>Truck M818 6x6</li></ul> |
|
|
||||||
| AntiAir | SHORAD | HQ-7 Site | <ul><li>HQ-7 Self-Propelled STR</li><li>HQ-7 Launcher</li><li>ZU-23 on Ural-375</li></ul> |
|
|
||||||
| AntiAir | SHORAD | Freya EWR Site | <ul><li>EWR FuMG-401 Freya LZ</li><li>2 cm Flakvierling 38</li><li>8.8 cm Flak 18</li><li>LUV Kubelwagen 82</li><li>Sd.Kfz.7 Tractor</li><li>LUV Kettenrad</li><li>PU Maschinensatz_33</li><li>AAA SP Kdo.G.40</li><li>Infantry Mauser 98</li></ul> |
|
|
||||||
| AntiAir | SHORAD | Short Range Anti Air Group | <ul><li>Classes = [ SHORAD]</li><li>Classes = [ Logistics]</li></ul> |
|
|
||||||
| AntiAir | LORAD | Patriot Battery | <ul><li>SAM Patriot STR</li><li>SAM Patriot CR (AMG AN/MRC-137)</li><li>SAM Patriot ECS</li><li>SAM Patriot C2 ICC</li><li>SAM Patriot EPP-III</li><li>SAM Patriot LN</li><li>Classes = [ AAA]</li><li>Classes = [ SHORAD]</li></ul> |
|
|
||||||
| AntiAir | LORAD | SA-5/S-200 Site | <ul><li>SAM SA-5 S-200 ST-68U "Tin Shield" SR</li><li>SAM SA-5 S-200 "Square Pair" TR"</li><li>Truck Ural-375</li><li>SAM SA-5 S-200 "Gammon" LN"</li></ul> |
|
|
||||||
| AntiAir | LORAD | SA-12/S-300V Battery | <ul><li>SAM SA-12 S-300V 9S15 SR</li><li>SAM SA-12 S-300V 9S19 SR</li><li>SAM SA-12 S-300V 9S457 CP</li><li>SAM SA-12 S-300V 9S32 TR</li><li>SAM SA-12 S-300V 9A82 LN</li><li>SAM SA-12 S-300V 9A83 LN</li><li>SA-19 Grison (2K22 Tunguska)</li><li>SA-15 Tor</li></ul> |
|
|
||||||
| AntiAir | LORAD | SA-20/S-300PMU-1 Battery | <ul><li>SAM SA-20 S-300PMU1 SR 5N66E</li><li>SAM SA-20 S-300PMU1 SR 64N6E</li><li>SAM SA-20 S-300PMU1 CP 54K6</li><li>SAM SA-20 S-300PMU1 TR 30N6E</li><li>SAM SA-20 S-300PMU1 LN 5P85CE</li><li>SAM SA-20 S-300PMU1 LN 5P85DE</li><li>SA-19 Grison (2K22 Tunguska)</li><li>SA-15 Tor</li></ul> |
|
|
||||||
| AntiAir | LORAD | SA-20B/S-300PMU-2 Battery | <ul><li>SAM SA-20 S-300PMU1 SR 5N66E</li><li>SAM SA-20 S-300PMU1 SR 64N6E</li><li>SAM SA-20B S-300PMU2 CP 54K6E2</li><li>SAM SA-20B S-300PMU2 TR 92H6E(truck)</li><li>SAM SA-20B S-300PMU2 LN 5P85SE2</li><li>SA-19 Grison (2K22 Tunguska)</li><li>SA-15 Tor</li></ul> |
|
|
||||||
| AntiAir | LORAD | SA-23/S-300VM Battery | <ul><li>SAM SA-23 S-300VM 9S15M2 SR</li><li>SAM SA-23 S-300VM 9S19M2 SR</li><li>SAM SA-23 S-300VM 9S457ME CP</li><li>SAM SA-23 S-300VM 9S32ME TR</li><li>SAM SA-23 S-300VM 9A82ME LN</li><li>SAM SA-23 S-300VM 9A83ME LN</li><li>SA-19 Grison (2K22 Tunguska)</li><li>SA-15 Tor</li></ul> |
|
|
||||||
| AntiAir | LORAD | SA-10/S-300PS Battery | <ul><li>SAM SA-10 S-300 "Grumble" Clam Shell SR</li><li>SAM SA-10 S-300 "Grumble" Big Bird SR</li><li>SAM SA-10 S-300 "Grumble" C2</li><li>SAM SA-10 S-300 "Grumble" Flap Lid TR</li><li>SAM SA-10 S-300 "Grumble" TEL D</li><li>SAM SA-10 S-300 "Grumble" TEL C</li><li>Classes = [ AAA]</li><li>Classes = [ SHORAD]</li></ul> |
|
|
||||||
| AntiAir | EarlyWarningRadar | Early-Warning Radar | <ul><li>Classes = [ EarlyWarningRadar]</li></ul> |
|
|
||||||
| Building | StrikeTarget | ww2bunker1 | <ul><li>Siegfried Line</li><li>Fire Control Bunker</li><li></li></ul> |
|
|
||||||
| Building | StrikeTarget | allycamp1 | <ul><li>FARP Tent</li><li>Haystack 4</li><li>Haystack 3</li><li>Concertina wire</li><li></li></ul> |
|
|
||||||
| Building | StrikeTarget | fuel1 | <ul><li>Tank</li><li>Tank 3</li></ul> |
|
|
||||||
| Building | StrikeTarget | ware1 | <ul><li>Warehouse</li><li>Hangar A</li></ul> |
|
|
||||||
| Building | StrikeTarget | farp1 | <ul><li>FARP Tent</li><li>FARP Ammo Dump Coating</li><li>FARP CP Blindage</li><li>FARP Fuel Depot</li></ul> |
|
|
||||||
| Building | StrikeTarget | derrick1 | <ul><li>Oil derrick</li><li>Pump station</li><li>Subsidiary structure 2</li></ul> |
|
|
||||||
| Building | StrikeTarget | village1 | <ul><li>Small house 1A</li><li>Small werehouse 1</li><li>Small house 1B</li></ul> |
|
|
||||||
| Building | StrikeTarget | ww2bunker2 | <ul><li>Fire Control Bunker</li><li>Siegfried Line</li><li>Concertina wire</li><li>Belgian gate</li><li>Czech hedgehogs 1</li></ul> |
|
|
||||||
| Building | Ammo | ammo1 | <ul><li>.Ammunition depot</li><li>Hangar B</li></ul> |
|
|
||||||
| Building | StrikeTarget, Comms | comms | <ul><li>TV tower, Comms tower M</li></ul> |
|
|
||||||
| Building | Oil | oil1 | <ul><li>Oil platform</li></ul> |
|
|
||||||
| Building | FOB | fob1 | <ul><li>.Command Center</li><li>Barracks 2</li><li>Garage small B</li></ul> |
|
|
||||||
| Building | StrikeTarget, Power | power1 | <ul><li>Repair workshop</li><li>Workshop A</li><li>Garage B</li><li>Farm B</li></ul> |
|
|
||||||
| Building | Factory | factory1 | <ul><li>Tech combine</li><li>Tech hangar A</li></ul> |
|
|
||||||
| Defenses | Missile | Missile | <ul><li>Classes = [ Missile]</li><li>Classes = [ Logistics]</li><li>Classes = [ AAA]</li><li>Classes = [ SHORAD]</li></ul> |
|
|
||||||
| Defenses | Coastal | Silkworm | <ul><li>Classes = [ SearchRadar]</li><li>Classes = [ Missile]</li><li>Classes = [ Logistics]</li><li>Classes = [ AAA]</li><li>Classes = [ SHORAD]</li></ul> |
|
|
||||||
| GroundForce | BaseDefense, FrontLine | Armor Group | <ul><li>Classes = [ APC, ATGM, IFV, Tank]</li></ul> |
|
|
||||||
| GroundForce | BaseDefense, FrontLine | Armor Group with Anti-Air | <ul><li>Classes = [ APC, ATGM, IFV, Tank]</li><li>Classes = [ AAA, SHORAD, Manpad]</li></ul> |
|
|
||||||
| Naval | Navy | WW2 LST Group | <ul><li>LS Samuel Chase</li><li>LST Mk.II</li></ul> |
|
|
||||||
| Naval | Navy | Russian Navy | <ul><li>Corvette 1124.4 Grish, Corvette 1241.1 Molniya</li><li>Frigate 11540 Neustrashimy, Frigate 1135M Rezky</li><li>Cruiser 1164 Moskva</li></ul> |
|
|
||||||
| Naval | Navy | Chinese Navy | <ul><li>Type 054A Frigate</li><li>Type 052C Destroyer, Type 052B Destroyer</li></ul> |
|
|
||||||
| Naval | Navy | Naval Two Ship | <ul><li>Classes = [ Destroyer, Cruiser, Boat, Submarine, LandingShip]</li></ul> |
|
|
||||||
| Naval | AircraftCarrier | Carrier Group | <ul><li>Classes = [ AircraftCarrier]</li><li>Classes = [ Destroyer]</li></ul> |
|
|
||||||
| Naval | AircraftCarrier | Carrier Strike Group 8 | <ul><li>CVN-74 John C. Stennis</li><li>DDG Arleigh Burke IIa</li><li>CG Ticonderoga</li></ul> |
|
|
||||||
| Naval | HelicopterCarrier | LHA Group | <ul><li>Classes = [ HelicopterCarrier]</li><li>Classes = [ Destroyer]</li></ul> |
|
|
||||||
| Missing Units | SK_C_28_naval_gun, house2arm | | |
|
|
||||||
BIN
doc/templates/template_overview.png
vendored
BIN
doc/templates/template_overview.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB |
82
game/armedforces/armedforces.py
Normal file
82
game/armedforces/armedforces.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
from typing import TYPE_CHECKING, Iterator, Optional
|
||||||
|
from game import db
|
||||||
|
from game.data.groups import GroupRole, GroupTask
|
||||||
|
from game.armedforces.forcegroup import ForceGroup
|
||||||
|
from game.profiling import logged_duration
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.factions.faction import Faction
|
||||||
|
|
||||||
|
|
||||||
|
# TODO More comments and rename
|
||||||
|
class ArmedForces:
|
||||||
|
"""TODO Description"""
|
||||||
|
|
||||||
|
# All available force groups for a specific Role
|
||||||
|
forces: dict[GroupRole, list[ForceGroup]]
|
||||||
|
|
||||||
|
def __init__(self, faction: Faction):
|
||||||
|
with logged_duration(f"Loading armed forces for {faction.name}"):
|
||||||
|
self._load_forces(faction)
|
||||||
|
|
||||||
|
def add_or_update_force_group(self, new_group: ForceGroup) -> None:
|
||||||
|
"""TODO Description"""
|
||||||
|
if new_group.role in self.forces:
|
||||||
|
# Check if a force group with the same units exists
|
||||||
|
for force_group in self.forces[new_group.role]:
|
||||||
|
if (
|
||||||
|
force_group.units == new_group.units
|
||||||
|
and force_group.tasks == new_group.tasks
|
||||||
|
):
|
||||||
|
# Update existing group if units and tasks are equal
|
||||||
|
force_group.update_group(new_group)
|
||||||
|
return
|
||||||
|
# Add a new force group
|
||||||
|
self.add_force_group(new_group)
|
||||||
|
|
||||||
|
def add_force_group(self, force_group: ForceGroup) -> None:
|
||||||
|
"""Adds a force group to the forces"""
|
||||||
|
if force_group.role in self.forces:
|
||||||
|
self.forces[force_group.role].append(force_group)
|
||||||
|
else:
|
||||||
|
self.forces[force_group.role] = [force_group]
|
||||||
|
|
||||||
|
def _load_forces(self, faction: Faction) -> None:
|
||||||
|
"""Initialize all armed_forces for the given faction"""
|
||||||
|
# This function will create a ForgeGroup for each global Layout and PresetGroup
|
||||||
|
self.forces = {}
|
||||||
|
|
||||||
|
preset_layouts = [
|
||||||
|
layout
|
||||||
|
for preset_group in faction.preset_groups
|
||||||
|
for layout in preset_group.layouts
|
||||||
|
]
|
||||||
|
|
||||||
|
# Generate Troops for all generic layouts and presets
|
||||||
|
for layout in db.LAYOUTS.layouts:
|
||||||
|
if (
|
||||||
|
layout.generic or layout in preset_layouts
|
||||||
|
) and layout.usable_by_faction(faction):
|
||||||
|
# Creates a faction compatible GorceGroup
|
||||||
|
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
|
||||||
|
|
||||||
|
def groups_for_task(self, group_task: GroupTask) -> Iterator[ForceGroup]:
|
||||||
|
for groups in self.forces.values():
|
||||||
|
for unit_group in groups:
|
||||||
|
if group_task in unit_group.tasks:
|
||||||
|
yield unit_group
|
||||||
|
|
||||||
|
def groups_for_tasks(self, tasks: list[GroupTask]) -> list[ForceGroup]:
|
||||||
|
groups = []
|
||||||
|
for task in tasks:
|
||||||
|
for group in self.groups_for_task(task):
|
||||||
|
if group not in groups:
|
||||||
|
groups.append(group)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def random_group_for_task(self, group_task: GroupTask) -> Optional[ForceGroup]:
|
||||||
|
unit_groups = list(self.groups_for_task(group_task))
|
||||||
|
return random.choice(unit_groups) if unit_groups else None
|
||||||
250
game/armedforces/forcegroup.py
Normal file
250
game/armedforces/forcegroup.py
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import ClassVar, TYPE_CHECKING, Type, Any, Iterator, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from dcs import Point
|
||||||
|
|
||||||
|
from game import db
|
||||||
|
from game.data.groups import GroupRole, GroupTask
|
||||||
|
from game.data.radar_db import UNITS_WITH_RADAR
|
||||||
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
|
from game.dcs.shipunittype import ShipUnitType
|
||||||
|
from game.dcs.unittype import UnitType
|
||||||
|
from game.point_with_heading import PointWithHeading
|
||||||
|
from game.layout.layout import TheaterLayout, AntiAirLayout, GroupLayout
|
||||||
|
from dcs.unittype import UnitType as DcsUnitType, VehicleType, ShipType, StaticType
|
||||||
|
|
||||||
|
from game.theater.theatergroup import TheaterGroup
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game import Game
|
||||||
|
from game.factions.faction import Faction
|
||||||
|
from game.theater import TheaterGroundObject, ControlPoint
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ForceGroup:
|
||||||
|
"""A logical group of multiple units and layouts which have a specific tasking"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
units: list[UnitType[Any]]
|
||||||
|
statics: list[Type[DcsUnitType]]
|
||||||
|
role: GroupRole
|
||||||
|
tasks: list[GroupTask] = field(default_factory=list)
|
||||||
|
layouts: list[TheaterLayout] = field(default_factory=list)
|
||||||
|
|
||||||
|
_by_name: ClassVar[dict[str, ForceGroup]] = {}
|
||||||
|
_by_role: ClassVar[dict[GroupRole, list[ForceGroup]]] = {}
|
||||||
|
_loaded: bool = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def for_layout(layout: TheaterLayout, faction: Faction) -> ForceGroup:
|
||||||
|
"""TODO Documentation"""
|
||||||
|
units: set[UnitType[Any]] = set()
|
||||||
|
statics: set[Type[DcsUnitType]] = set()
|
||||||
|
for group in layout.groups:
|
||||||
|
for unit_type in group.possible_types_for_faction(faction):
|
||||||
|
if issubclass(unit_type, VehicleType):
|
||||||
|
units.add(next(GroundUnitType.for_dcs_type(unit_type)))
|
||||||
|
elif issubclass(unit_type, ShipType):
|
||||||
|
units.add(next(ShipUnitType.for_dcs_type(unit_type)))
|
||||||
|
elif issubclass(unit_type, StaticType):
|
||||||
|
statics.add(unit_type)
|
||||||
|
|
||||||
|
return ForceGroup(
|
||||||
|
f"{layout.role.value}: {', '.join([t.description for t in layout.tasks])}",
|
||||||
|
list(units),
|
||||||
|
list(statics),
|
||||||
|
layout.role,
|
||||||
|
layout.tasks,
|
||||||
|
[layout],
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def named(cls, name: str) -> ForceGroup:
|
||||||
|
if not cls._loaded:
|
||||||
|
cls._load_all()
|
||||||
|
return cls._by_name[name]
|
||||||
|
|
||||||
|
def has_access_to_dcs_type(self, type: Type[DcsUnitType]) -> bool:
|
||||||
|
return (
|
||||||
|
any(unit.dcs_unit_type == type for unit in self.units)
|
||||||
|
or type in self.statics
|
||||||
|
)
|
||||||
|
|
||||||
|
def dcs_unit_types_for_group(self, group: GroupLayout) -> list[Type[DcsUnitType]]:
|
||||||
|
"""TODO Description"""
|
||||||
|
unit_types = [t for t in group.unit_types if self.has_access_to_dcs_type(t)]
|
||||||
|
|
||||||
|
alternative_types = []
|
||||||
|
for accessible_unit in self.units:
|
||||||
|
if accessible_unit.unit_class in group.unit_classes:
|
||||||
|
unit_types.append(accessible_unit.dcs_unit_type)
|
||||||
|
if accessible_unit.unit_class in group.alternative_classes:
|
||||||
|
alternative_types.append(accessible_unit.dcs_unit_type)
|
||||||
|
|
||||||
|
return unit_types or alternative_types
|
||||||
|
|
||||||
|
def unit_types_for_group(self, group: GroupLayout) -> Iterator[UnitType[Any]]:
|
||||||
|
for dcs_type in self.dcs_unit_types_for_group(group):
|
||||||
|
if issubclass(dcs_type, VehicleType):
|
||||||
|
yield next(GroundUnitType.for_dcs_type(dcs_type))
|
||||||
|
elif issubclass(dcs_type, ShipType):
|
||||||
|
yield next(ShipUnitType.for_dcs_type(dcs_type))
|
||||||
|
|
||||||
|
def statics_for_group(self, group: GroupLayout) -> Iterator[Type[DcsUnitType]]:
|
||||||
|
for dcs_type in self.dcs_unit_types_for_group(group):
|
||||||
|
if issubclass(dcs_type, StaticType):
|
||||||
|
yield dcs_type
|
||||||
|
|
||||||
|
def random_dcs_unit_type_for_group(self, group: GroupLayout) -> Type[DcsUnitType]:
|
||||||
|
"""TODO Description"""
|
||||||
|
return random.choice(self.dcs_unit_types_for_group(group))
|
||||||
|
|
||||||
|
def update_group(self, new_group: ForceGroup) -> None:
|
||||||
|
"""Update the group from another group. This will merge statics and layouts."""
|
||||||
|
# Merge layouts and statics
|
||||||
|
self.statics = list(set(self.statics + new_group.statics))
|
||||||
|
self.layouts = list(set(self.layouts + new_group.layouts))
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
position: PointWithHeading,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
game: Game,
|
||||||
|
) -> TheaterGroundObject:
|
||||||
|
"""Create a random TheaterGroundObject from the available templates"""
|
||||||
|
layout = random.choice(self.layouts)
|
||||||
|
return self.create_ground_object_for_layout(
|
||||||
|
layout, name, position, control_point, game
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_ground_object_for_layout(
|
||||||
|
self,
|
||||||
|
layout: TheaterLayout,
|
||||||
|
name: str,
|
||||||
|
position: PointWithHeading,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
game: Game,
|
||||||
|
) -> TheaterGroundObject:
|
||||||
|
"""Create a TheaterGroundObject for the given template"""
|
||||||
|
go = layout.create_ground_object(name, position, control_point)
|
||||||
|
# Generate all groups using the randomization if it defined
|
||||||
|
for group in layout.groups:
|
||||||
|
# Choose a random unit_type for the group
|
||||||
|
try:
|
||||||
|
unit_type = self.random_dcs_unit_type_for_group(group)
|
||||||
|
except IndexError:
|
||||||
|
if group.optional:
|
||||||
|
# If group is optional it is ok when no unit_type is available
|
||||||
|
continue
|
||||||
|
# if non-optional this is a error
|
||||||
|
raise RuntimeError(f"No accessible unit for {self.name} - {group.name}")
|
||||||
|
self.create_theater_group_for_tgo(go, group, name, game, unit_type)
|
||||||
|
|
||||||
|
return go
|
||||||
|
|
||||||
|
def create_theater_group_for_tgo(
|
||||||
|
self,
|
||||||
|
ground_object: TheaterGroundObject,
|
||||||
|
group: GroupLayout,
|
||||||
|
name: str,
|
||||||
|
game: Game,
|
||||||
|
unit_type: Type[DcsUnitType],
|
||||||
|
unit_count: Optional[int] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Create a TheaterGroup and add it to the given TGO"""
|
||||||
|
# Random UnitCounter if not forced
|
||||||
|
if unit_count is None:
|
||||||
|
unit_count = group.unit_counter
|
||||||
|
# Static and non Static groups have to be separated
|
||||||
|
group_id = group.group - 1
|
||||||
|
if len(ground_object.groups) <= group_id:
|
||||||
|
# Requested group was not yet created
|
||||||
|
ground_group = TheaterGroup.from_template(
|
||||||
|
game.next_group_id(), group, ground_object, unit_type, unit_count
|
||||||
|
)
|
||||||
|
# Set Group Name
|
||||||
|
ground_group.name = f"{name} {group_id}"
|
||||||
|
ground_object.groups.append(ground_group)
|
||||||
|
units = ground_group.units
|
||||||
|
else:
|
||||||
|
ground_group = ground_object.groups[group_id]
|
||||||
|
units = group.generate_units(ground_object, unit_type, unit_count)
|
||||||
|
ground_group.units.extend(units)
|
||||||
|
|
||||||
|
# Assign UniqueID, name and align relative to ground_object
|
||||||
|
for u_id, unit in enumerate(units):
|
||||||
|
unit.id = game.next_unit_id()
|
||||||
|
unit.name = unit.unit_type.name if unit.unit_type else unit.type.name
|
||||||
|
unit.position = PointWithHeading.from_point(
|
||||||
|
Point(
|
||||||
|
ground_object.position.x + unit.position.x,
|
||||||
|
ground_object.position.y + unit.position.y,
|
||||||
|
),
|
||||||
|
# Align heading to GroundObject defined by the campaign designer
|
||||||
|
unit.position.heading + ground_object.heading,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
isinstance(self, AntiAirLayout)
|
||||||
|
and unit.unit_type
|
||||||
|
and unit.unit_type.dcs_unit_type in UNITS_WITH_RADAR
|
||||||
|
):
|
||||||
|
# Head Radars towards the center of the conflict
|
||||||
|
unit.position.heading = (
|
||||||
|
game.theater.heading_to_conflict_from(unit.position)
|
||||||
|
or unit.position.heading
|
||||||
|
)
|
||||||
|
# Rotate unit around the center to align the orientation of the group
|
||||||
|
unit.position.rotate(ground_object.position, ground_object.heading)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _load_all(cls) -> None:
|
||||||
|
for file in Path("resources/units/groups").glob("*.yaml"):
|
||||||
|
if not file.is_file():
|
||||||
|
raise RuntimeError(f"{file.name} is not a valid ForceGroup")
|
||||||
|
|
||||||
|
with file.open(encoding="utf-8") as data_file:
|
||||||
|
data = yaml.safe_load(data_file)
|
||||||
|
|
||||||
|
group_role = GroupRole(data.get("role"))
|
||||||
|
|
||||||
|
group_tasks = [GroupTask.by_description(n) for n in data.get("tasks", [])]
|
||||||
|
|
||||||
|
units = [UnitType.named(unit) for unit in data.get("units", [])]
|
||||||
|
|
||||||
|
statics = []
|
||||||
|
for static in data.get("statics", []):
|
||||||
|
static_type = db.static_type_from_name(static)
|
||||||
|
if static_type is None:
|
||||||
|
logging.error(f"Static {static} for {file} is not valid")
|
||||||
|
else:
|
||||||
|
statics.append(static_type)
|
||||||
|
|
||||||
|
layouts = [next(db.LAYOUTS.by_name(n)) for n in data.get("layouts")]
|
||||||
|
|
||||||
|
force_group = ForceGroup(
|
||||||
|
name=data.get("name"),
|
||||||
|
units=units,
|
||||||
|
statics=statics,
|
||||||
|
role=group_role,
|
||||||
|
tasks=group_tasks,
|
||||||
|
layouts=layouts,
|
||||||
|
)
|
||||||
|
|
||||||
|
cls._by_name[force_group.name] = force_group
|
||||||
|
if group_role in cls._by_role:
|
||||||
|
cls._by_role[group_role].append(force_group)
|
||||||
|
else:
|
||||||
|
cls._by_role[group_role] = [force_group]
|
||||||
|
|
||||||
|
cls._loaded = True
|
||||||
@ -9,6 +9,7 @@ from game.campaignloader import CampaignAirWingConfig
|
|||||||
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
||||||
from game.commander import TheaterCommander
|
from game.commander import TheaterCommander
|
||||||
from game.commander.missionscheduler import MissionScheduler
|
from game.commander.missionscheduler import MissionScheduler
|
||||||
|
from game.armedforces.armedforces import ArmedForces
|
||||||
from game.income import Income
|
from game.income import Income
|
||||||
from game.navmesh import NavMesh
|
from game.navmesh import NavMesh
|
||||||
from game.orderedset import OrderedSet
|
from game.orderedset import OrderedSet
|
||||||
@ -41,6 +42,7 @@ class Coalition:
|
|||||||
self.bullseye = Bullseye(Point(0, 0))
|
self.bullseye = Bullseye(Point(0, 0))
|
||||||
self.faker = Faker(self.faction.locales)
|
self.faker = Faker(self.faction.locales)
|
||||||
self.air_wing = AirWing(player, game, self.faction)
|
self.air_wing = AirWing(player, game, self.faction)
|
||||||
|
self.armed_forces = ArmedForces(self.faction)
|
||||||
self.transfers = PendingTransfers(game, player)
|
self.transfers = PendingTransfers(game, player)
|
||||||
|
|
||||||
# Late initialized because the two coalitions in the game are mutually
|
# Late initialized because the two coalitions in the game are mutually
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
from dcs.vehicles import AirDefence
|
from dcs.vehicles import AirDefence
|
||||||
|
|
||||||
|
from game.theater.theatergroup import TheaterUnit
|
||||||
|
|
||||||
|
|
||||||
class AlicCodes:
|
class AlicCodes:
|
||||||
CODES = {
|
CODES = {
|
||||||
@ -37,5 +39,5 @@ class AlicCodes:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def code_for(cls, unit_type: str) -> int:
|
def code_for(cls, unit: TheaterUnit) -> int:
|
||||||
return cls.CODES[unit_type]
|
return cls.CODES[unit.type.id]
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import dcs
|
import dcs
|
||||||
|
|
||||||
|
REQUIRED_BUILDINGS = [
|
||||||
|
"ammo",
|
||||||
|
"factory",
|
||||||
|
"fob",
|
||||||
|
]
|
||||||
|
|
||||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||||
"fuel",
|
"fuel",
|
||||||
"comms",
|
"comms",
|
||||||
|
|||||||
@ -104,13 +104,13 @@ MODERN_DOCTRINE = Doctrine(
|
|||||||
sweep_distance=nautical_miles(60),
|
sweep_distance=nautical_miles(60),
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
{
|
{
|
||||||
UnitClass.Tank: 3,
|
UnitClass.TANK: 3,
|
||||||
UnitClass.Atgm: 2,
|
UnitClass.ATGM: 2,
|
||||||
UnitClass.Apc: 2,
|
UnitClass.APC: 2,
|
||||||
UnitClass.Ifv: 3,
|
UnitClass.IFV: 3,
|
||||||
UnitClass.Artillery: 1,
|
UnitClass.ARTILLERY: 1,
|
||||||
UnitClass.SHORAD: 2,
|
UnitClass.SHORAD: 2,
|
||||||
UnitClass.Recon: 1,
|
UnitClass.RECON: 1,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -141,13 +141,13 @@ COLDWAR_DOCTRINE = Doctrine(
|
|||||||
sweep_distance=nautical_miles(40),
|
sweep_distance=nautical_miles(40),
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
{
|
{
|
||||||
UnitClass.Tank: 4,
|
UnitClass.TANK: 4,
|
||||||
UnitClass.Atgm: 2,
|
UnitClass.ATGM: 2,
|
||||||
UnitClass.Apc: 3,
|
UnitClass.APC: 3,
|
||||||
UnitClass.Ifv: 2,
|
UnitClass.IFV: 2,
|
||||||
UnitClass.Artillery: 1,
|
UnitClass.ARTILLERY: 1,
|
||||||
UnitClass.SHORAD: 2,
|
UnitClass.SHORAD: 2,
|
||||||
UnitClass.Recon: 1,
|
UnitClass.RECON: 1,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -178,12 +178,12 @@ WWII_DOCTRINE = Doctrine(
|
|||||||
sweep_distance=nautical_miles(10),
|
sweep_distance=nautical_miles(10),
|
||||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||||
{
|
{
|
||||||
UnitClass.Tank: 3,
|
UnitClass.TANK: 3,
|
||||||
UnitClass.Atgm: 3,
|
UnitClass.ATGM: 3,
|
||||||
UnitClass.Apc: 3,
|
UnitClass.APC: 3,
|
||||||
UnitClass.Artillery: 1,
|
UnitClass.ARTILLERY: 1,
|
||||||
UnitClass.SHORAD: 3,
|
UnitClass.SHORAD: 3,
|
||||||
UnitClass.Recon: 1,
|
UnitClass.RECON: 1,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,63 +1,69 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class GroupRole(Enum):
|
class GroupRole(Enum):
|
||||||
Unknow = "Unknown"
|
"""Role of a ForceGroup within the ArmedForces"""
|
||||||
AntiAir = "AntiAir"
|
|
||||||
Building = "Building"
|
AIR_DEFENSE = "AntiAir"
|
||||||
Naval = "Naval"
|
BUILDING = "Building"
|
||||||
GroundForce = "GroundForce"
|
DEFENSES = "Defenses"
|
||||||
Defenses = "Defenses"
|
GROUND_FORCE = "GroundForce"
|
||||||
Air = "Air"
|
NAVAL = "Naval"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks(self) -> list[GroupTask]:
|
||||||
|
return [task for task in GroupTask if task.role == self]
|
||||||
|
|
||||||
|
|
||||||
class GroupTask(Enum):
|
class GroupTask(Enum):
|
||||||
EWR = "EarlyWarningRadar"
|
"""Specific Tasking of a ForceGroup"""
|
||||||
AAA = "AAA"
|
|
||||||
SHORAD = "SHORAD"
|
|
||||||
MERAD = "MERAD"
|
|
||||||
LORAD = "LORAD"
|
|
||||||
AircraftCarrier = "AircraftCarrier"
|
|
||||||
HelicopterCarrier = "HelicopterCarrier"
|
|
||||||
Navy = "Navy"
|
|
||||||
BaseDefense = "BaseDefense" # Ground
|
|
||||||
FrontLine = "FrontLine"
|
|
||||||
Air = "Air"
|
|
||||||
Missile = "Missile"
|
|
||||||
Coastal = "Coastal"
|
|
||||||
Factory = "Factory"
|
|
||||||
Ammo = "Ammo"
|
|
||||||
Oil = "Oil"
|
|
||||||
FOB = "FOB"
|
|
||||||
StrikeTarget = "StrikeTarget"
|
|
||||||
Comms = "Comms"
|
|
||||||
Power = "Power"
|
|
||||||
|
|
||||||
|
def __init__(self, description: str, role: GroupRole):
|
||||||
|
self.description = description
|
||||||
|
self.role = role
|
||||||
|
|
||||||
ROLE_TASKINGS: dict[GroupRole, list[GroupTask]] = {
|
@classmethod
|
||||||
GroupRole.Unknow: [], # No Tasking
|
def by_description(cls, description: str) -> GroupTask:
|
||||||
GroupRole.AntiAir: [
|
for task in GroupTask:
|
||||||
GroupTask.EWR,
|
if task.description == description:
|
||||||
GroupTask.AAA,
|
return task
|
||||||
GroupTask.SHORAD,
|
raise RuntimeError(f"GroupTask with description {description} does not exist")
|
||||||
GroupTask.MERAD,
|
|
||||||
GroupTask.LORAD,
|
# ANTI AIR
|
||||||
],
|
AAA = ("AAA", GroupRole.AIR_DEFENSE)
|
||||||
GroupRole.GroundForce: [GroupTask.BaseDefense, GroupTask.FrontLine],
|
EARLY_WARNING_RADAR = ("EarlyWarningRadar", GroupRole.AIR_DEFENSE)
|
||||||
GroupRole.Naval: [
|
LORAD = ("LORAD", GroupRole.AIR_DEFENSE)
|
||||||
GroupTask.AircraftCarrier,
|
MERAD = ("MERAD", GroupRole.AIR_DEFENSE)
|
||||||
GroupTask.HelicopterCarrier,
|
SHORAD = ("SHORAD", GroupRole.AIR_DEFENSE)
|
||||||
GroupTask.Navy,
|
|
||||||
],
|
# NAVAL
|
||||||
GroupRole.Building: [
|
AIRCRAFT_CARRIER = ("AircraftCarrier", GroupRole.NAVAL)
|
||||||
GroupTask.Factory,
|
HELICOPTER_CARRIER = ("HelicopterCarrier", GroupRole.NAVAL)
|
||||||
GroupTask.Ammo,
|
NAVY = ("Navy", GroupRole.NAVAL)
|
||||||
GroupTask.Oil,
|
|
||||||
GroupTask.FOB,
|
# GROUND FORCES
|
||||||
GroupTask.StrikeTarget,
|
BASE_DEFENSE = ("BaseDefense", GroupRole.GROUND_FORCE)
|
||||||
GroupTask.Comms,
|
FRONT_LINE = ("FrontLine", GroupRole.GROUND_FORCE)
|
||||||
GroupTask.Power,
|
|
||||||
],
|
# DEFENSES
|
||||||
GroupRole.Defenses: [GroupTask.Missile, GroupTask.Coastal],
|
COASTAL = ("Coastal", GroupRole.DEFENSES)
|
||||||
GroupRole.Air: [GroupTask.Air],
|
MISSILE = ("Missile", GroupRole.DEFENSES)
|
||||||
}
|
|
||||||
|
# BUILDINGS
|
||||||
|
ALLY_CAMP = ("AllyCamp", GroupRole.BUILDING)
|
||||||
|
AMMO = ("Ammo", GroupRole.BUILDING)
|
||||||
|
COMMS = ("Comms", GroupRole.BUILDING)
|
||||||
|
DERRICK = ("Derrick", GroupRole.BUILDING)
|
||||||
|
FACTORY = ("Factory", GroupRole.BUILDING)
|
||||||
|
FARP = ("Farp", GroupRole.BUILDING)
|
||||||
|
FOB = ("FOB", GroupRole.BUILDING)
|
||||||
|
FUEL = ("Fuel", GroupRole.BUILDING)
|
||||||
|
OFFSHORE_STRIKE_TARGET = ("OffShoreStrikeTarget", GroupRole.BUILDING)
|
||||||
|
OIL = ("Oil", GroupRole.BUILDING)
|
||||||
|
POWER = ("Power", GroupRole.BUILDING)
|
||||||
|
STRIKE_TARGET = ("StrikeTarget", GroupRole.BUILDING)
|
||||||
|
VILLAGE = ("Village", GroupRole.BUILDING)
|
||||||
|
WARE = ("Ware", GroupRole.BUILDING)
|
||||||
|
WW2_BUNKER = ("WW2Bunker", GroupRole.BUILDING)
|
||||||
|
|||||||
@ -2,39 +2,40 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from enum import unique, Enum
|
from enum import unique, Enum
|
||||||
|
|
||||||
from game.data.groups import GroupRole, GroupTask
|
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class UnitClass(Enum):
|
class UnitClass(Enum):
|
||||||
Unknown = "Unknown"
|
UNKNOWN = "Unknown"
|
||||||
Tank = "Tank"
|
|
||||||
Atgm = "ATGM"
|
|
||||||
Ifv = "IFV"
|
|
||||||
Apc = "APC"
|
|
||||||
Artillery = "Artillery"
|
|
||||||
Logistics = "Logistics"
|
|
||||||
Recon = "Recon"
|
|
||||||
Infantry = "Infantry"
|
|
||||||
AAA = "AAA"
|
AAA = "AAA"
|
||||||
|
AIRCRAFT_CARRIER = "AircraftCarrier"
|
||||||
|
APC = "APC"
|
||||||
|
ARTILLERY = "Artillery"
|
||||||
|
ATGM = "ATGM"
|
||||||
|
BOAT = "Boat"
|
||||||
|
COMMAND_POST = "CommandPost"
|
||||||
|
CRUISER = "Cruiser"
|
||||||
|
DESTROYER = "Destroyer"
|
||||||
|
EARLY_WARNING_RADAR = "EarlyWarningRadar"
|
||||||
|
FORTIFICATION = "Fortification"
|
||||||
|
FRIGATE = "Frigate"
|
||||||
|
HELICOPTER_CARRIER = "HelicopterCarrier"
|
||||||
|
IFV = "IFV"
|
||||||
|
INFANTRY = "Infantry"
|
||||||
|
LANDING_SHIP = "LandingShip"
|
||||||
|
LAUNCHER = "Launcher"
|
||||||
|
LOGISTICS = "Logistics"
|
||||||
|
MANPAD = "Manpad"
|
||||||
|
MISSILE = "Missile"
|
||||||
|
OPTICAL_TRACKER = "OpticalTracker"
|
||||||
|
PLANE = "Plane"
|
||||||
|
POWER = "Power"
|
||||||
|
RECON = "Recon"
|
||||||
|
SEARCH_LIGHT = "SearchLight"
|
||||||
|
SEARCH_RADAR = "SearchRadar"
|
||||||
|
SEARCH_TRACK_RADAR = "SearchTrackRadar"
|
||||||
SHORAD = "SHORAD"
|
SHORAD = "SHORAD"
|
||||||
Manpad = "Manpad"
|
SPECIALIZED_RADAR = "SpecializedRadar"
|
||||||
SR = "SearchRadar"
|
SUBMARINE = "Submarine"
|
||||||
STR = "SearchTrackRadar"
|
TANK = "Tank"
|
||||||
LowAltSR = "LowAltSearchRadar"
|
|
||||||
TR = "TrackRadar"
|
|
||||||
LN = "Launcher"
|
|
||||||
EWR = "EarlyWarningRadar"
|
|
||||||
TELAR = "TELAR"
|
TELAR = "TELAR"
|
||||||
Missile = "Missile"
|
TRACK_RADAR = "TrackRadar"
|
||||||
AircraftCarrier = "AircraftCarrier"
|
|
||||||
HelicopterCarrier = "HelicopterCarrier"
|
|
||||||
Destroyer = "Destroyer"
|
|
||||||
Cruiser = "Cruiser"
|
|
||||||
Submarine = "Submarine"
|
|
||||||
LandingShip = "LandingShip"
|
|
||||||
Boat = "Boat"
|
|
||||||
Plane = "Plane"
|
|
||||||
|
|
||||||
def to_dict(self) -> str:
|
|
||||||
return self.value
|
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
|
||||||
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, ClassVar, Iterator, Optional, TYPE_CHECKING, Type
|
from typing import Any, Iterator, Optional, TYPE_CHECKING, Type
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from dcs.helicopters import helicopter_map
|
from dcs.helicopters import helicopter_map
|
||||||
@ -397,5 +396,5 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
|||||||
channel_namer=radio_config.channel_namer,
|
channel_namer=radio_config.channel_namer,
|
||||||
kneeboard_units=units,
|
kneeboard_units=units,
|
||||||
utc_kneeboard=data.get("utc_kneeboard", False),
|
utc_kneeboard=data.get("utc_kneeboard", False),
|
||||||
unit_class=UnitClass.Plane,
|
unit_class=UnitClass.PLANE,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Type, Optional, Iterator
|
from typing import Type, Iterator
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from dcs.unittype import VehicleType
|
from dcs.unittype import VehicleType
|
||||||
@ -55,8 +55,11 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
|
|||||||
introduction = "No data."
|
introduction = "No data."
|
||||||
|
|
||||||
class_name = data.get("class")
|
class_name = data.get("class")
|
||||||
# TODO Exception handling for missing classes
|
if class_name is None:
|
||||||
unit_class = UnitClass(class_name) if class_name else UnitClass.Unknown
|
logging.warning(f"{vehicle.id} has no class")
|
||||||
|
unit_class = UnitClass.UNKNOWN
|
||||||
|
else:
|
||||||
|
unit_class = UnitClass(class_name)
|
||||||
|
|
||||||
for variant in data.get("variants", [vehicle.id]):
|
for variant in data.get("variants", [vehicle.id]):
|
||||||
yield GroundUnitType(
|
yield GroundUnitType(
|
||||||
|
|||||||
@ -1,15 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Type, Optional, ClassVar, Iterator
|
from typing import Type, Iterator
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from dcs.ships import ship_map
|
from dcs.ships import ship_map
|
||||||
from dcs.unittype import VehicleType, ShipType
|
from dcs.unittype import ShipType
|
||||||
from dcs.vehicles import vehicle_map
|
|
||||||
|
|
||||||
from game.data.units import UnitClass
|
from game.data.units import UnitClass
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
@ -70,5 +68,5 @@ class ShipUnitType(UnitType[Type[ShipType]]):
|
|||||||
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."),
|
||||||
role=data.get("role", "No data."),
|
role=data.get("role", "No data."),
|
||||||
price=data.get("price", 1),
|
price=data.get("price"),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,157 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import itertools
|
|
||||||
import random
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import ClassVar, TYPE_CHECKING, Any, Iterator
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from game.data.groups import GroupRole, GroupTask
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
|
||||||
from game.dcs.shipunittype import ShipUnitType
|
|
||||||
from game.dcs.unittype import UnitType
|
|
||||||
from game.point_with_heading import PointWithHeading
|
|
||||||
from gen.templates import GroundObjectTemplate
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game import Game
|
|
||||||
from game.factions.faction import Faction
|
|
||||||
from game.theater import TheaterGroundObject, ControlPoint
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UnitGroup:
|
|
||||||
name: str
|
|
||||||
ground_units: list[GroundUnitType]
|
|
||||||
ship_units: list[ShipUnitType]
|
|
||||||
statics: list[str]
|
|
||||||
role: GroupRole
|
|
||||||
tasks: list[GroupTask] = field(default_factory=list)
|
|
||||||
template_names: list[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
_by_name: ClassVar[dict[str, UnitGroup]] = {}
|
|
||||||
_by_role: ClassVar[dict[GroupRole, list[UnitGroup]]] = {}
|
|
||||||
_loaded: bool = False
|
|
||||||
_templates: list[GroundObjectTemplate] = field(default_factory=list)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def update_from_unit_group(self, unit_group: UnitGroup) -> None:
|
|
||||||
# Update tasking and templates
|
|
||||||
self.tasks.extend([task for task in unit_group.tasks if task not in self.tasks])
|
|
||||||
self._templates.extend(
|
|
||||||
[
|
|
||||||
template
|
|
||||||
for template in unit_group.templates
|
|
||||||
if template not in self.templates
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def templates(self) -> list[GroundObjectTemplate]:
|
|
||||||
return self._templates
|
|
||||||
|
|
||||||
def add_template(self, faction_template: GroundObjectTemplate) -> None:
|
|
||||||
template = copy.deepcopy(faction_template)
|
|
||||||
updated_groups = []
|
|
||||||
for group in template.groups:
|
|
||||||
unit_types = list(
|
|
||||||
itertools.chain(
|
|
||||||
[u.dcs_id for u in self.ground_units if group.can_use_unit(u)],
|
|
||||||
[s.dcs_id for s in self.ship_units if group.can_use_unit(s)],
|
|
||||||
[s for s in self.statics if group.can_use_unit_type(s)],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if unit_types:
|
|
||||||
group.set_possible_types(unit_types)
|
|
||||||
updated_groups.append(group)
|
|
||||||
template.groups = updated_groups
|
|
||||||
self._templates.append(template)
|
|
||||||
|
|
||||||
def load_templates(self, faction: Faction) -> None:
|
|
||||||
self._templates = []
|
|
||||||
if self.template_names:
|
|
||||||
# Preferred templates
|
|
||||||
for template_name in self.template_names:
|
|
||||||
template = faction.templates.by_name(template_name)
|
|
||||||
if template:
|
|
||||||
self.add_template(template)
|
|
||||||
|
|
||||||
if not self._templates:
|
|
||||||
# Find all matching templates if no preferred set or available
|
|
||||||
for template in list(
|
|
||||||
faction.templates.for_role_and_tasks(self.role, self.tasks)
|
|
||||||
):
|
|
||||||
if any(self.has_unit_type(unit) for unit in template.units):
|
|
||||||
self.add_template(template)
|
|
||||||
|
|
||||||
def set_templates(self, templates: list[GroundObjectTemplate]) -> None:
|
|
||||||
self._templates = templates
|
|
||||||
|
|
||||||
def has_unit_type(self, unit_type: UnitType[Any]) -> bool:
|
|
||||||
return unit_type in self.ground_units or unit_type in self.ship_units
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unit_types(self) -> Iterator[str]:
|
|
||||||
for unit in self.ground_units:
|
|
||||||
yield unit.dcs_id
|
|
||||||
for ship in self.ship_units:
|
|
||||||
yield ship.dcs_id
|
|
||||||
for static in self.statics:
|
|
||||||
yield static
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def named(cls, name: str) -> UnitGroup:
|
|
||||||
if not cls._loaded:
|
|
||||||
cls._load_all()
|
|
||||||
return cls._by_name[name]
|
|
||||||
|
|
||||||
def generate(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: PointWithHeading,
|
|
||||||
control_point: ControlPoint,
|
|
||||||
game: Game,
|
|
||||||
) -> TheaterGroundObject:
|
|
||||||
template = random.choice(self.templates)
|
|
||||||
return template.generate(name, position, control_point, game)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _load_all(cls) -> None:
|
|
||||||
for file in Path("resources/units/unit_groups").glob("*.yaml"):
|
|
||||||
if not file.is_file():
|
|
||||||
continue
|
|
||||||
|
|
||||||
with file.open(encoding="utf-8") as data_file:
|
|
||||||
data = yaml.safe_load(data_file)
|
|
||||||
|
|
||||||
group_role = GroupRole(data.get("role"))
|
|
||||||
|
|
||||||
group_tasks = [GroupTask(n) for n in data.get("tasks", [])]
|
|
||||||
|
|
||||||
ground_units = [
|
|
||||||
GroundUnitType.named(n) for n in data.get("ground_units", [])
|
|
||||||
]
|
|
||||||
ship_units = [ShipUnitType.named(n) for n in data.get("ship_units", [])]
|
|
||||||
|
|
||||||
unit_group = UnitGroup(
|
|
||||||
name=data.get("name"),
|
|
||||||
ground_units=ground_units,
|
|
||||||
ship_units=ship_units,
|
|
||||||
statics=data.get("statics", []),
|
|
||||||
role=group_role,
|
|
||||||
tasks=group_tasks,
|
|
||||||
template_names=data.get("templates", []),
|
|
||||||
)
|
|
||||||
|
|
||||||
cls._by_name[unit_group.name] = unit_group
|
|
||||||
if group_role in cls._by_role:
|
|
||||||
cls._by_role[group_role].append(unit_group)
|
|
||||||
else:
|
|
||||||
cls._by_role[group_role] = [unit_group]
|
|
||||||
|
|
||||||
cls._loaded = True
|
|
||||||
@ -4,7 +4,7 @@ from abc import ABC
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator, Optional
|
from typing import TypeVar, Generic, Type, ClassVar, Any, Iterator
|
||||||
|
|
||||||
from dcs.unittype import UnitType as DcsUnitType
|
from dcs.unittype import UnitType as DcsUnitType
|
||||||
|
|
||||||
@ -26,7 +26,9 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
|
|||||||
unit_class: UnitClass
|
unit_class: UnitClass
|
||||||
|
|
||||||
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
|
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
|
||||||
_by_unit_type: ClassVar[dict[DcsUnitTypeT, list[UnitType[Any]]]] = defaultdict(list)
|
_by_unit_type: ClassVar[dict[Type[DcsUnitType], list[UnitType[Any]]]] = defaultdict(
|
||||||
|
list
|
||||||
|
)
|
||||||
_loaded: ClassVar[bool] = False
|
_loaded: ClassVar[bool] = False
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -43,7 +45,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def named(cls, name: str) -> UnitType[Any]:
|
def named(cls, name: str) -> UnitType[Any]:
|
||||||
raise NotImplementedError
|
return cls._by_name[name]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
|
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
|
||||||
|
|||||||
@ -26,7 +26,7 @@ if TYPE_CHECKING:
|
|||||||
ConvoyUnit,
|
ConvoyUnit,
|
||||||
FlyingUnit,
|
FlyingUnit,
|
||||||
FrontLineUnit,
|
FrontLineUnit,
|
||||||
GroundObjectMapping,
|
TheaterUnitMapping,
|
||||||
UnitMap,
|
UnitMap,
|
||||||
SceneryObjectMapping,
|
SceneryObjectMapping,
|
||||||
)
|
)
|
||||||
@ -72,8 +72,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[GroundObjectMapping] = field(default_factory=list)
|
player_ground_objects: List[TheaterUnitMapping] = field(default_factory=list)
|
||||||
enemy_ground_objects: List[GroundObjectMapping] = field(default_factory=list)
|
enemy_ground_objects: List[TheaterUnitMapping] = field(default_factory=list)
|
||||||
|
|
||||||
player_scenery: List[SceneryObjectMapping] = field(default_factory=list)
|
player_scenery: List[SceneryObjectMapping] = field(default_factory=list)
|
||||||
enemy_scenery: List[SceneryObjectMapping] = field(default_factory=list)
|
enemy_scenery: List[SceneryObjectMapping] = field(default_factory=list)
|
||||||
@ -158,7 +158,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[GroundObjectMapping]:
|
def ground_object_losses(self) -> Iterator[TheaterUnitMapping]:
|
||||||
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
|
||||||
|
|
||||||
@ -224,15 +224,7 @@ class Debriefing:
|
|||||||
else:
|
else:
|
||||||
losses = self.ground_losses.enemy_ground_objects
|
losses = self.ground_losses.enemy_ground_objects
|
||||||
for loss in losses:
|
for loss in losses:
|
||||||
# We do not have handling for ships and statics UniType yet so we have to
|
losses_by_type[loss.theater_unit.type.id] += 1
|
||||||
# take more care here. Fallback for ship and static is to use the type str
|
|
||||||
# which is the dcs_type.id
|
|
||||||
unit_type = (
|
|
||||||
loss.ground_unit.unit_type.name
|
|
||||||
if loss.ground_unit.unit_type
|
|
||||||
else loss.ground_unit.type
|
|
||||||
)
|
|
||||||
losses_by_type[unit_type] += 1
|
|
||||||
return losses_by_type
|
return losses_by_type
|
||||||
|
|
||||||
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
|
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||||
@ -286,9 +278,9 @@ class Debriefing:
|
|||||||
losses.enemy_cargo_ships.append(cargo_ship)
|
losses.enemy_cargo_ships.append(cargo_ship)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ground_object = self.unit_map.ground_object(unit_name)
|
ground_object = self.unit_map.theater_units(unit_name)
|
||||||
if ground_object is not None:
|
if ground_object is not None:
|
||||||
if ground_object.ground_unit.ground_object.is_friendly(to_player=True):
|
if ground_object.theater_unit.ground_object.is_friendly(to_player=True):
|
||||||
losses.player_ground_objects.append(ground_object)
|
losses.player_ground_objects.append(ground_object)
|
||||||
else:
|
else:
|
||||||
losses.enemy_ground_objects.append(ground_object)
|
losses.enemy_ground_objects.append(ground_object)
|
||||||
|
|||||||
@ -1,21 +1,22 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import random
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from functools import cached_property
|
||||||
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
|
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
|
||||||
from dcs.unittype import ShipType
|
from dcs.unittype import ShipType, StaticType
|
||||||
|
from dcs.unittype import UnitType as DcsUnitType
|
||||||
|
|
||||||
from game.data.building_data import (
|
from game.data.building_data import (
|
||||||
WW2_ALLIES_BUILDINGS,
|
WW2_ALLIES_BUILDINGS,
|
||||||
DEFAULT_AVAILABLE_BUILDINGS,
|
DEFAULT_AVAILABLE_BUILDINGS,
|
||||||
WW2_GERMANY_BUILDINGS,
|
WW2_GERMANY_BUILDINGS,
|
||||||
WW2_FREE,
|
WW2_FREE,
|
||||||
|
REQUIRED_BUILDINGS,
|
||||||
)
|
)
|
||||||
from game.data.doctrine import (
|
from game.data.doctrine import (
|
||||||
Doctrine,
|
Doctrine,
|
||||||
@ -24,18 +25,12 @@ from game.data.doctrine import (
|
|||||||
WWII_DOCTRINE,
|
WWII_DOCTRINE,
|
||||||
)
|
)
|
||||||
from game.data.units import UnitClass
|
from game.data.units import UnitClass
|
||||||
from game.data.groups import GroupRole, GroupTask
|
from game.data.groups import GroupRole
|
||||||
from game import db
|
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.dcs.shipunittype import ShipUnitType
|
from game.dcs.shipunittype import ShipUnitType
|
||||||
from game.dcs.unitgroup import UnitGroup
|
from game.armedforces.forcegroup import ForceGroup
|
||||||
from game.dcs.unittype import UnitType
|
from game.dcs.unittype import UnitType
|
||||||
from gen.templates import (
|
|
||||||
GroundObjectTemplates,
|
|
||||||
GroundObjectTemplate,
|
|
||||||
GroupTemplate,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.theater.start_generator import ModSettings
|
from game.theater.start_generator import ModSettings
|
||||||
@ -84,7 +79,7 @@ class Faction:
|
|||||||
air_defense_units: List[GroundUnitType] = field(default_factory=list)
|
air_defense_units: List[GroundUnitType] = field(default_factory=list)
|
||||||
|
|
||||||
# A list of all supported sets of units
|
# A list of all supported sets of units
|
||||||
preset_groups: list[UnitGroup] = field(default_factory=list)
|
preset_groups: list[ForceGroup] = field(default_factory=list)
|
||||||
|
|
||||||
# Possible Missile site generators for this faction
|
# Possible Missile site generators for this faction
|
||||||
missiles: List[GroundUnitType] = field(default_factory=list)
|
missiles: List[GroundUnitType] = field(default_factory=list)
|
||||||
@ -110,7 +105,7 @@ class Faction:
|
|||||||
# doctrine
|
# doctrine
|
||||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||||
|
|
||||||
# List of available building templates for this faction
|
# List of available building layouts for this faction
|
||||||
building_set: List[str] = field(default_factory=list)
|
building_set: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
# List of default livery overrides
|
# List of default livery overrides
|
||||||
@ -125,47 +120,24 @@ class Faction:
|
|||||||
#: both will use it.
|
#: both will use it.
|
||||||
unrestricted_satnav: bool = False
|
unrestricted_satnav: bool = False
|
||||||
|
|
||||||
# All possible templates which can be generated by the faction
|
def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
|
||||||
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
|
# Vehicle and Ship Units
|
||||||
|
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
|
||||||
# All available unit_groups
|
|
||||||
unit_groups: dict[GroupRole, list[UnitGroup]] = field(default_factory=dict)
|
|
||||||
|
|
||||||
# Save all accessible units for performance increase
|
|
||||||
_accessible_units: list[UnitType[Any]] = field(default_factory=list)
|
|
||||||
|
|
||||||
def __getitem__(self, item: str) -> Any:
|
|
||||||
return getattr(self, item)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def accessible_units(self) -> Iterator[UnitType[Any]]:
|
|
||||||
yield from self._accessible_units
|
|
||||||
|
|
||||||
@property
|
|
||||||
def air_defenses(self) -> list[str]:
|
|
||||||
"""Returns the Air Defense types"""
|
|
||||||
air_defenses = [a.name for a in self.air_defense_units]
|
|
||||||
air_defenses.extend(
|
|
||||||
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AntiAir]
|
|
||||||
)
|
|
||||||
return sorted(air_defenses)
|
|
||||||
|
|
||||||
def has_access_to_unit_type(self, unit_type: str) -> bool:
|
|
||||||
# GroundUnits
|
|
||||||
if any(unit_type == u.dcs_id for u in self.accessible_units):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Statics
|
# Statics
|
||||||
if db.static_type_from_name(unit_type) is not None:
|
if issubclass(unit_type, StaticType):
|
||||||
# TODO Improve the statics checking
|
# TODO Improve the statics checking
|
||||||
|
# We currently do not have any list or similar to check if a faction has
|
||||||
|
# access to a specific static. There we accept any static here
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
|
def has_access_to_unit_class(self, unit_class: UnitClass) -> bool:
|
||||||
return any(unit.unit_class is unit_class for unit in self.accessible_units)
|
return any(unit.unit_class is unit_class for unit in self.accessible_units)
|
||||||
|
|
||||||
def _load_accessible_units(self, templates: GroundObjectTemplates) -> None:
|
@cached_property
|
||||||
self._accessible_units = []
|
def accessible_units(self) -> list[UnitType[Any]]:
|
||||||
all_units: Iterator[UnitType[Any]] = itertools.chain(
|
all_units: Iterator[UnitType[Any]] = itertools.chain(
|
||||||
self.ground_units,
|
self.ground_units,
|
||||||
self.infantry_units,
|
self.infantry_units,
|
||||||
@ -173,138 +145,22 @@ class Faction:
|
|||||||
self.naval_units,
|
self.naval_units,
|
||||||
self.missiles,
|
self.missiles,
|
||||||
(
|
(
|
||||||
ground_unit
|
unit
|
||||||
for preset_group in self.preset_groups
|
for preset_group in self.preset_groups
|
||||||
for ground_unit in preset_group.ground_units
|
for unit in preset_group.units
|
||||||
),
|
|
||||||
(
|
|
||||||
ship_unit
|
|
||||||
for preset_group in self.preset_groups
|
|
||||||
for ship_unit in preset_group.ship_units
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for unit in all_units:
|
return list(all_units)
|
||||||
if unit not in self._accessible_units:
|
|
||||||
self._accessible_units.append(unit)
|
|
||||||
|
|
||||||
def initialize(
|
@property
|
||||||
self, all_templates: GroundObjectTemplates, mod_settings: ModSettings
|
def air_defenses(self) -> list[str]:
|
||||||
) -> None:
|
"""Returns the Air Defense types"""
|
||||||
# Apply the mod settings
|
# This is used for the faction overview in NewGameWizard
|
||||||
self._apply_mod_settings(mod_settings)
|
air_defenses = [a.name for a in self.air_defense_units]
|
||||||
# Load all accessible units and store them for performant later usage
|
air_defenses.extend(
|
||||||
self._load_accessible_units(all_templates)
|
[pg.name for pg in self.preset_groups if pg.role == GroupRole.AIR_DEFENSE]
|
||||||
# Load all faction compatible templates
|
|
||||||
self._load_templates(all_templates)
|
|
||||||
# Load Unit Groups
|
|
||||||
self._load_unit_groups()
|
|
||||||
|
|
||||||
def _add_unit_group(self, unit_group: UnitGroup, merge: bool = True) -> None:
|
|
||||||
if not unit_group.templates:
|
|
||||||
unit_group.load_templates(self)
|
|
||||||
if not unit_group.templates:
|
|
||||||
# Empty templates will throw an error on generation
|
|
||||||
logging.error(
|
|
||||||
f"Skipping Unit group {unit_group.name} as no templates are available to generate the group"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if unit_group.role in self.unit_groups:
|
|
||||||
for group in self.unit_groups[unit_group.role]:
|
|
||||||
if merge and all(task in group.tasks for task in unit_group.tasks):
|
|
||||||
# Update existing group if same tasking
|
|
||||||
group.update_from_unit_group(unit_group)
|
|
||||||
return
|
|
||||||
# Add new Unit_group
|
|
||||||
self.unit_groups[unit_group.role].append(unit_group)
|
|
||||||
else:
|
|
||||||
self.unit_groups[unit_group.role] = [unit_group]
|
|
||||||
|
|
||||||
def _load_unit_groups(self) -> None:
|
|
||||||
# This function will create all the UnitGroups for the faction
|
|
||||||
# It will create a unit group for each global Template, Building or
|
|
||||||
# Legacy supported templates (not yet migrated from the generators).
|
|
||||||
# For every preset_group there will be a separate UnitGroup so no mixed
|
|
||||||
# UnitGroups will be generated for them. Special groups like complex SAM Systems
|
|
||||||
self.unit_groups = {}
|
|
||||||
|
|
||||||
# Generate UnitGroups for all global templates
|
|
||||||
for role, template in self.templates.templates:
|
|
||||||
if template.generic or role == GroupRole.Building:
|
|
||||||
# Build groups for global templates and buildings
|
|
||||||
self._add_group_for_template(role, template)
|
|
||||||
|
|
||||||
# Add preset groups
|
|
||||||
for preset_group in self.preset_groups:
|
|
||||||
# Add as separate group, do not merge with generic groups!
|
|
||||||
self._add_unit_group(preset_group, False)
|
|
||||||
|
|
||||||
def _add_group_for_template(
|
|
||||||
self, role: GroupRole, template: GroundObjectTemplate
|
|
||||||
) -> None:
|
|
||||||
unit_group = UnitGroup(
|
|
||||||
f"{role.value}: {', '.join([t.value for t in template.tasks])}",
|
|
||||||
[u for u in template.units if isinstance(u, GroundUnitType)],
|
|
||||||
[u for u in template.units if isinstance(u, ShipUnitType)],
|
|
||||||
list(template.statics),
|
|
||||||
role,
|
|
||||||
)
|
)
|
||||||
unit_group.tasks = template.tasks
|
return sorted(air_defenses)
|
||||||
unit_group.set_templates([template])
|
|
||||||
self._add_unit_group(unit_group)
|
|
||||||
|
|
||||||
def initialize_group_template(
|
|
||||||
self, group: GroupTemplate, faction_sensitive: bool = True
|
|
||||||
) -> bool:
|
|
||||||
# Sensitive defines if the initialization should check if the unit is available
|
|
||||||
# to this faction or not. It is disabled for migration only atm.
|
|
||||||
unit_types = [
|
|
||||||
t
|
|
||||||
for t in group.unit_types
|
|
||||||
if not faction_sensitive or self.has_access_to_unit_type(t)
|
|
||||||
]
|
|
||||||
|
|
||||||
alternative_types = []
|
|
||||||
for accessible_unit in self.accessible_units:
|
|
||||||
if accessible_unit.unit_class in group.unit_classes:
|
|
||||||
unit_types.append(accessible_unit.dcs_id)
|
|
||||||
if accessible_unit.unit_class in group.alternative_classes:
|
|
||||||
alternative_types.append(accessible_unit.dcs_id)
|
|
||||||
|
|
||||||
if not unit_types and not alternative_types and not group.optional:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
types = unit_types or alternative_types
|
|
||||||
group.set_possible_types(types)
|
|
||||||
return len(types) > 0
|
|
||||||
|
|
||||||
def _load_templates(self, all_templates: GroundObjectTemplates) -> None:
|
|
||||||
self.templates = GroundObjectTemplates()
|
|
||||||
# This loads all templates which are usable by the faction
|
|
||||||
for role, template in all_templates.templates:
|
|
||||||
# Make a deep copy of a template and add it to the template_list.
|
|
||||||
# This is required to have faction independent templates. Otherwise
|
|
||||||
# the reference would be the same and changes would affect all.
|
|
||||||
faction_template = copy.deepcopy(template)
|
|
||||||
try:
|
|
||||||
faction_template.groups[:] = [
|
|
||||||
group_template
|
|
||||||
for group_template in faction_template.groups
|
|
||||||
if self.initialize_group_template(group_template)
|
|
||||||
]
|
|
||||||
if (
|
|
||||||
role == GroupRole.Building
|
|
||||||
and GroupTask.StrikeTarget in template.tasks
|
|
||||||
and faction_template.category not in self.building_set
|
|
||||||
):
|
|
||||||
# Special handling for strike targets. Skip if not supported by faction
|
|
||||||
continue
|
|
||||||
if faction_template.groups:
|
|
||||||
self.templates.add_template(role, faction_template)
|
|
||||||
continue
|
|
||||||
except StopIteration:
|
|
||||||
pass
|
|
||||||
|
|
||||||
logging.info(f"{self.name} can not use template {template.name}")
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||||
@ -350,8 +206,13 @@ class Faction:
|
|||||||
]
|
]
|
||||||
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
|
faction.missiles = [GroundUnitType.named(n) for n in json.get("missiles", [])]
|
||||||
|
|
||||||
|
faction.naval_units = [
|
||||||
|
ShipUnitType.named(n) for n in json.get("naval_units", [])
|
||||||
|
]
|
||||||
|
|
||||||
|
# This has to be loaded AFTER GroundUnitType and ShipUnitType to work properly
|
||||||
faction.preset_groups = [
|
faction.preset_groups = [
|
||||||
UnitGroup.named(n) for n in json.get("preset_groups", [])
|
ForceGroup.named(n) for n in json.get("preset_groups", [])
|
||||||
]
|
]
|
||||||
|
|
||||||
faction.requirements = json.get("requirements", {})
|
faction.requirements = json.get("requirements", {})
|
||||||
@ -359,10 +220,6 @@ class Faction:
|
|||||||
faction.carrier_names = json.get("carrier_names", [])
|
faction.carrier_names = json.get("carrier_names", [])
|
||||||
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
|
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
|
||||||
|
|
||||||
faction.naval_units = [
|
|
||||||
ShipUnitType.named(n) for n in json.get("naval_units", [])
|
|
||||||
]
|
|
||||||
|
|
||||||
faction.has_jtac = json.get("has_jtac", False)
|
faction.has_jtac = json.get("has_jtac", False)
|
||||||
jtac_name = json.get("jtac_unit", None)
|
jtac_name = json.get("jtac_unit", None)
|
||||||
if jtac_name is not None:
|
if jtac_name is not None:
|
||||||
@ -394,6 +251,9 @@ class Faction:
|
|||||||
else:
|
else:
|
||||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||||
|
|
||||||
|
# Add required buildings for the game logic (e.g. ammo, factory..)
|
||||||
|
faction.building_set.extend(REQUIRED_BUILDINGS)
|
||||||
|
|
||||||
# Load liveries override
|
# Load liveries override
|
||||||
faction.liveries_overrides = {}
|
faction.liveries_overrides = {}
|
||||||
liveries_overrides = json.get("liveries_overrides", {})
|
liveries_overrides = json.get("liveries_overrides", {})
|
||||||
@ -403,9 +263,6 @@ class Faction:
|
|||||||
|
|
||||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||||
|
|
||||||
# Templates
|
|
||||||
faction.templates = GroundObjectTemplates()
|
|
||||||
|
|
||||||
return faction
|
return faction
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -419,44 +276,7 @@ class Faction:
|
|||||||
if unit.unit_class is unit_class:
|
if unit.unit_class is unit_class:
|
||||||
yield unit
|
yield unit
|
||||||
|
|
||||||
def groups_for_role_and_task(
|
def apply_mod_settings(self, mod_settings: ModSettings) -> None:
|
||||||
self, group_role: GroupRole, group_task: Optional[GroupTask] = None
|
|
||||||
) -> list[UnitGroup]:
|
|
||||||
if group_role not in self.unit_groups:
|
|
||||||
return []
|
|
||||||
groups = []
|
|
||||||
for unit_group in self.unit_groups[group_role]:
|
|
||||||
if not group_task or group_task in unit_group.tasks:
|
|
||||||
groups.append(unit_group)
|
|
||||||
return groups
|
|
||||||
|
|
||||||
def groups_for_role_and_tasks(
|
|
||||||
self, group_role: GroupRole, tasks: list[GroupTask]
|
|
||||||
) -> list[UnitGroup]:
|
|
||||||
groups = []
|
|
||||||
for task in tasks:
|
|
||||||
for group in self.groups_for_role_and_task(group_role, task):
|
|
||||||
if group not in groups:
|
|
||||||
groups.append(group)
|
|
||||||
return groups
|
|
||||||
|
|
||||||
def random_group_for_role(self, group_role: GroupRole) -> Optional[UnitGroup]:
|
|
||||||
unit_groups = self.groups_for_role_and_task(group_role)
|
|
||||||
return random.choice(unit_groups) if unit_groups else None
|
|
||||||
|
|
||||||
def random_group_for_role_and_task(
|
|
||||||
self, group_role: GroupRole, group_task: GroupTask
|
|
||||||
) -> Optional[UnitGroup]:
|
|
||||||
unit_groups = self.groups_for_role_and_task(group_role, group_task)
|
|
||||||
return random.choice(unit_groups) if unit_groups else None
|
|
||||||
|
|
||||||
def random_group_for_role_and_tasks(
|
|
||||||
self, group_role: GroupRole, tasks: list[GroupTask]
|
|
||||||
) -> Optional[UnitGroup]:
|
|
||||||
unit_groups = self.groups_for_role_and_tasks(group_role, tasks)
|
|
||||||
return random.choice(unit_groups) if unit_groups else None
|
|
||||||
|
|
||||||
def _apply_mod_settings(self, mod_settings: ModSettings) -> None:
|
|
||||||
# aircraft
|
# aircraft
|
||||||
if not mod_settings.a4_skyhawk:
|
if not mod_settings.a4_skyhawk:
|
||||||
self.remove_aircraft("A-4E-C")
|
self.remove_aircraft("A-4E-C")
|
||||||
@ -516,20 +336,20 @@ class Faction:
|
|||||||
self.remove_vehicle("KORNET")
|
self.remove_vehicle("KORNET")
|
||||||
# high digit sams
|
# high digit sams
|
||||||
if not mod_settings.high_digit_sams:
|
if not mod_settings.high_digit_sams:
|
||||||
self.remove_presets("SA-10B/S-300PS")
|
self.remove_preset("SA-10B/S-300PS")
|
||||||
self.remove_presets("SA-12/S-300V")
|
self.remove_preset("SA-12/S-300V")
|
||||||
self.remove_presets("SA-20/S-300PMU-1")
|
self.remove_preset("SA-20/S-300PMU-1")
|
||||||
self.remove_presets("SA-20B/S-300PMU-2")
|
self.remove_preset("SA-20B/S-300PMU-2")
|
||||||
self.remove_presets("SA-23/S-300VM")
|
self.remove_preset("SA-23/S-300VM")
|
||||||
self.remove_presets("SA-17")
|
self.remove_preset("SA-17")
|
||||||
self.remove_presets("KS-19")
|
self.remove_preset("KS-19")
|
||||||
|
|
||||||
def remove_aircraft(self, name: str) -> None:
|
def remove_aircraft(self, name: str) -> None:
|
||||||
for i in self.aircrafts:
|
for i in self.aircrafts:
|
||||||
if i.dcs_unit_type.id == name:
|
if i.dcs_unit_type.id == name:
|
||||||
self.aircrafts.remove(i)
|
self.aircrafts.remove(i)
|
||||||
|
|
||||||
def remove_presets(self, name: str) -> None:
|
def remove_preset(self, name: str) -> None:
|
||||||
for pg in self.preset_groups:
|
for pg in self.preset_groups:
|
||||||
if pg.name == name:
|
if pg.name == name:
|
||||||
self.preset_groups.remove(pg)
|
self.preset_groups.remove(pg)
|
||||||
|
|||||||
@ -500,7 +500,7 @@ class Game:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def iads_considerate_culling(self, tgo: TheaterGroundObject[Any]) -> bool:
|
def iads_considerate_culling(self, tgo: TheaterGroundObject) -> bool:
|
||||||
if not self.settings.perf_do_not_cull_threatening_iads:
|
if not self.settings.perf_do_not_cull_threatening_iads:
|
||||||
return self.position_culled(tgo.position)
|
return self.position_culled(tgo.position)
|
||||||
else:
|
else:
|
||||||
|
|||||||
4
game/layout/__init__.py
Normal file
4
game/layout/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from layout import TheaterLayout
|
||||||
|
from game.layout.layoutloader import LayoutLoader
|
||||||
|
|
||||||
|
LAYOUTS = LayoutLoader()
|
||||||
268
game/layout/layout.py
Normal file
268
game/layout/layout.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import TYPE_CHECKING, Type
|
||||||
|
|
||||||
|
from dcs import Point
|
||||||
|
from dcs.unit import Unit
|
||||||
|
from dcs.unittype import UnitType as DcsUnitType
|
||||||
|
|
||||||
|
from game.data.groups import GroupRole, GroupTask
|
||||||
|
from game.data.units import UnitClass
|
||||||
|
from game.point_with_heading import PointWithHeading
|
||||||
|
from game.theater.theatergroundobject import (
|
||||||
|
SamGroundObject,
|
||||||
|
EwrGroundObject,
|
||||||
|
BuildingGroundObject,
|
||||||
|
MissileSiteGroundObject,
|
||||||
|
ShipGroundObject,
|
||||||
|
CarrierGroundObject,
|
||||||
|
LhaGroundObject,
|
||||||
|
CoastalSiteGroundObject,
|
||||||
|
VehicleGroupGroundObject,
|
||||||
|
IadsGroundObject,
|
||||||
|
)
|
||||||
|
from game.theater.theatergroup import TheaterUnit
|
||||||
|
from game.utils import Heading
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.factions.faction import Faction
|
||||||
|
from game.theater.theatergroundobject import TheaterGroundObject
|
||||||
|
from game.theater.controlpoint import ControlPoint
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LayoutUnit:
|
||||||
|
"""The Position and Orientation of a single unit within the GroupLayout"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
position: Point
|
||||||
|
heading: int
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_unit(unit: Unit) -> LayoutUnit:
|
||||||
|
"""Creates a LayoutUnit from a DCS Unit"""
|
||||||
|
return LayoutUnit(
|
||||||
|
unit.name,
|
||||||
|
Point(int(unit.position.x), int(unit.position.y)),
|
||||||
|
int(unit.heading),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GroupLayout:
|
||||||
|
"""The Layout of a TheaterGroup"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
units: list[LayoutUnit]
|
||||||
|
|
||||||
|
# The group this template will be merged into
|
||||||
|
group: int = 1
|
||||||
|
|
||||||
|
# Define the amount of random units to be created by the randomizer.
|
||||||
|
# This can be a fixed int or a random value from a range of two ints as tuple
|
||||||
|
unit_count: list[int] = field(default_factory=list)
|
||||||
|
|
||||||
|
# defintion which unit types are supported
|
||||||
|
unit_types: list[Type[DcsUnitType]] = field(default_factory=list)
|
||||||
|
unit_classes: list[UnitClass] = field(default_factory=list)
|
||||||
|
alternative_classes: list[UnitClass] = field(default_factory=list)
|
||||||
|
|
||||||
|
# Defines if this groupTemplate is required or not
|
||||||
|
optional: bool = False
|
||||||
|
|
||||||
|
# if enabled the specific group will be generated during generation
|
||||||
|
# Can only be set to False if Optional = True
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
# TODO Caching for faction!
|
||||||
|
def possible_types_for_faction(self, faction: Faction) -> list[Type[DcsUnitType]]:
|
||||||
|
"""TODO Description"""
|
||||||
|
unit_types = [t for t in self.unit_types if faction.has_access_to_dcs_type(t)]
|
||||||
|
|
||||||
|
alternative_types = []
|
||||||
|
for accessible_unit in faction.accessible_units:
|
||||||
|
if accessible_unit.unit_class in self.unit_classes:
|
||||||
|
unit_types.append(accessible_unit.dcs_unit_type)
|
||||||
|
if accessible_unit.unit_class in self.alternative_classes:
|
||||||
|
alternative_types.append(accessible_unit.dcs_unit_type)
|
||||||
|
|
||||||
|
if not unit_types and not alternative_types and not self.optional:
|
||||||
|
raise LayoutException
|
||||||
|
|
||||||
|
return unit_types or alternative_types
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_counter(self) -> int:
|
||||||
|
"""TODO Documentation"""
|
||||||
|
default = len(self.units)
|
||||||
|
if self.unit_count:
|
||||||
|
if len(self.unit_count) == 1:
|
||||||
|
count = self.unit_count[0]
|
||||||
|
else:
|
||||||
|
count = random.choice(range(min(self.unit_count), max(self.unit_count)))
|
||||||
|
if count > default:
|
||||||
|
logging.error(
|
||||||
|
f"UnitCount for Group Layout {self.name} "
|
||||||
|
f"exceeds max available units for this group"
|
||||||
|
)
|
||||||
|
return default
|
||||||
|
return count
|
||||||
|
return default
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_size(self) -> int:
|
||||||
|
return len(self.units)
|
||||||
|
|
||||||
|
def generate_units(
|
||||||
|
self, go: TheaterGroundObject, unit_type: Type[DcsUnitType], amount: int
|
||||||
|
) -> list[TheaterUnit]:
|
||||||
|
"""TODO Documentation"""
|
||||||
|
return [
|
||||||
|
TheaterUnit.from_template(i, unit_type, self.units[i], go)
|
||||||
|
for i in range(amount)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TheaterLayout:
|
||||||
|
"""TODO Documentation"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, role: GroupRole, description: str = "") -> None:
|
||||||
|
self.name = name
|
||||||
|
self.role = role
|
||||||
|
self.description = description
|
||||||
|
self.tasks: list[GroupTask] = [] # The supported tasks
|
||||||
|
self.groups: list[GroupLayout] = []
|
||||||
|
|
||||||
|
# If the template is generic it will be used the generate the general
|
||||||
|
# UnitGroups during faction initialization. Generic Groups allow to be mixed
|
||||||
|
self.generic: bool = False
|
||||||
|
|
||||||
|
def usable_by_faction(self, faction: Faction) -> bool:
|
||||||
|
# Special handling for Buildings
|
||||||
|
if (
|
||||||
|
isinstance(self, BuildingLayout)
|
||||||
|
and self.category not in faction.building_set
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if faction has at least 1 possible unit for non-optional groups
|
||||||
|
try:
|
||||||
|
return all(
|
||||||
|
len(group.possible_types_for_faction(faction)) > 0
|
||||||
|
for group in self.groups
|
||||||
|
if not group.optional
|
||||||
|
)
|
||||||
|
except LayoutException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_ground_object(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
position: PointWithHeading,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
) -> TheaterGroundObject:
|
||||||
|
"""TODO Documentation"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def add_group(self, new_group: GroupLayout, index: int = 0) -> None:
|
||||||
|
"""Adds a group in the correct order to the template"""
|
||||||
|
if len(self.groups) > index:
|
||||||
|
self.groups.insert(index, new_group)
|
||||||
|
else:
|
||||||
|
self.groups.append(new_group)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self) -> int:
|
||||||
|
return sum([len(group.units) for group in self.groups])
|
||||||
|
|
||||||
|
|
||||||
|
class AntiAirLayout(TheaterLayout):
|
||||||
|
def create_ground_object(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
position: PointWithHeading,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
) -> IadsGroundObject:
|
||||||
|
|
||||||
|
if GroupTask.EARLY_WARNING_RADAR in self.tasks:
|
||||||
|
return EwrGroundObject(name, position, position.heading, control_point)
|
||||||
|
elif any(tasking in self.tasks for tasking in GroupRole.AIR_DEFENSE.tasks):
|
||||||
|
return SamGroundObject(name, position, position.heading, control_point)
|
||||||
|
raise RuntimeError(
|
||||||
|
f" No Template for AntiAir tasking ({', '.join(task.description for task in self.tasks)})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildingLayout(TheaterLayout):
|
||||||
|
def create_ground_object(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
position: PointWithHeading,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
) -> BuildingGroundObject:
|
||||||
|
return BuildingGroundObject(
|
||||||
|
name,
|
||||||
|
self.category,
|
||||||
|
position,
|
||||||
|
Heading.from_degrees(0),
|
||||||
|
control_point,
|
||||||
|
self.category == "fob",
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category(self) -> str:
|
||||||
|
for task in self.tasks:
|
||||||
|
if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]:
|
||||||
|
return task.description.lower()
|
||||||
|
raise RuntimeError(f"Building Template {self.name} has no building category")
|
||||||
|
|
||||||
|
|
||||||
|
class NavalLayout(TheaterLayout):
|
||||||
|
def create_ground_object(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
position: PointWithHeading,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
) -> TheaterGroundObject:
|
||||||
|
if GroupTask.NAVY in self.tasks:
|
||||||
|
return ShipGroundObject(name, position, control_point)
|
||||||
|
elif GroupTask.AIRCRAFT_CARRIER in self.tasks:
|
||||||
|
return CarrierGroundObject(name, control_point)
|
||||||
|
elif GroupTask.HELICOPTER_CARRIER in self.tasks:
|
||||||
|
return LhaGroundObject(name, control_point)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class DefensesLayout(TheaterLayout):
|
||||||
|
def create_ground_object(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
position: PointWithHeading,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
) -> TheaterGroundObject:
|
||||||
|
if GroupTask.MISSILE in self.tasks:
|
||||||
|
return MissileSiteGroundObject(
|
||||||
|
name, position, position.heading, control_point
|
||||||
|
)
|
||||||
|
elif GroupTask.COASTAL in self.tasks:
|
||||||
|
return CoastalSiteGroundObject(
|
||||||
|
name, position, control_point, position.heading
|
||||||
|
)
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class GroundForceLayout(TheaterLayout):
|
||||||
|
def create_ground_object(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
position: PointWithHeading,
|
||||||
|
control_point: ControlPoint,
|
||||||
|
) -> TheaterGroundObject:
|
||||||
|
return VehicleGroupGroundObject(name, position, position.heading, control_point)
|
||||||
203
game/layout/layoutloader.py
Normal file
203
game/layout/layoutloader.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import logging
|
||||||
|
import pickle
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterator
|
||||||
|
|
||||||
|
import dcs
|
||||||
|
import yaml
|
||||||
|
from dcs import Point
|
||||||
|
from dcs.unitgroup import StaticGroup
|
||||||
|
|
||||||
|
from game import persistency
|
||||||
|
from game.data.groups import GroupRole, GroupTask
|
||||||
|
from game.layout.layout import (
|
||||||
|
TheaterLayout,
|
||||||
|
GroupLayout,
|
||||||
|
LayoutUnit,
|
||||||
|
AntiAirLayout,
|
||||||
|
BuildingLayout,
|
||||||
|
NavalLayout,
|
||||||
|
GroundForceLayout,
|
||||||
|
DefensesLayout,
|
||||||
|
)
|
||||||
|
from game.layout.layoutmapping import GroupLayoutMapping, LayoutMapping
|
||||||
|
from game.profiling import logged_duration
|
||||||
|
from game.version import VERSION
|
||||||
|
|
||||||
|
TEMPLATE_DIR = "resources/layouts/"
|
||||||
|
TEMPLATE_DUMP = "Liberation/layouts.p"
|
||||||
|
|
||||||
|
TEMPLATE_TYPES = {
|
||||||
|
GroupRole.AIR_DEFENSE: AntiAirLayout,
|
||||||
|
GroupRole.BUILDING: BuildingLayout,
|
||||||
|
GroupRole.NAVAL: NavalLayout,
|
||||||
|
GroupRole.GROUND_FORCE: GroundForceLayout,
|
||||||
|
GroupRole.DEFENSES: DefensesLayout,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutLoader:
|
||||||
|
# list of layouts per category. e.g. AA or similar
|
||||||
|
_templates: dict[str, TheaterLayout] = {}
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._templates = {}
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
if not self._templates:
|
||||||
|
with logged_duration("Loading layouts"):
|
||||||
|
self.load_templates()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layouts(self) -> Iterator[TheaterLayout]:
|
||||||
|
self.initialize()
|
||||||
|
yield from self._templates.values()
|
||||||
|
|
||||||
|
def load_templates(self) -> None:
|
||||||
|
"""This will load all pre-loaded layouts from a pickle file.
|
||||||
|
If pickle can not be loaded it will import and dump the layouts"""
|
||||||
|
# We use a pickle for performance reasons. Importing takes many seconds
|
||||||
|
file = Path(persistency.base_path()) / TEMPLATE_DUMP
|
||||||
|
if file.is_file():
|
||||||
|
# Load from pickle if existing
|
||||||
|
with file.open("rb") as f:
|
||||||
|
try:
|
||||||
|
version, self._templates = pickle.load(f)
|
||||||
|
# Check if the game version of the dump is identical to the current
|
||||||
|
if version == VERSION:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error {e} reading layouts dump. Recreating.")
|
||||||
|
# If no dump is available or game version is different create a new dump
|
||||||
|
self.import_templates()
|
||||||
|
|
||||||
|
def import_templates(self) -> None:
|
||||||
|
"""This will import all layouts from the template folder
|
||||||
|
and dumps them to a pickle"""
|
||||||
|
mappings: dict[str, list[LayoutMapping]] = {}
|
||||||
|
with logged_duration("Parsing mapping yamls"):
|
||||||
|
for file in Path(TEMPLATE_DIR).rglob("*.yaml"):
|
||||||
|
if not file.is_file():
|
||||||
|
continue
|
||||||
|
with file.open("r", encoding="utf-8") as f:
|
||||||
|
mapping_dict = yaml.safe_load(f)
|
||||||
|
|
||||||
|
template_map = LayoutMapping.from_dict(mapping_dict, f.name)
|
||||||
|
|
||||||
|
if template_map.layout_file in mappings:
|
||||||
|
mappings[template_map.layout_file].append(template_map)
|
||||||
|
else:
|
||||||
|
mappings[template_map.layout_file] = [template_map]
|
||||||
|
|
||||||
|
with logged_duration(f"Parsing all layout miz multithreaded"):
|
||||||
|
with ThreadPoolExecutor() as exe:
|
||||||
|
for miz, maps in mappings.items():
|
||||||
|
exe.submit(self._load_from_miz, miz, maps)
|
||||||
|
|
||||||
|
logging.info(f"Imported {len(self._templates)} layouts")
|
||||||
|
self._dump_templates()
|
||||||
|
|
||||||
|
def _dump_templates(self) -> None:
|
||||||
|
file = Path(persistency.base_path()) / TEMPLATE_DUMP
|
||||||
|
dump = (VERSION, self._templates)
|
||||||
|
with file.open("wb") as fdata:
|
||||||
|
pickle.dump(dump, fdata)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def mapping_for_group(
|
||||||
|
mappings: list[LayoutMapping], group_name: str
|
||||||
|
) -> tuple[LayoutMapping, int, GroupLayoutMapping]:
|
||||||
|
for mapping in mappings:
|
||||||
|
for g_id, group_mapping in enumerate(mapping.groups):
|
||||||
|
if (
|
||||||
|
group_mapping.name == group_name
|
||||||
|
or group_name in group_mapping.statics
|
||||||
|
):
|
||||||
|
return mapping, g_id, group_mapping
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
def _load_from_miz(self, miz: str, mappings: list[LayoutMapping]) -> None:
|
||||||
|
template_position: dict[str, Point] = {}
|
||||||
|
temp_mis = dcs.Mission()
|
||||||
|
with logged_duration(f"Parsing {miz}"):
|
||||||
|
# The load_file takes a lot of time to compute. That's why the layouts
|
||||||
|
# are written to a pickle and can be reloaded from the ui
|
||||||
|
# Example the whole routine: 0:00:00.934417,
|
||||||
|
# the .load_file() method: 0:00:00.920409
|
||||||
|
temp_mis.load_file(miz)
|
||||||
|
|
||||||
|
for country in itertools.chain(
|
||||||
|
temp_mis.coalition["red"].countries.values(),
|
||||||
|
temp_mis.coalition["blue"].countries.values(),
|
||||||
|
):
|
||||||
|
for dcs_group in itertools.chain(
|
||||||
|
temp_mis.country(country.name).vehicle_group,
|
||||||
|
temp_mis.country(country.name).ship_group,
|
||||||
|
temp_mis.country(country.name).static_group,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
mapping, group_id, group_mapping = self.mapping_for_group(
|
||||||
|
mappings, dcs_group.name
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
logging.warning(f"No mapping for dcs group {dcs_group.name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
template = self._templates.get(mapping.name, None)
|
||||||
|
if template is None:
|
||||||
|
# Create a new template
|
||||||
|
template = TEMPLATE_TYPES[mapping.role](
|
||||||
|
mapping.name, mapping.role, mapping.description
|
||||||
|
)
|
||||||
|
template.generic = mapping.generic
|
||||||
|
template.tasks = mapping.tasks
|
||||||
|
self._templates[template.name] = template
|
||||||
|
|
||||||
|
for i, unit in enumerate(dcs_group.units):
|
||||||
|
group_template = None
|
||||||
|
for group in template.groups:
|
||||||
|
if group.name == group_mapping.name:
|
||||||
|
# We already have a layoutgroup for this dcs_group
|
||||||
|
group_template = group
|
||||||
|
if not group_template:
|
||||||
|
group_template = GroupLayout(
|
||||||
|
group_mapping.name,
|
||||||
|
[],
|
||||||
|
group_mapping.group,
|
||||||
|
group_mapping.unit_count,
|
||||||
|
group_mapping.unit_types,
|
||||||
|
group_mapping.unit_classes,
|
||||||
|
group_mapping.alternative_classes,
|
||||||
|
)
|
||||||
|
group_template.optional = group_mapping.optional
|
||||||
|
# Add the group at the correct position
|
||||||
|
template.add_group(group_template, group_id)
|
||||||
|
unit_template = LayoutUnit.from_unit(unit)
|
||||||
|
if i == 0 and template.name not in template_position:
|
||||||
|
template_position[template.name] = unit.position
|
||||||
|
unit_template.position = (
|
||||||
|
unit_template.position - template_position[template.name]
|
||||||
|
)
|
||||||
|
group_template.units.append(unit_template)
|
||||||
|
|
||||||
|
def by_name(self, template_name: str) -> Iterator[TheaterLayout]:
|
||||||
|
for template in self.layouts:
|
||||||
|
if template.name == template_name:
|
||||||
|
yield template
|
||||||
|
|
||||||
|
def by_task(self, group_task: GroupTask) -> Iterator[TheaterLayout]:
|
||||||
|
for template in self.layouts:
|
||||||
|
if not group_task or group_task in template.tasks:
|
||||||
|
yield template
|
||||||
|
|
||||||
|
def by_tasks(self, group_tasks: list[GroupTask]) -> Iterator[TheaterLayout]:
|
||||||
|
unique_templates = []
|
||||||
|
for group_task in group_tasks:
|
||||||
|
for template in self.by_task(group_task):
|
||||||
|
if template not in unique_templates:
|
||||||
|
unique_templates.append(template)
|
||||||
|
yield from unique_templates
|
||||||
156
game/layout/layoutmapping.py
Normal file
156
game/layout/layoutmapping.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Type
|
||||||
|
|
||||||
|
from dcs.unittype import UnitType as DcsUnitType
|
||||||
|
|
||||||
|
from game import db
|
||||||
|
from game.data.groups import GroupRole, GroupTask
|
||||||
|
from game.data.units import UnitClass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GroupLayoutMapping:
|
||||||
|
# The group name used in the template.miz
|
||||||
|
name: str
|
||||||
|
|
||||||
|
# Defines if the group is required for the template or can be skipped
|
||||||
|
optional: bool = False
|
||||||
|
|
||||||
|
# All static units for the group
|
||||||
|
statics: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
# Defines to which tgo group the groupTemplate will be added
|
||||||
|
# This allows to merge groups back together. Default: Merge all to group 1
|
||||||
|
group: int = field(default=1)
|
||||||
|
|
||||||
|
# How many units should be generated from the grouplayout. If only one value is
|
||||||
|
# added this will be an exact amount. If 2 values are used it will be a random
|
||||||
|
# amount between these values.
|
||||||
|
unit_count: list[int] = field(default_factory=list)
|
||||||
|
|
||||||
|
# All unit types the template supports.
|
||||||
|
unit_types: list[Type[DcsUnitType]] = field(default_factory=list)
|
||||||
|
|
||||||
|
# All unit classes the template supports.
|
||||||
|
unit_classes: list[UnitClass] = field(default_factory=list)
|
||||||
|
|
||||||
|
# TODO Clarify if this is required. Only used for EWRs to also Use SR when no
|
||||||
|
# dedicated EWRs are available to the faction
|
||||||
|
alternative_classes: list[UnitClass] = field(default_factory=list)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
d = self.__dict__
|
||||||
|
if not self.optional:
|
||||||
|
d.pop("optional")
|
||||||
|
if not self.statics:
|
||||||
|
d.pop("statics")
|
||||||
|
if not self.unit_types:
|
||||||
|
d.pop("unit_types")
|
||||||
|
if not self.unit_classes:
|
||||||
|
d.pop("unit_classes")
|
||||||
|
else:
|
||||||
|
d["unit_classes"] = [unit_class.value for unit_class in self.unit_classes]
|
||||||
|
if not self.alternative_classes:
|
||||||
|
d.pop("alternative_classes")
|
||||||
|
else:
|
||||||
|
d["alternative_classes"] = [
|
||||||
|
unit_class.value for unit_class in self.alternative_classes
|
||||||
|
]
|
||||||
|
if not self.unit_count:
|
||||||
|
d.pop("unit_count")
|
||||||
|
return d
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(d: dict[str, Any]) -> GroupLayoutMapping:
|
||||||
|
optional = d["optional"] if "optional" in d else False
|
||||||
|
statics = d["statics"] if "statics" in d else []
|
||||||
|
unit_count = d["unit_count"] if "unit_count" in d else []
|
||||||
|
unit_types = []
|
||||||
|
if "unit_types" in d:
|
||||||
|
for u in d["unit_types"]:
|
||||||
|
unit_type = db.unit_type_from_name(u)
|
||||||
|
if unit_type:
|
||||||
|
unit_types.append(unit_type)
|
||||||
|
group = d["group"] if "group" in d else 1
|
||||||
|
unit_classes = (
|
||||||
|
[UnitClass(u) for u in d["unit_classes"]] if "unit_classes" in d else []
|
||||||
|
)
|
||||||
|
alternative_classes = (
|
||||||
|
[UnitClass(u) for u in d["alternative_classes"]]
|
||||||
|
if "alternative_classes" in d
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
return GroupLayoutMapping(
|
||||||
|
d["name"],
|
||||||
|
optional,
|
||||||
|
statics,
|
||||||
|
group,
|
||||||
|
unit_count,
|
||||||
|
unit_types,
|
||||||
|
unit_classes,
|
||||||
|
alternative_classes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LayoutMapping:
|
||||||
|
# The name of the Template
|
||||||
|
name: str
|
||||||
|
|
||||||
|
# An optional description to give more information about the template
|
||||||
|
description: str
|
||||||
|
|
||||||
|
# Optional field to define if the template can be used to create generic groups
|
||||||
|
generic: bool
|
||||||
|
|
||||||
|
# The role the template can be used for
|
||||||
|
role: GroupRole
|
||||||
|
|
||||||
|
# All taskings the template can be used for
|
||||||
|
tasks: list[GroupTask]
|
||||||
|
|
||||||
|
# All Groups the template has
|
||||||
|
groups: list[GroupLayoutMapping]
|
||||||
|
|
||||||
|
# Define the miz file for the template. Optional. If empty use the mapping name
|
||||||
|
layout_file: str
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
d = {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.description,
|
||||||
|
"generic": self.generic,
|
||||||
|
"role": self.role.value,
|
||||||
|
"tasks": [task.description for task in self.tasks],
|
||||||
|
"groups": [group.to_dict() for group in self.groups],
|
||||||
|
"layout_file": self.layout_file,
|
||||||
|
}
|
||||||
|
if not self.description:
|
||||||
|
d.pop("description")
|
||||||
|
if not self.generic:
|
||||||
|
# Only save if true
|
||||||
|
d.pop("generic")
|
||||||
|
if not self.layout_file:
|
||||||
|
d.pop("layout_file")
|
||||||
|
return d
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_dict(d: dict[str, Any], file_name: str) -> LayoutMapping:
|
||||||
|
groups = [GroupLayoutMapping.from_dict(group) for group in d["groups"]]
|
||||||
|
description = d["description"] if "description" in d else ""
|
||||||
|
generic = d["generic"] if "generic" in d else False
|
||||||
|
layout_file = (
|
||||||
|
d["layout_file"] if "layout_file" in d else file_name.replace("yaml", "miz")
|
||||||
|
)
|
||||||
|
tasks = [GroupTask.by_description(task) for task in d["tasks"]]
|
||||||
|
return LayoutMapping(
|
||||||
|
d["name"],
|
||||||
|
description,
|
||||||
|
generic,
|
||||||
|
GroupRole(d["role"]),
|
||||||
|
tasks,
|
||||||
|
groups,
|
||||||
|
layout_file,
|
||||||
|
)
|
||||||
@ -12,7 +12,7 @@ from dcs.unitgroup import FlyingGroup
|
|||||||
from game.ato import Flight, FlightWaypoint
|
from game.ato import Flight, FlightWaypoint
|
||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
from game.missiongenerator.airsupport import AirSupport
|
from game.missiongenerator.airsupport import AirSupport
|
||||||
from game.theater import MissionTarget, GroundUnit
|
from game.theater import MissionTarget, TheaterUnit
|
||||||
|
|
||||||
TARGET_WAYPOINTS = (
|
TARGET_WAYPOINTS = (
|
||||||
FlightWaypointType.TARGET_GROUP_LOC,
|
FlightWaypointType.TARGET_GROUP_LOC,
|
||||||
@ -82,7 +82,7 @@ class PydcsWaypointBuilder:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def register_special_waypoints(
|
def register_special_waypoints(
|
||||||
self, targets: Iterable[Union[MissionTarget, GroundUnit]]
|
self, targets: Iterable[Union[MissionTarget, TheaterUnit]]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create special target waypoints for various aircraft"""
|
"""Create special target waypoints for various aircraft"""
|
||||||
for i, t in enumerate(targets):
|
for i, t in enumerate(targets):
|
||||||
|
|||||||
@ -221,7 +221,7 @@ class FlotGenerator:
|
|||||||
if self.game.settings.manpads:
|
if self.game.settings.manpads:
|
||||||
# 50% of armored units protected by manpad
|
# 50% of armored units protected by manpad
|
||||||
if random.choice([True, False]):
|
if random.choice([True, False]):
|
||||||
manpads = list(faction.infantry_with_class(UnitClass.Manpad))
|
manpads = list(faction.infantry_with_class(UnitClass.MANPAD))
|
||||||
if manpads:
|
if manpads:
|
||||||
u = random.choices(
|
u = random.choices(
|
||||||
manpads, weights=[m.spawn_weight for m in manpads]
|
manpads, weights=[m.spawn_weight for m in manpads]
|
||||||
@ -237,10 +237,10 @@ class FlotGenerator:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
possible_infantry_units = set(faction.infantry_with_class(UnitClass.Infantry))
|
possible_infantry_units = set(faction.infantry_with_class(UnitClass.INFANTRY))
|
||||||
if self.game.settings.manpads:
|
if self.game.settings.manpads:
|
||||||
possible_infantry_units |= set(
|
possible_infantry_units |= set(
|
||||||
faction.infantry_with_class(UnitClass.Manpad)
|
faction.infantry_with_class(UnitClass.MANPAD)
|
||||||
)
|
)
|
||||||
if not possible_infantry_units:
|
if not possible_infantry_units:
|
||||||
return
|
return
|
||||||
|
|||||||
@ -40,7 +40,7 @@ from game.ato.flightwaypointtype import FlightWaypointType
|
|||||||
from game.data.alic import AlicCodes
|
from game.data.alic import AlicCodes
|
||||||
from game.dcs.aircrafttype import AircraftType
|
from game.dcs.aircrafttype import AircraftType
|
||||||
from game.radio.radios import RadioFrequency
|
from game.radio.radios import RadioFrequency
|
||||||
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, GroundUnit
|
from game.theater import ConflictTheater, LatLon, TheaterGroundObject, TheaterUnit
|
||||||
from game.theater.bullseye import Bullseye
|
from game.theater.bullseye import Bullseye
|
||||||
from game.utils import Distance, UnitSystem, meters, mps, pounds
|
from game.utils import Distance, UnitSystem, meters, mps, pounds
|
||||||
from game.weather import Weather
|
from game.weather import Weather
|
||||||
@ -607,14 +607,14 @@ class SeadTaskPage(KneeboardPage):
|
|||||||
self.theater = theater
|
self.theater = theater
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_units(self) -> Iterator[GroundUnit]:
|
def target_units(self) -> Iterator[TheaterUnit]:
|
||||||
if isinstance(self.flight.package.target, TheaterGroundObject):
|
if isinstance(self.flight.package.target, TheaterGroundObject):
|
||||||
yield from self.flight.package.target.strike_targets
|
yield from self.flight.package.target.strike_targets
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def alic_for(unit_type: str) -> str:
|
def alic_for(unit: TheaterUnit) -> str:
|
||||||
try:
|
try:
|
||||||
return str(AlicCodes.code_for(unit_type))
|
return str(AlicCodes.code_for(unit))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@ -634,13 +634,13 @@ class SeadTaskPage(KneeboardPage):
|
|||||||
|
|
||||||
writer.write(path)
|
writer.write(path)
|
||||||
|
|
||||||
def target_info_row(self, unit: GroundUnit) -> List[str]:
|
def target_info_row(self, unit: TheaterUnit) -> List[str]:
|
||||||
ll = self.theater.point_to_ll(unit.position)
|
ll = self.theater.point_to_ll(unit.position)
|
||||||
unit_type = unit_type_from_name(unit.type)
|
unit_type = unit.type
|
||||||
name = unit.name if unit_type is None else unit_type.name
|
name = unit.name if unit_type is None else unit_type.name
|
||||||
return [
|
return [
|
||||||
name,
|
name,
|
||||||
self.alic_for(unit.type),
|
self.alic_for(unit),
|
||||||
ll.format_dms(include_decimal_seconds=True),
|
ll.format_dms(include_decimal_seconds=True),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -58,16 +58,14 @@ from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
|
|||||||
from game.dcs.helpers import static_type_from_name, unit_type_from_name
|
from game.dcs.helpers import static_type_from_name, unit_type_from_name
|
||||||
from game.radio.radios import RadioFrequency, RadioRegistry
|
from game.radio.radios import RadioFrequency, RadioRegistry
|
||||||
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
|
||||||
from game.theater import ControlPoint, TheaterGroundObject
|
from game.theater import ControlPoint, TheaterGroundObject, TheaterUnit
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
CarrierGroundObject,
|
CarrierGroundObject,
|
||||||
GenericCarrierGroundObject,
|
GenericCarrierGroundObject,
|
||||||
LhaGroundObject,
|
LhaGroundObject,
|
||||||
MissileSiteGroundObject,
|
MissileSiteGroundObject,
|
||||||
GroundGroup,
|
|
||||||
GroundUnit,
|
|
||||||
SceneryGroundUnit,
|
|
||||||
)
|
)
|
||||||
|
from game.theater.theatergroup import SceneryUnit, TheaterGroup
|
||||||
from game.unitmap import UnitMap
|
from game.unitmap import UnitMap
|
||||||
from game.utils import Heading, feet, knots, mps
|
from game.utils import Heading, feet, knots, mps
|
||||||
from gen.runways import RunwayData
|
from gen.runways import RunwayData
|
||||||
@ -100,95 +98,114 @@ class GroundObjectGenerator:
|
|||||||
def culled(self) -> bool:
|
def culled(self) -> bool:
|
||||||
return self.game.iads_considerate_culling(self.ground_object)
|
return self.game.iads_considerate_culling(self.ground_object)
|
||||||
|
|
||||||
def generate(self, unique_name: bool = True) -> None:
|
def generate(self) -> None:
|
||||||
if self.culled:
|
if self.culled:
|
||||||
return
|
return
|
||||||
|
|
||||||
for group in self.ground_object.groups:
|
for group in self.ground_object.groups:
|
||||||
if not group.units:
|
vehicle_units = []
|
||||||
logging.warning(f"Found empty group in {self.ground_object}")
|
ship_units = []
|
||||||
continue
|
# Split the different unit types to be compliant to dcs limitation
|
||||||
group_name = group.group_name if unique_name else group.name
|
for unit in group.units:
|
||||||
moving_group: Optional[MovingGroup[Any]] = None
|
if unit.is_static:
|
||||||
for i, unit in enumerate(group.units):
|
# A Static unit has to be a single static group
|
||||||
if isinstance(unit, SceneryGroundUnit):
|
self.create_static_group(unit)
|
||||||
# Special handling for scenery objects:
|
elif unit.is_vehicle and unit.alive:
|
||||||
# Only create a trigger zone and no "real" dcs unit
|
# All alive Vehicles
|
||||||
self.add_trigger_zone_for_scenery(unit)
|
vehicle_units.append(unit)
|
||||||
continue
|
elif unit.is_ship and unit.alive:
|
||||||
|
# All alive Ships
|
||||||
|
ship_units.append(unit)
|
||||||
|
if vehicle_units:
|
||||||
|
self.create_vehicle_group(group.group_name, vehicle_units)
|
||||||
|
if ship_units:
|
||||||
|
self.create_ship_group(group.group_name, ship_units)
|
||||||
|
|
||||||
# Only skip dead units after trigger zone for scenery created!
|
def create_vehicle_group(
|
||||||
if not unit.alive:
|
self, group_name: str, units: list[TheaterUnit]
|
||||||
continue
|
) -> VehicleGroup:
|
||||||
|
vehicle_group: Optional[VehicleGroup] = None
|
||||||
|
for unit in units:
|
||||||
|
assert issubclass(unit.type, VehicleType)
|
||||||
|
if vehicle_group is None:
|
||||||
|
vehicle_group = self.m.vehicle_group(
|
||||||
|
self.country,
|
||||||
|
group_name,
|
||||||
|
unit.type,
|
||||||
|
position=unit.position,
|
||||||
|
heading=unit.position.heading.degrees,
|
||||||
|
)
|
||||||
|
vehicle_group.units[0].player_can_drive = True
|
||||||
|
self.enable_eplrs(vehicle_group, unit.type)
|
||||||
|
vehicle_group.units[0].name = unit.unit_name
|
||||||
|
self.set_alarm_state(vehicle_group)
|
||||||
|
else:
|
||||||
|
vehicle_unit = Vehicle(
|
||||||
|
self.m.next_unit_id(),
|
||||||
|
unit.unit_name,
|
||||||
|
unit.type.id,
|
||||||
|
)
|
||||||
|
vehicle_unit.player_can_drive = True
|
||||||
|
vehicle_unit.position = unit.position
|
||||||
|
vehicle_unit.heading = unit.position.heading.degrees
|
||||||
|
vehicle_group.add_unit(vehicle_unit)
|
||||||
|
self._register_theater_unit(unit, vehicle_group.units[-1])
|
||||||
|
if vehicle_group is None:
|
||||||
|
raise RuntimeError(f"Error creating VehicleGroup for {group_name}")
|
||||||
|
return vehicle_group
|
||||||
|
|
||||||
unit_type = unit_type_from_name(unit.type)
|
def create_ship_group(
|
||||||
if not unit_type:
|
self,
|
||||||
raise RuntimeError(
|
group_name: str,
|
||||||
f"Unit type {unit.type} is not a valid dcs unit type"
|
units: list[TheaterUnit],
|
||||||
)
|
frequency: Optional[RadioFrequency] = None,
|
||||||
|
) -> ShipGroup:
|
||||||
|
ship_group: Optional[ShipGroup] = None
|
||||||
|
for unit in units:
|
||||||
|
assert issubclass(unit.type, ShipType)
|
||||||
|
if ship_group is None:
|
||||||
|
ship_group = self.m.ship_group(
|
||||||
|
self.country,
|
||||||
|
group_name,
|
||||||
|
unit.type,
|
||||||
|
position=unit.position,
|
||||||
|
heading=unit.position.heading.degrees,
|
||||||
|
)
|
||||||
|
if frequency:
|
||||||
|
ship_group.set_frequency(frequency.hertz)
|
||||||
|
ship_group.units[0].name = unit.unit_name
|
||||||
|
self.set_alarm_state(ship_group)
|
||||||
|
else:
|
||||||
|
ship_unit = Ship(
|
||||||
|
self.m.next_unit_id(),
|
||||||
|
unit.unit_name,
|
||||||
|
unit.type,
|
||||||
|
)
|
||||||
|
if frequency:
|
||||||
|
ship_unit.set_frequency(frequency.hertz)
|
||||||
|
ship_unit.position = unit.position
|
||||||
|
ship_unit.heading = unit.position.heading.degrees
|
||||||
|
ship_group.add_unit(ship_unit)
|
||||||
|
self._register_theater_unit(unit, ship_group.units[-1])
|
||||||
|
if ship_group is None:
|
||||||
|
raise RuntimeError(f"Error creating ShipGroup for {group_name}")
|
||||||
|
return ship_group
|
||||||
|
|
||||||
unit_name = unit.unit_name if unique_name else unit.name
|
def create_static_group(self, unit: TheaterUnit) -> None:
|
||||||
if moving_group is None or group.static_group:
|
if isinstance(unit, SceneryUnit):
|
||||||
# First unit of the group will create the dcs group
|
# Special handling for scenery objects:
|
||||||
if issubclass(unit_type, VehicleType):
|
# Only create a trigger zone and no "real" dcs unit
|
||||||
moving_group = self.m.vehicle_group(
|
self.add_trigger_zone_for_scenery(unit)
|
||||||
self.country,
|
return
|
||||||
group_name,
|
|
||||||
unit_type,
|
|
||||||
position=unit.position,
|
|
||||||
heading=unit.position.heading.degrees,
|
|
||||||
)
|
|
||||||
moving_group.units[0].player_can_drive = True
|
|
||||||
self.enable_eplrs(moving_group, unit_type)
|
|
||||||
elif issubclass(unit_type, ShipType):
|
|
||||||
moving_group = self.m.ship_group(
|
|
||||||
self.country,
|
|
||||||
group_name,
|
|
||||||
unit_type,
|
|
||||||
position=unit.position,
|
|
||||||
heading=unit.position.heading.degrees,
|
|
||||||
)
|
|
||||||
elif issubclass(unit_type, StaticType):
|
|
||||||
static_group = self.m.static_group(
|
|
||||||
country=self.country,
|
|
||||||
name=unit_name,
|
|
||||||
_type=unit_type,
|
|
||||||
position=unit.position,
|
|
||||||
heading=unit.position.heading.degrees,
|
|
||||||
dead=not unit.alive,
|
|
||||||
)
|
|
||||||
self._register_ground_unit(unit, static_group.units[0])
|
|
||||||
continue
|
|
||||||
|
|
||||||
if moving_group:
|
static_group = self.m.static_group(
|
||||||
moving_group.units[0].name = unit_name
|
country=self.country,
|
||||||
self.set_alarm_state(moving_group)
|
name=unit.unit_name,
|
||||||
self._register_ground_unit(unit, moving_group.units[0])
|
_type=unit.type,
|
||||||
else:
|
position=unit.position,
|
||||||
raise RuntimeError("DCS Group creation failed")
|
heading=unit.position.heading.degrees,
|
||||||
else:
|
dead=not unit.alive,
|
||||||
# Additional Units in the group
|
)
|
||||||
dcs_unit: Optional[Unit] = None
|
self._register_theater_unit(unit, static_group.units[0])
|
||||||
if issubclass(unit_type, VehicleType):
|
|
||||||
dcs_unit = Vehicle(
|
|
||||||
self.m.next_unit_id(),
|
|
||||||
unit_name,
|
|
||||||
unit.type,
|
|
||||||
)
|
|
||||||
dcs_unit.player_can_drive = True
|
|
||||||
elif issubclass(unit_type, ShipType):
|
|
||||||
dcs_unit = Ship(
|
|
||||||
self.m.next_unit_id(),
|
|
||||||
unit_name,
|
|
||||||
unit_type,
|
|
||||||
)
|
|
||||||
if dcs_unit:
|
|
||||||
dcs_unit.position = unit.position
|
|
||||||
dcs_unit.heading = unit.position.heading.degrees
|
|
||||||
moving_group.add_unit(dcs_unit)
|
|
||||||
self._register_ground_unit(unit, dcs_unit)
|
|
||||||
else:
|
|
||||||
raise RuntimeError("DCS Unit creation failed")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
|
||||||
@ -201,14 +218,14 @@ class GroundObjectGenerator:
|
|||||||
else:
|
else:
|
||||||
group.points[0].tasks.append(OptAlarmState(1))
|
group.points[0].tasks.append(OptAlarmState(1))
|
||||||
|
|
||||||
def _register_ground_unit(
|
def _register_theater_unit(
|
||||||
self,
|
self,
|
||||||
ground_unit: GroundUnit,
|
theater_unit: TheaterUnit,
|
||||||
dcs_unit: Unit,
|
dcs_unit: Unit,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.unit_map.add_ground_object_mapping(ground_unit, dcs_unit)
|
self.unit_map.add_theater_unit_mapping(theater_unit, dcs_unit)
|
||||||
|
|
||||||
def add_trigger_zone_for_scenery(self, scenery: SceneryGroundUnit) -> None:
|
def add_trigger_zone_for_scenery(self, scenery: SceneryUnit) -> None:
|
||||||
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
|
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
|
||||||
color = (
|
color = (
|
||||||
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
|
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
|
||||||
@ -265,7 +282,7 @@ class MissileSiteGenerator(GroundObjectGenerator):
|
|||||||
# culled despite being a threat.
|
# culled despite being a threat.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def generate(self, unique_name: bool = True) -> None:
|
def generate(self) -> None:
|
||||||
super(MissileSiteGenerator, self).generate()
|
super(MissileSiteGenerator, self).generate()
|
||||||
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
|
||||||
# TODO : Should be pre-planned ?
|
# TODO : Should be pre-planned ?
|
||||||
@ -347,7 +364,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
|||||||
self.icls_alloc = icls_alloc
|
self.icls_alloc = icls_alloc
|
||||||
self.runways = runways
|
self.runways = runways
|
||||||
|
|
||||||
def generate(self, unique_name: bool = True) -> None:
|
def generate(self) -> None:
|
||||||
|
|
||||||
# This can also be refactored as the general generation was updated
|
# This can also be refactored as the general generation was updated
|
||||||
atc = self.radio_registry.alloc_uhf()
|
atc = self.radio_registry.alloc_uhf()
|
||||||
@ -357,40 +374,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
|||||||
logging.warning(f"Found empty carrier group in {self.control_point}")
|
logging.warning(f"Found empty carrier group in {self.control_point}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Correct unit type for the carrier.
|
ship_group = self.create_ship_group(group.group_name, group.units, atc)
|
||||||
# This is only used for the super carrier setting
|
|
||||||
unit_type = (
|
|
||||||
self.get_carrier_type(group)
|
|
||||||
if g_id == 0
|
|
||||||
else ship_map[group.units[0].type]
|
|
||||||
)
|
|
||||||
|
|
||||||
ship_group = self.m.ship_group(
|
|
||||||
self.country,
|
|
||||||
group.group_name if unique_name else group.name,
|
|
||||||
unit_type,
|
|
||||||
position=group.units[0].position,
|
|
||||||
heading=group.units[0].position.heading.degrees,
|
|
||||||
)
|
|
||||||
|
|
||||||
ship_group.set_frequency(atc.hertz)
|
|
||||||
ship_group.units[0].name = (
|
|
||||||
group.units[0].unit_name if unique_name else group.units[0].name
|
|
||||||
)
|
|
||||||
self._register_ground_unit(group.units[0], ship_group.units[0])
|
|
||||||
|
|
||||||
for unit in group.units[1:]:
|
|
||||||
ship = Ship(
|
|
||||||
self.m.next_unit_id(),
|
|
||||||
unit.unit_name if unique_name else unit.name,
|
|
||||||
unit_type_from_name(unit.type),
|
|
||||||
)
|
|
||||||
ship.position.x = unit.position.x
|
|
||||||
ship.position.y = unit.position.y
|
|
||||||
ship.heading = unit.position.heading.degrees
|
|
||||||
ship.set_frequency(atc.hertz)
|
|
||||||
ship_group.add_unit(ship)
|
|
||||||
self._register_ground_unit(unit, ship)
|
|
||||||
|
|
||||||
# Always steam into the wind, even if the carrier is being moved.
|
# Always steam into the wind, even if the carrier is being moved.
|
||||||
# There are multiple unsimulated hours between turns, so we can
|
# There are multiple unsimulated hours between turns, so we can
|
||||||
@ -400,19 +384,24 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
|||||||
|
|
||||||
# Set Carrier Specific Options
|
# Set Carrier Specific Options
|
||||||
if g_id == 0:
|
if g_id == 0:
|
||||||
|
# Correct unit type for the carrier.
|
||||||
|
# This is only used for the super carrier setting
|
||||||
|
ship_group.units[0].type = self.get_carrier_type(group).id
|
||||||
tacan = self.tacan_registry.alloc_for_band(
|
tacan = self.tacan_registry.alloc_for_band(
|
||||||
TacanBand.X, TacanUsage.TransmitReceive
|
TacanBand.X, TacanUsage.TransmitReceive
|
||||||
)
|
)
|
||||||
tacan_callsign = self.tacan_callsign()
|
tacan_callsign = self.tacan_callsign()
|
||||||
icls = next(self.icls_alloc)
|
icls = next(self.icls_alloc)
|
||||||
|
|
||||||
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
|
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
|
||||||
self.add_runway_data(
|
self.add_runway_data(
|
||||||
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
|
brc or Heading.from_degrees(0), atc, tacan, tacan_callsign, icls
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
|
def get_carrier_type(self, group: TheaterGroup) -> Type[ShipType]:
|
||||||
return ship_map[group.units[0].type]
|
carrier_type = group.units[0].type
|
||||||
|
if issubclass(carrier_type, ShipType):
|
||||||
|
return carrier_type
|
||||||
|
raise RuntimeError(f"First unit of TGO {group.name} is no Ship")
|
||||||
|
|
||||||
def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
|
def steam_into_wind(self, group: ShipGroup) -> Optional[Heading]:
|
||||||
wind = self.game.conditions.weather.wind.at_0m
|
wind = self.game.conditions.weather.wind.at_0m
|
||||||
@ -479,7 +468,7 @@ class GenericCarrierGenerator(GroundObjectGenerator):
|
|||||||
class CarrierGenerator(GenericCarrierGenerator):
|
class CarrierGenerator(GenericCarrierGenerator):
|
||||||
"""Generator for CV(N) groups."""
|
"""Generator for CV(N) groups."""
|
||||||
|
|
||||||
def get_carrier_type(self, group: GroundGroup) -> Type[ShipType]:
|
def get_carrier_type(self, group: TheaterGroup) -> 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 = self.upgrade_to_supercarrier(unit_type, self.control_point.name)
|
unit_type = self.upgrade_to_supercarrier(unit_type, self.control_point.name)
|
||||||
|
|||||||
@ -176,7 +176,7 @@ class ProcurementAi:
|
|||||||
worst_fulfillment = fulfillment
|
worst_fulfillment = fulfillment
|
||||||
worst_balanced = unit_class
|
worst_balanced = unit_class
|
||||||
if worst_balanced is None:
|
if worst_balanced is None:
|
||||||
return UnitClass.Tank
|
return UnitClass.TANK
|
||||||
return worst_balanced
|
return worst_balanced
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@ -132,7 +132,7 @@ class MissionResultsProcessor:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def commit_ground_losses(debriefing: Debriefing) -> None:
|
def commit_ground_losses(debriefing: Debriefing) -> None:
|
||||||
for ground_object_loss in debriefing.ground_object_losses:
|
for ground_object_loss in debriefing.ground_object_losses:
|
||||||
ground_object_loss.ground_unit.kill()
|
ground_object_loss.theater_unit.kill()
|
||||||
for scenery_object_loss in debriefing.scenery_object_losses:
|
for scenery_object_loss in debriefing.scenery_object_losses:
|
||||||
scenery_object_loss.ground_unit.kill()
|
scenery_object_loss.ground_unit.kill()
|
||||||
|
|
||||||
|
|||||||
@ -20,13 +20,11 @@ from typing import (
|
|||||||
Set,
|
Set,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Tuple,
|
Tuple,
|
||||||
Union,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.ships import Forrestal, KUZNECOW, LHA_Tarawa, Stennis, Type_071
|
from dcs.ships import Forrestal, KUZNECOW, LHA_Tarawa, Stennis, Type_071
|
||||||
from dcs.terrain.terrain import Airport, ParkingSlot
|
from dcs.terrain.terrain import Airport, ParkingSlot
|
||||||
from dcs.unit import Unit
|
|
||||||
from dcs.unitgroup import ShipGroup, StaticGroup
|
from dcs.unitgroup import ShipGroup, StaticGroup
|
||||||
|
|
||||||
from game.dcs.helpers import unit_type_from_name
|
from game.dcs.helpers import unit_type_from_name
|
||||||
@ -39,14 +37,10 @@ from gen.runways import RunwayAssigner, RunwayData
|
|||||||
from .base import Base
|
from .base import Base
|
||||||
from .missiontarget import MissionTarget
|
from .missiontarget import MissionTarget
|
||||||
from .theatergroundobject import (
|
from .theatergroundobject import (
|
||||||
BuildingGroundObject,
|
|
||||||
GenericCarrierGroundObject,
|
GenericCarrierGroundObject,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
BuildingGroundObject,
|
|
||||||
CarrierGroundObject,
|
|
||||||
LhaGroundObject,
|
|
||||||
GroundUnit,
|
|
||||||
)
|
)
|
||||||
|
from .theatergroup import TheaterUnit
|
||||||
from ..ato.starttype import StartType
|
from ..ato.starttype import StartType
|
||||||
from ..data.units import UnitClass
|
from ..data.units import UnitClass
|
||||||
from ..dcs.aircrafttype import AircraftType
|
from ..dcs.aircrafttype import AircraftType
|
||||||
@ -525,8 +519,8 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
for group in g.groups:
|
for group in g.groups:
|
||||||
for u in group.units:
|
for u in group.units:
|
||||||
if u.unit_type and u.unit_type.unit_class in [
|
if u.unit_type and u.unit_type.unit_class in [
|
||||||
UnitClass.AircraftCarrier,
|
UnitClass.AIRCRAFT_CARRIER,
|
||||||
UnitClass.HelicopterCarrier,
|
UnitClass.HELICOPTER_CARRIER,
|
||||||
]:
|
]:
|
||||||
return group.group_name
|
return group.group_name
|
||||||
return None
|
return None
|
||||||
@ -816,28 +810,26 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return self.front_line_capacity_with(self.active_ammo_depots_count)
|
return self.front_line_capacity_with(self.active_ammo_depots_count)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_ammo_depots(self) -> Iterator[BuildingGroundObject]:
|
def all_ammo_depots(self) -> Iterator[TheaterGroundObject]:
|
||||||
for tgo in self.connected_objectives:
|
for tgo in self.connected_objectives:
|
||||||
if not tgo.is_ammo_depot:
|
if tgo.is_ammo_depot:
|
||||||
continue
|
|
||||||
assert isinstance(tgo, BuildingGroundObject)
|
|
||||||
yield tgo
|
|
||||||
|
|
||||||
@property
|
|
||||||
def active_ammo_depots(self) -> Iterator[BuildingGroundObject]:
|
|
||||||
for tgo in self.all_ammo_depots:
|
|
||||||
if not tgo.is_dead:
|
|
||||||
yield tgo
|
yield tgo
|
||||||
|
|
||||||
|
def ammo_depot_count(self, alive_only: bool = False) -> int:
|
||||||
|
return sum(
|
||||||
|
ammo_depot.alive_unit_count if alive_only else ammo_depot.unit_count
|
||||||
|
for ammo_depot in self.all_ammo_depots
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_ammo_depots_count(self) -> int:
|
def active_ammo_depots_count(self) -> int:
|
||||||
"""Return the number of available ammo depots"""
|
"""Return the number of available ammo depots"""
|
||||||
return len(list(self.active_ammo_depots))
|
return self.ammo_depot_count(True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_ammo_depots_count(self) -> int:
|
def total_ammo_depots_count(self) -> int:
|
||||||
"""Return the number of ammo depots, including dead ones"""
|
"""Return the number of ammo depots, including dead ones"""
|
||||||
return len(list(self.all_ammo_depots))
|
return self.ammo_depot_count()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_fuel_depots_count(self) -> int:
|
def active_fuel_depots_count(self) -> int:
|
||||||
@ -856,7 +848,7 @@ class ControlPoint(MissionTarget, ABC):
|
|||||||
return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
|
return len([obj for obj in self.connected_objectives if obj.category == "fuel"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> list[GroundUnit]:
|
def strike_targets(self) -> list[TheaterUnit]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1008,7 +1000,7 @@ class NavalControlPoint(ControlPoint, ABC):
|
|||||||
# while its escorts are still alive.
|
# while its escorts are still alive.
|
||||||
for group in self.find_main_tgo().groups:
|
for group in self.find_main_tgo().groups:
|
||||||
for u in group.units:
|
for u in group.units:
|
||||||
if unit_type_from_name(u.type) in [
|
if u.type in [
|
||||||
Forrestal,
|
Forrestal,
|
||||||
Stennis,
|
Stennis,
|
||||||
LHA_Tarawa,
|
LHA_Tarawa,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from dcs.unit import Unit
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.ato.flighttype import FlightType
|
from game.ato.flighttype import FlightType
|
||||||
from game.theater.theatergroundobject import GroundUnit
|
from game.theater import TheaterUnit
|
||||||
|
|
||||||
|
|
||||||
class MissionTarget:
|
class MissionTarget:
|
||||||
@ -47,5 +47,5 @@ class MissionTarget:
|
|||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> list[GroundUnit]:
|
def strike_targets(self) -> list[TheaterUnit]:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@ -6,19 +6,18 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import dcs.statics
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.factions.faction import Faction
|
from game.factions.faction import Faction
|
||||||
from game.scenery_group import SceneryGroup
|
from game.scenery_group import SceneryGroup
|
||||||
from game.theater import PointWithHeading
|
from game.theater import PointWithHeading
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
AirDefenseRange,
|
|
||||||
BuildingGroundObject,
|
BuildingGroundObject,
|
||||||
SceneryGroundUnit,
|
|
||||||
GroundGroup,
|
|
||||||
)
|
)
|
||||||
|
from .theatergroup import SceneryUnit, TheaterGroup
|
||||||
from game.utils import Heading
|
from game.utils import Heading
|
||||||
from game.version import VERSION
|
from game.version import VERSION
|
||||||
from gen.templates import GroundObjectTemplates, GroundObjectTemplate
|
|
||||||
from gen.naming import namegen
|
from gen.naming import namegen
|
||||||
from . import (
|
from . import (
|
||||||
ConflictTheater,
|
ConflictTheater,
|
||||||
@ -28,9 +27,9 @@ from . import (
|
|||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
)
|
)
|
||||||
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||||
from ..data.units import UnitClass
|
from ..data.groups import GroupRole, GroupTask
|
||||||
from ..data.groups import GroupRole, GroupTask, ROLE_TASKINGS
|
from ..armedforces.forcegroup import ForceGroup
|
||||||
from ..dcs.unitgroup import UnitGroup
|
from ..armedforces.armedforces import ArmedForces
|
||||||
from ..profiling import logged_duration
|
from ..profiling import logged_duration
|
||||||
from ..settings import Settings
|
from ..settings import Settings
|
||||||
|
|
||||||
@ -77,9 +76,8 @@ class GameGenerator:
|
|||||||
self.air_wing_config = air_wing_config
|
self.air_wing_config = air_wing_config
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.generator_settings = generator_settings
|
self.generator_settings = generator_settings
|
||||||
|
self.player.apply_mod_settings(mod_settings)
|
||||||
with logged_duration(f"Initializing faction and templates"):
|
self.enemy.apply_mod_settings(mod_settings)
|
||||||
self.initialize_factions(mod_settings)
|
|
||||||
|
|
||||||
def generate(self) -> Game:
|
def generate(self) -> Game:
|
||||||
with logged_duration("TGO population"):
|
with logged_duration("TGO population"):
|
||||||
@ -126,12 +124,6 @@ class GameGenerator:
|
|||||||
for cp in to_remove:
|
for cp in to_remove:
|
||||||
self.theater.controlpoints.remove(cp)
|
self.theater.controlpoints.remove(cp)
|
||||||
|
|
||||||
def initialize_factions(self, mod_settings: ModSettings) -> None:
|
|
||||||
with logged_duration("Loading Templates from mapping"):
|
|
||||||
templates = GroundObjectTemplates.from_folder("resources/templates/")
|
|
||||||
self.player.initialize(templates, mod_settings)
|
|
||||||
self.enemy.initialize(templates, mod_settings)
|
|
||||||
|
|
||||||
|
|
||||||
class ControlPointGroundObjectGenerator:
|
class ControlPointGroundObjectGenerator:
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -152,35 +144,30 @@ class ControlPointGroundObjectGenerator:
|
|||||||
def faction(self) -> Faction:
|
def faction(self) -> Faction:
|
||||||
return self.game.coalition_for(self.control_point.captured).faction
|
return self.game.coalition_for(self.control_point.captured).faction
|
||||||
|
|
||||||
|
@property
|
||||||
|
def armed_forces(self) -> ArmedForces:
|
||||||
|
return self.game.coalition_for(self.control_point.captured).armed_forces
|
||||||
|
|
||||||
def generate(self) -> bool:
|
def generate(self) -> bool:
|
||||||
self.control_point.connected_objectives = []
|
self.control_point.connected_objectives = []
|
||||||
self.generate_navy()
|
self.generate_navy()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def generate_random_ground_object(
|
def generate_random_ground_object(
|
||||||
self, unit_groups: list[UnitGroup], position: PointWithHeading
|
self, unit_groups: list[ForceGroup], position: PointWithHeading
|
||||||
) -> None:
|
) -> None:
|
||||||
self.generate_ground_object_from_group(random.choice(unit_groups), position)
|
self.generate_ground_object_from_group(random.choice(unit_groups), position)
|
||||||
|
|
||||||
def generate_ground_object_from_group(
|
def generate_ground_object_from_group(
|
||||||
self, unit_group: UnitGroup, position: PointWithHeading
|
self, unit_group: ForceGroup, position: PointWithHeading
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
ground_object = unit_group.generate(
|
||||||
with logged_duration(
|
namegen.random_objective_name(),
|
||||||
f"Ground Object generation for unit_group "
|
position,
|
||||||
f"{unit_group.name} ({unit_group.role.value})"
|
self.control_point,
|
||||||
):
|
self.game,
|
||||||
ground_object = unit_group.generate(
|
)
|
||||||
namegen.random_objective_name(),
|
self.control_point.connected_objectives.append(ground_object)
|
||||||
position,
|
|
||||||
self.control_point,
|
|
||||||
self.game,
|
|
||||||
)
|
|
||||||
self.control_point.connected_objectives.append(ground_object)
|
|
||||||
except NotImplementedError:
|
|
||||||
logging.error("Template Generator not implemented yet")
|
|
||||||
except IndexError:
|
|
||||||
logging.error(f"No templates to generate object from {unit_group.name}")
|
|
||||||
|
|
||||||
def generate_navy(self) -> None:
|
def generate_navy(self) -> None:
|
||||||
skip_player_navy = self.generator_settings.no_player_navy
|
skip_player_navy = self.generator_settings.no_player_navy
|
||||||
@ -190,11 +177,9 @@ class ControlPointGroundObjectGenerator:
|
|||||||
if not self.control_point.captured and skip_enemy_navy:
|
if not self.control_point.captured and skip_enemy_navy:
|
||||||
return
|
return
|
||||||
for position in self.control_point.preset_locations.ships:
|
for position in self.control_point.preset_locations.ships:
|
||||||
unit_group = self.faction.random_group_for_role_and_task(
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.NAVY)
|
||||||
GroupRole.Naval, GroupTask.Navy
|
|
||||||
)
|
|
||||||
if not unit_group:
|
if not unit_group:
|
||||||
logging.error(f"{self.faction_name} has no UnitGroup for Navy")
|
logging.warning(f"{self.faction_name} has no ForceGroup for Navy")
|
||||||
return
|
return
|
||||||
self.generate_ground_object_from_group(unit_group, position)
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
@ -217,11 +202,9 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
unit_group = self.faction.random_group_for_role_and_task(
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.AIRCRAFT_CARRIER)
|
||||||
GroupRole.Naval, GroupTask.AircraftCarrier
|
|
||||||
)
|
|
||||||
if not unit_group:
|
if not unit_group:
|
||||||
logging.error(f"{self.faction_name} has no UnitGroup for AircraftCarrier")
|
logging.error(f"{self.faction_name} has no access to AircraftCarrier")
|
||||||
return False
|
return False
|
||||||
self.generate_ground_object_from_group(
|
self.generate_ground_object_from_group(
|
||||||
unit_group,
|
unit_group,
|
||||||
@ -246,11 +229,11 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
unit_group = self.faction.random_group_for_role_and_task(
|
unit_group = self.armed_forces.random_group_for_task(
|
||||||
GroupRole.Naval, GroupTask.HelicopterCarrier
|
GroupTask.HELICOPTER_CARRIER
|
||||||
)
|
)
|
||||||
if not unit_group:
|
if not unit_group:
|
||||||
logging.error(f"{self.faction_name} has no UnitGroup for HelicopterCarrier")
|
logging.error(f"{self.faction_name} has no access to HelicopterCarrier")
|
||||||
return False
|
return False
|
||||||
self.generate_ground_object_from_group(
|
self.generate_ground_object_from_group(
|
||||||
unit_group,
|
unit_group,
|
||||||
@ -293,11 +276,9 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
|
|
||||||
def generate_armor_groups(self) -> None:
|
def generate_armor_groups(self) -> None:
|
||||||
for position in self.control_point.preset_locations.armor_groups:
|
for position in self.control_point.preset_locations.armor_groups:
|
||||||
unit_group = self.faction.random_group_for_role_and_tasks(
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.BASE_DEFENSE)
|
||||||
GroupRole.GroundForce, ROLE_TASKINGS[GroupRole.GroundForce]
|
|
||||||
)
|
|
||||||
if not unit_group:
|
if not unit_group:
|
||||||
logging.error(f"{self.faction_name} has no templates for Armor Groups")
|
logging.error(f"{self.faction_name} has no ForceGroup for Armor")
|
||||||
return
|
return
|
||||||
self.generate_ground_object_from_group(unit_group, position)
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
@ -318,11 +299,11 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
|
|
||||||
def generate_ewrs(self) -> None:
|
def generate_ewrs(self) -> None:
|
||||||
for position in self.control_point.preset_locations.ewrs:
|
for position in self.control_point.preset_locations.ewrs:
|
||||||
unit_group = self.faction.random_group_for_role_and_task(
|
unit_group = self.armed_forces.random_group_for_task(
|
||||||
GroupRole.AntiAir, GroupTask.EWR
|
GroupTask.EARLY_WARNING_RADAR
|
||||||
)
|
)
|
||||||
if not unit_group:
|
if not unit_group:
|
||||||
logging.error(f"{self.faction_name} has no UnitGroup for EWR")
|
logging.error(f"{self.faction_name} has no ForceGroup for EWR")
|
||||||
return
|
return
|
||||||
self.generate_ground_object_from_group(unit_group, position)
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
@ -331,31 +312,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
group_task: GroupTask,
|
group_task: GroupTask,
|
||||||
position: PointWithHeading,
|
position: PointWithHeading,
|
||||||
) -> None:
|
) -> None:
|
||||||
unit_group = self.faction.random_group_for_role_and_task(
|
# GroupTask is the type of the building to be generated
|
||||||
GroupRole.Building, group_task
|
unit_group = self.armed_forces.random_group_for_task(group_task)
|
||||||
)
|
|
||||||
if not unit_group:
|
if not unit_group:
|
||||||
logging.error(
|
raise RuntimeError(
|
||||||
f"{self.faction_name} has no access to Building ({group_task.value})"
|
f"{self.faction_name} has no access to Building {group_task.description}"
|
||||||
)
|
)
|
||||||
return
|
|
||||||
self.generate_ground_object_from_group(unit_group, position)
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
def generate_ammunition_depots(self) -> None:
|
def generate_ammunition_depots(self) -> None:
|
||||||
for position in self.control_point.preset_locations.ammunition_depots:
|
for position in self.control_point.preset_locations.ammunition_depots:
|
||||||
self.generate_building_at(GroupTask.Ammo, position)
|
self.generate_building_at(GroupTask.AMMO, position)
|
||||||
|
|
||||||
def generate_factories(self) -> None:
|
def generate_factories(self) -> None:
|
||||||
for position in self.control_point.preset_locations.factories:
|
for position in self.control_point.preset_locations.factories:
|
||||||
self.generate_building_at(GroupTask.Factory, position)
|
self.generate_building_at(GroupTask.FACTORY, position)
|
||||||
|
|
||||||
def generate_aa_at(
|
def generate_aa_at(
|
||||||
self, position: PointWithHeading, tasks: list[GroupTask]
|
self, position: PointWithHeading, tasks: list[GroupTask]
|
||||||
) -> None:
|
) -> None:
|
||||||
for task in tasks:
|
for task in tasks:
|
||||||
unit_group = self.faction.random_group_for_role_and_task(
|
unit_group = self.armed_forces.random_group_for_task(task)
|
||||||
GroupRole.AntiAir, task
|
|
||||||
)
|
|
||||||
if unit_group:
|
if unit_group:
|
||||||
# Only take next (smaller) aa_range when no template available for the
|
# Only take next (smaller) aa_range when no template available for the
|
||||||
# most requested range. Otherwise break the loop and continue
|
# most requested range. Otherwise break the loop and continue
|
||||||
@ -363,7 +340,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
return
|
return
|
||||||
|
|
||||||
logging.error(
|
logging.error(
|
||||||
f"{self.faction_name} has no access to SAM Templates ({', '.join([task.value for task in tasks])})"
|
f"{self.faction_name} has no access to SAM {', '.join([task.description for task in tasks])}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def generate_scenery_sites(self) -> None:
|
def generate_scenery_sites(self) -> None:
|
||||||
@ -380,21 +357,20 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
Heading.from_degrees(0),
|
Heading.from_degrees(0),
|
||||||
self.control_point,
|
self.control_point,
|
||||||
)
|
)
|
||||||
ground_group = GroundGroup(
|
ground_group = TheaterGroup(
|
||||||
self.game.next_group_id(),
|
self.game.next_group_id(),
|
||||||
scenery.zone_def.name,
|
scenery.zone_def.name,
|
||||||
PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)),
|
PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)),
|
||||||
[],
|
[],
|
||||||
g,
|
g,
|
||||||
)
|
)
|
||||||
ground_group.static_group = True
|
|
||||||
g.groups.append(ground_group)
|
g.groups.append(ground_group)
|
||||||
# Each nested trigger zone is a target/building/unit for an objective.
|
# Each nested trigger zone is a target/building/unit for an objective.
|
||||||
for zone in scenery.zones:
|
for zone in scenery.zones:
|
||||||
scenery_unit = SceneryGroundUnit(
|
scenery_unit = SceneryUnit(
|
||||||
zone.id,
|
zone.id,
|
||||||
zone.name,
|
zone.name,
|
||||||
"",
|
dcs.statics.Fortification.White_Flag,
|
||||||
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
|
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
|
||||||
g,
|
g,
|
||||||
)
|
)
|
||||||
@ -405,31 +381,27 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
|||||||
|
|
||||||
def generate_missile_sites(self) -> None:
|
def generate_missile_sites(self) -> None:
|
||||||
for position in self.control_point.preset_locations.missile_sites:
|
for position in self.control_point.preset_locations.missile_sites:
|
||||||
unit_group = self.faction.random_group_for_role_and_task(
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.MISSILE)
|
||||||
GroupRole.Defenses, GroupTask.Missile
|
|
||||||
)
|
|
||||||
if not unit_group:
|
if not unit_group:
|
||||||
logging.error(f"{self.faction_name} has no UnitGroup for Missile")
|
logging.warning(f"{self.faction_name} has no ForceGroup for Missile")
|
||||||
return
|
return
|
||||||
self.generate_ground_object_from_group(unit_group, position)
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
def generate_coastal_sites(self) -> None:
|
def generate_coastal_sites(self) -> None:
|
||||||
for position in self.control_point.preset_locations.coastal_defenses:
|
for position in self.control_point.preset_locations.coastal_defenses:
|
||||||
unit_group = self.faction.random_group_for_role_and_task(
|
unit_group = self.armed_forces.random_group_for_task(GroupTask.COASTAL)
|
||||||
GroupRole.Defenses, GroupTask.Coastal
|
|
||||||
)
|
|
||||||
if not unit_group:
|
if not unit_group:
|
||||||
logging.error(f"{self.faction_name} has no UnitGroup for Coastal")
|
logging.warning(f"{self.faction_name} has no ForceGroup for Coastal")
|
||||||
return
|
return
|
||||||
self.generate_ground_object_from_group(unit_group, position)
|
self.generate_ground_object_from_group(unit_group, position)
|
||||||
|
|
||||||
def generate_strike_targets(self) -> None:
|
def generate_strike_targets(self) -> None:
|
||||||
for position in self.control_point.preset_locations.strike_locations:
|
for position in self.control_point.preset_locations.strike_locations:
|
||||||
self.generate_building_at(GroupTask.StrikeTarget, position)
|
self.generate_building_at(GroupTask.STRIKE_TARGET, position)
|
||||||
|
|
||||||
def generate_offshore_strike_targets(self) -> None:
|
def generate_offshore_strike_targets(self) -> None:
|
||||||
for position in self.control_point.preset_locations.offshore_strike_locations:
|
for position in self.control_point.preset_locations.offshore_strike_locations:
|
||||||
self.generate_building_at(GroupTask.Oil, position)
|
self.generate_building_at(GroupTask.OIL, position)
|
||||||
|
|
||||||
|
|
||||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||||
|
|||||||
@ -3,28 +3,20 @@ from __future__ import annotations
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from collections.abc import Sequence
|
from typing import Iterator, List, TYPE_CHECKING
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
|
|
||||||
|
|
||||||
from dcs.unittype import VehicleType, ShipType
|
from dcs.unittype import VehicleType
|
||||||
from dcs.vehicles import vehicle_map
|
from dcs.vehicles import vehicle_map
|
||||||
from dcs.ships import ship_map
|
|
||||||
|
|
||||||
from dcs.mapping import Point
|
from dcs.mapping import Point
|
||||||
from dcs.triggers import TriggerZone
|
|
||||||
|
|
||||||
from game.dcs.helpers import unit_type_from_name
|
from game.dcs.helpers import unit_type_from_name
|
||||||
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
||||||
from ..dcs.groundunittype import GroundUnitType
|
|
||||||
from ..dcs.shipunittype import ShipUnitType
|
|
||||||
from ..dcs.unittype import UnitType
|
|
||||||
from ..point_with_heading import PointWithHeading
|
|
||||||
from ..utils import Distance, Heading, meters
|
from ..utils import Distance, Heading, meters
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from gen.templates import UnitTemplate, GroupTemplate
|
from .theatergroup import TheaterUnit, TheaterGroup
|
||||||
from .controlpoint import ControlPoint
|
from .controlpoint import ControlPoint
|
||||||
from ..ato.flighttype import FlightType
|
from ..ato.flighttype import FlightType
|
||||||
|
|
||||||
@ -53,126 +45,6 @@ NAME_BY_CATEGORY = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class SkynetRole(Enum):
|
|
||||||
#: A radar SAM that should be controlled by Skynet.
|
|
||||||
Sam = "Sam"
|
|
||||||
|
|
||||||
#: A radar SAM that should be controlled and used as an EWR by Skynet.
|
|
||||||
SamAsEwr = "SamAsEwr"
|
|
||||||
|
|
||||||
#: An air defense unit that should be used as point defense by Skynet.
|
|
||||||
PointDefense = "PD"
|
|
||||||
|
|
||||||
#: All other types of groups that might be present in a SAM TGO. This includes
|
|
||||||
#: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet
|
|
||||||
#: should use this role.
|
|
||||||
NoSkynetBehavior = "NoSkynetBehavior"
|
|
||||||
|
|
||||||
|
|
||||||
class AirDefenseRange(Enum):
|
|
||||||
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
|
|
||||||
Short = ("short", SkynetRole.NoSkynetBehavior)
|
|
||||||
Medium = ("medium", SkynetRole.Sam)
|
|
||||||
Long = ("long", SkynetRole.SamAsEwr)
|
|
||||||
|
|
||||||
def __init__(self, description: str, default_role: SkynetRole) -> None:
|
|
||||||
self.range_name = description
|
|
||||||
self.default_role = default_role
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GroundUnit:
|
|
||||||
# Units can be everything.. Static, Vehicle, Ship.
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
type: str # dcs.UnitType as string
|
|
||||||
position: PointWithHeading
|
|
||||||
ground_object: TheaterGroundObject
|
|
||||||
alive: bool = True
|
|
||||||
_unit_type: Optional[UnitType[Any]] = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_template(
|
|
||||||
id: int, unit_type: str, t: UnitTemplate, go: TheaterGroundObject
|
|
||||||
) -> GroundUnit:
|
|
||||||
return GroundUnit(
|
|
||||||
id,
|
|
||||||
t.name,
|
|
||||||
unit_type,
|
|
||||||
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
|
||||||
go,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unit_type(self) -> Optional[UnitType[Any]]:
|
|
||||||
if not self._unit_type:
|
|
||||||
try:
|
|
||||||
unit_type: Optional[UnitType[Any]] = None
|
|
||||||
dcs_type = db.unit_type_from_name(self.type)
|
|
||||||
if dcs_type and issubclass(dcs_type, VehicleType):
|
|
||||||
unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
|
|
||||||
elif dcs_type and issubclass(dcs_type, ShipType):
|
|
||||||
unit_type = next(ShipUnitType.for_dcs_type(dcs_type))
|
|
||||||
self._unit_type = unit_type
|
|
||||||
except StopIteration:
|
|
||||||
logging.error(f"No UnitType for {self.type}")
|
|
||||||
pass
|
|
||||||
return self._unit_type
|
|
||||||
|
|
||||||
def kill(self) -> None:
|
|
||||||
self.alive = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unit_name(self) -> str:
|
|
||||||
return f"{str(self.id).zfill(4)} | {self.name}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def display_name(self) -> str:
|
|
||||||
dead_label = " [DEAD]" if not self.alive else ""
|
|
||||||
unit_label = self.unit_type or self.type or self.name
|
|
||||||
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
|
|
||||||
|
|
||||||
|
|
||||||
class SceneryGroundUnit(GroundUnit):
|
|
||||||
"""Special GroundUnit for handling scenery ground objects"""
|
|
||||||
|
|
||||||
zone: TriggerZone
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GroundGroup:
|
|
||||||
id: int
|
|
||||||
name: str
|
|
||||||
position: PointWithHeading
|
|
||||||
units: list[GroundUnit]
|
|
||||||
ground_object: TheaterGroundObject
|
|
||||||
static_group: bool = False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_template(
|
|
||||||
id: int,
|
|
||||||
g: GroupTemplate,
|
|
||||||
go: TheaterGroundObject,
|
|
||||||
) -> GroundGroup:
|
|
||||||
tgo_group = GroundGroup(
|
|
||||||
id,
|
|
||||||
g.name,
|
|
||||||
PointWithHeading.from_point(go.position, go.heading),
|
|
||||||
g.generate_units(go),
|
|
||||||
go,
|
|
||||||
)
|
|
||||||
tgo_group.static_group = g.static
|
|
||||||
return tgo_group
|
|
||||||
|
|
||||||
@property
|
|
||||||
def group_name(self) -> str:
|
|
||||||
return f"{str(self.id).zfill(4)} | {self.name}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def alive_units(self) -> int:
|
|
||||||
return sum([unit.alive for unit in self.units])
|
|
||||||
|
|
||||||
|
|
||||||
class TheaterGroundObject(MissionTarget):
|
class TheaterGroundObject(MissionTarget):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -188,27 +60,28 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
self.heading = heading
|
self.heading = heading
|
||||||
self.control_point = control_point
|
self.control_point = control_point
|
||||||
self.sea_object = sea_object
|
self.sea_object = sea_object
|
||||||
self.groups: List[GroundGroup] = []
|
self.groups: List[TheaterGroup] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_dead(self) -> bool:
|
def is_dead(self) -> bool:
|
||||||
return self.alive_unit_count == 0
|
return self.alive_unit_count == 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def units(self) -> Iterator[GroundUnit]:
|
def units(self) -> Iterator[TheaterUnit]:
|
||||||
"""
|
"""
|
||||||
:return: all the units at this location
|
:return: all the units at this location
|
||||||
"""
|
"""
|
||||||
yield from itertools.chain.from_iterable([g.units for g in self.groups])
|
yield from itertools.chain.from_iterable([g.units for g in self.groups])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def statics(self) -> Iterator[GroundUnit]:
|
def statics(self) -> Iterator[TheaterUnit]:
|
||||||
for group in self.groups:
|
for group in self.groups:
|
||||||
if group.static_group:
|
for unit in group.units:
|
||||||
yield from group.units
|
if unit.is_static:
|
||||||
|
yield unit
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dead_units(self) -> list[GroundUnit]:
|
def dead_units(self) -> list[TheaterUnit]:
|
||||||
"""
|
"""
|
||||||
:return: all the dead units at this location
|
:return: all the dead units at this location
|
||||||
"""
|
"""
|
||||||
@ -253,6 +126,10 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
]
|
]
|
||||||
yield from super().mission_types(for_player)
|
yield from super().mission_types(for_player)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_count(self) -> int:
|
||||||
|
return sum([g.unit_count for g in self.groups])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alive_unit_count(self) -> int:
|
def alive_unit_count(self) -> int:
|
||||||
return sum([g.alive_units for g in self.groups])
|
return sum([g.alive_units for g in self.groups])
|
||||||
@ -269,20 +146,15 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _max_range_of_type(self, group: GroundGroup, range_type: str) -> Distance:
|
def _max_range_of_type(self, group: TheaterGroup, range_type: str) -> Distance:
|
||||||
if not self.might_have_aa:
|
if not self.might_have_aa:
|
||||||
return meters(0)
|
return meters(0)
|
||||||
|
|
||||||
max_range = meters(0)
|
max_range = meters(0)
|
||||||
for u in group.units:
|
for u in group.units:
|
||||||
unit = unit_type_from_name(u.type)
|
|
||||||
if unit is None:
|
|
||||||
logging.error(f"Unknown unit type {u.type}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Some units in pydcs have detection_range/threat_range defined,
|
# Some units in pydcs have detection_range/threat_range defined,
|
||||||
# but explicitly set to None.
|
# but explicitly set to None.
|
||||||
unit_range = getattr(unit, range_type, None)
|
unit_range = getattr(u.type, range_type, None)
|
||||||
if unit_range is not None:
|
if unit_range is not None:
|
||||||
max_range = max(max_range, meters(unit_range))
|
max_range = max(max_range, meters(unit_range))
|
||||||
return max_range
|
return max_range
|
||||||
@ -290,7 +162,7 @@ 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: GroundGroup) -> Distance:
|
def detection_range(self, group: TheaterGroup) -> 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:
|
||||||
@ -298,7 +170,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
max(self.threat_range(g) for g in self.groups) if self.groups else meters(0)
|
max(self.threat_range(g) for g in self.groups) if self.groups else meters(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
|
def threat_range(self, group: TheaterGroup, 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
|
||||||
@ -315,7 +187,7 @@ class TheaterGroundObject(MissionTarget):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def strike_targets(self) -> list[GroundUnit]:
|
def strike_targets(self) -> list[TheaterUnit]:
|
||||||
return [unit for unit in self.units if unit.alive]
|
return [unit for unit in self.units if unit.alive]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -543,13 +415,15 @@ class SamGroundObject(IadsGroundObject):
|
|||||||
def might_have_aa(self) -> bool:
|
def might_have_aa(self) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def threat_range(self, group: GroundGroup, radar_only: bool = False) -> Distance:
|
def threat_range(self, group: TheaterGroup, 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 = vehicle_map[unit.type]
|
if not unit.alive or not issubclass(unit.type, VehicleType):
|
||||||
|
continue
|
||||||
|
unit_type = unit.type
|
||||||
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:
|
||||||
|
|||||||
167
game/theater/theatergroup.py
Normal file
167
game/theater/theatergroup.py
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Any, TYPE_CHECKING, Type
|
||||||
|
|
||||||
|
from dcs.triggers import TriggerZone
|
||||||
|
from dcs.unittype import VehicleType, ShipType, StaticType
|
||||||
|
|
||||||
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
|
from game.dcs.shipunittype import ShipUnitType
|
||||||
|
from game.dcs.unittype import UnitType
|
||||||
|
from dcs.unittype import UnitType as DcsUnitType
|
||||||
|
|
||||||
|
from game.point_with_heading import PointWithHeading
|
||||||
|
from game.utils import Heading
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from game.layout.layout import LayoutUnit, GroupLayout
|
||||||
|
from game.theater import TheaterGroundObject
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TheaterUnit:
|
||||||
|
"""Representation of a single Unit in the Game"""
|
||||||
|
|
||||||
|
# Every Unit has a unique ID generated from the game
|
||||||
|
id: int
|
||||||
|
# The name of the Unit. Not required to be unique
|
||||||
|
name: str
|
||||||
|
# DCS UniType of the unit
|
||||||
|
type: Type[DcsUnitType]
|
||||||
|
# Position and orientation of the Unit
|
||||||
|
position: PointWithHeading
|
||||||
|
# The parent ground object
|
||||||
|
ground_object: TheaterGroundObject
|
||||||
|
# State of the unit, dead or alive
|
||||||
|
alive: bool = True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_template(
|
||||||
|
id: int, dcs_type: Type[DcsUnitType], t: LayoutUnit, go: TheaterGroundObject
|
||||||
|
) -> TheaterUnit:
|
||||||
|
return TheaterUnit(
|
||||||
|
id,
|
||||||
|
t.name,
|
||||||
|
dcs_type,
|
||||||
|
PointWithHeading.from_point(t.position, Heading.from_degrees(t.heading)),
|
||||||
|
go,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_type(self) -> Optional[UnitType[Any]]:
|
||||||
|
if issubclass(self.type, VehicleType):
|
||||||
|
return next(GroundUnitType.for_dcs_type(self.type))
|
||||||
|
elif issubclass(self.type, ShipType):
|
||||||
|
return next(ShipUnitType.for_dcs_type(self.type))
|
||||||
|
# None for not available StaticTypes
|
||||||
|
return None
|
||||||
|
|
||||||
|
def kill(self) -> None:
|
||||||
|
self.alive = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_name(self) -> str:
|
||||||
|
return f"{str(self.id).zfill(4)} | {self.name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> str:
|
||||||
|
dead_label = " [DEAD]" if not self.alive else ""
|
||||||
|
unit_label = self.unit_type or self.type.name or self.name
|
||||||
|
return f"{str(self.id).zfill(4)} | {unit_label}{dead_label}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_name(self) -> str:
|
||||||
|
dead_label = " [DEAD]" if not self.alive else ""
|
||||||
|
return f"<b>{self.type.id[0:18]}</b> {dead_label}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_static(self) -> bool:
|
||||||
|
return issubclass(self.type, StaticType)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_vehicle(self) -> bool:
|
||||||
|
return issubclass(self.type, VehicleType)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ship(self) -> bool:
|
||||||
|
return issubclass(self.type, ShipType)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
return self.type.id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repairable(self) -> bool:
|
||||||
|
# Only let units with UnitType be repairable as we just have prices for them
|
||||||
|
return self.unit_type is not None
|
||||||
|
|
||||||
|
|
||||||
|
class SceneryUnit(TheaterUnit):
|
||||||
|
"""Special TheaterUnit for handling scenery ground objects"""
|
||||||
|
|
||||||
|
# Scenery Objects are identified by a special trigger zone
|
||||||
|
zone: TriggerZone
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_name(self) -> str:
|
||||||
|
dead_label = " [DEAD]" if not self.alive else ""
|
||||||
|
return f"{str(self.id).zfill(4)} | {self.name}{dead_label}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_name(self) -> str:
|
||||||
|
dead_label = " [DEAD]" if not self.alive else ""
|
||||||
|
return f"<b>{self.name[0:18]}</b> {dead_label}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
return "missing"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repairable(self) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TheaterGroup:
|
||||||
|
"""Logical group for multiple TheaterUnits at a specific position"""
|
||||||
|
|
||||||
|
# Every Theater Group has a unique ID generated from the game
|
||||||
|
id: int # Unique ID
|
||||||
|
# The name of the Group. Not required to be unique
|
||||||
|
name: str
|
||||||
|
# Position and orientation of the Group
|
||||||
|
position: PointWithHeading
|
||||||
|
# All TheaterUnits within the group
|
||||||
|
units: list[TheaterUnit]
|
||||||
|
# The parent ground object
|
||||||
|
ground_object: TheaterGroundObject
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_template(
|
||||||
|
id: int,
|
||||||
|
g: GroupLayout,
|
||||||
|
go: TheaterGroundObject,
|
||||||
|
unit_type: Type[DcsUnitType],
|
||||||
|
unit_count: int,
|
||||||
|
) -> TheaterGroup:
|
||||||
|
return TheaterGroup(
|
||||||
|
id,
|
||||||
|
g.name,
|
||||||
|
PointWithHeading.from_point(go.position, go.heading),
|
||||||
|
g.generate_units(go, unit_type, unit_count),
|
||||||
|
go,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_name(self) -> str:
|
||||||
|
return f"{str(self.id).zfill(4)} | {self.name}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_count(self) -> int:
|
||||||
|
return len(self.units)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alive_units(self) -> int:
|
||||||
|
return sum([unit.alive for unit in self.units])
|
||||||
@ -12,9 +12,9 @@ from dcs.unitgroup import FlyingGroup, VehicleGroup, ShipGroup
|
|||||||
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
from game.dcs.groundunittype import GroundUnitType
|
||||||
from game.squadrons import Pilot
|
from game.squadrons import Pilot
|
||||||
from game.theater import Airfield, ControlPoint, GroundUnit
|
from game.theater import Airfield, ControlPoint, TheaterUnit
|
||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.theater.theatergroundobject import SceneryGroundUnit
|
from game.theater.theatergroup import SceneryUnit
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from game.transfers import CargoShip, Convoy, TransferOrder
|
from game.transfers import CargoShip, Convoy, TransferOrder
|
||||||
@ -33,14 +33,14 @@ class FrontLineUnit:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class GroundObjectMapping:
|
class TheaterUnitMapping:
|
||||||
ground_unit: GroundUnit
|
theater_unit: TheaterUnit
|
||||||
dcs_unit: Unit
|
dcs_unit: Unit
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SceneryObjectMapping:
|
class SceneryObjectMapping:
|
||||||
ground_unit: GroundUnit
|
ground_unit: TheaterUnit
|
||||||
trigger_zone: TriggerZone
|
trigger_zone: TriggerZone
|
||||||
|
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ 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_objects: Dict[str, GroundObjectMapping] = {}
|
self.theater_objects: Dict[str, TheaterUnitMapping] = {}
|
||||||
self.scenery_objects: Dict[str, SceneryObjectMapping] = {}
|
self.scenery_objects: Dict[str, SceneryObjectMapping] = {}
|
||||||
self.convoys: Dict[str, ConvoyUnit] = {}
|
self.convoys: Dict[str, ConvoyUnit] = {}
|
||||||
self.cargo_ships: Dict[str, CargoShip] = {}
|
self.cargo_ships: Dict[str, CargoShip] = {}
|
||||||
@ -103,18 +103,18 @@ class UnitMap:
|
|||||||
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
|
||||||
return self.front_line_units.get(name, None)
|
return self.front_line_units.get(name, None)
|
||||||
|
|
||||||
def add_ground_object_mapping(
|
def add_theater_unit_mapping(
|
||||||
self, ground_unit: GroundUnit, dcs_unit: Unit
|
self, theater_unit: TheaterUnit, dcs_unit: Unit
|
||||||
) -> None:
|
) -> None:
|
||||||
# Deaths for units at TGOs are recorded in the corresponding GroundUnit within
|
# Deaths for units at TGOs are recorded in the corresponding GroundUnit within
|
||||||
# the GroundGroup, so we have to match the dcs unit with the liberation unit
|
# the GroundGroup, so we have to match the dcs unit with the liberation unit
|
||||||
name = str(dcs_unit.name)
|
name = str(dcs_unit.name)
|
||||||
if name in self.ground_objects:
|
if name in self.theater_objects:
|
||||||
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
raise RuntimeError(f"Duplicate TGO unit: {name}")
|
||||||
self.ground_objects[name] = GroundObjectMapping(ground_unit, dcs_unit)
|
self.theater_objects[name] = TheaterUnitMapping(theater_unit, dcs_unit)
|
||||||
|
|
||||||
def ground_object(self, name: str) -> Optional[GroundObjectMapping]:
|
def theater_units(self, name: str) -> Optional[TheaterUnitMapping]:
|
||||||
return self.ground_objects.get(name, None)
|
return self.theater_objects.get(name, None)
|
||||||
|
|
||||||
def add_convoy_units(self, group: VehicleGroup, 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()):
|
||||||
@ -170,9 +170,7 @@ 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_scenery(
|
def add_scenery(self, scenery_unit: SceneryUnit, trigger_zone: TriggerZone) -> None:
|
||||||
self, scenery_unit: SceneryGroundUnit, trigger_zone: TriggerZone
|
|
||||||
) -> None:
|
|
||||||
name = str(trigger_zone.name)
|
name = str(trigger_zone.name)
|
||||||
if name in self.scenery_objects:
|
if name in self.scenery_objects:
|
||||||
raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)")
|
raise RuntimeError(f"Duplicate scenery object {name} (TriggerZone)")
|
||||||
|
|||||||
@ -44,11 +44,11 @@ from game.theater import (
|
|||||||
NavalControlPoint,
|
NavalControlPoint,
|
||||||
SamGroundObject,
|
SamGroundObject,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
|
TheaterUnit,
|
||||||
)
|
)
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
EwrGroundObject,
|
EwrGroundObject,
|
||||||
NavalGroundObject,
|
NavalGroundObject,
|
||||||
GroundUnit,
|
|
||||||
)
|
)
|
||||||
from game.typeguard import self_type_guard
|
from game.typeguard import self_type_guard
|
||||||
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
from game.utils import Distance, Heading, Speed, feet, knots, meters, nautical_miles
|
||||||
@ -1086,7 +1086,7 @@ class FlightPlanBuilder:
|
|||||||
self,
|
self,
|
||||||
flight: Flight,
|
flight: Flight,
|
||||||
# TODO: Custom targets should be an attribute of the flight.
|
# TODO: Custom targets should be an attribute of the flight.
|
||||||
custom_targets: Optional[List[GroundUnit]] = None,
|
custom_targets: Optional[List[TheaterUnit]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Creates a default flight plan for the given mission."""
|
"""Creates a default flight plan for the given mission."""
|
||||||
if flight not in self.package.flights:
|
if flight not in self.package.flights:
|
||||||
@ -1106,7 +1106,7 @@ class FlightPlanBuilder:
|
|||||||
) from ex
|
) from ex
|
||||||
|
|
||||||
def generate_flight_plan(
|
def generate_flight_plan(
|
||||||
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
|
self, flight: Flight, custom_targets: Optional[List[TheaterUnit]]
|
||||||
) -> FlightPlan:
|
) -> FlightPlan:
|
||||||
# TODO: Flesh out mission types.
|
# TODO: Flesh out mission types.
|
||||||
task = flight.flight_type
|
task = flight.flight_type
|
||||||
@ -1209,7 +1209,7 @@ class FlightPlanBuilder:
|
|||||||
targets: List[StrikeTarget] = []
|
targets: List[StrikeTarget] = []
|
||||||
|
|
||||||
for j, u in enumerate(location.strike_targets):
|
for j, u in enumerate(location.strike_targets):
|
||||||
targets.append(StrikeTarget(f"{u.type} #{j}", u))
|
targets.append(StrikeTarget(f"{u.type.id} #{j}", u))
|
||||||
|
|
||||||
return self.strike_flightplan(
|
return self.strike_flightplan(
|
||||||
flight, location, FlightWaypointType.INGRESS_STRIKE, targets
|
flight, location, FlightWaypointType.INGRESS_STRIKE, targets
|
||||||
@ -1668,7 +1668,7 @@ class FlightPlanBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def generate_dead(
|
def generate_dead(
|
||||||
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
|
self, flight: Flight, custom_targets: Optional[List[TheaterUnit]]
|
||||||
) -> StrikeFlightPlan:
|
) -> StrikeFlightPlan:
|
||||||
"""Generate a DEAD flight at a given location.
|
"""Generate a DEAD flight at a given location.
|
||||||
|
|
||||||
@ -1738,7 +1738,7 @@ class FlightPlanBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def generate_sead(
|
def generate_sead(
|
||||||
self, flight: Flight, custom_targets: Optional[List[GroundUnit]]
|
self, flight: Flight, custom_targets: Optional[List[TheaterUnit]]
|
||||||
) -> StrikeFlightPlan:
|
) -> StrikeFlightPlan:
|
||||||
"""Generate a SEAD flight at a given location.
|
"""Generate a SEAD flight at a given location.
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ from game.theater import (
|
|||||||
MissionTarget,
|
MissionTarget,
|
||||||
OffMapSpawn,
|
OffMapSpawn,
|
||||||
TheaterGroundObject,
|
TheaterGroundObject,
|
||||||
|
TheaterUnit,
|
||||||
)
|
)
|
||||||
from game.utils import Distance, meters, nautical_miles
|
from game.utils import Distance, meters, nautical_miles
|
||||||
from game.ato.flightwaypointtype import FlightWaypointType
|
from game.ato.flightwaypointtype import FlightWaypointType
|
||||||
@ -28,13 +29,13 @@ if TYPE_CHECKING:
|
|||||||
from game.ato.flight import Flight
|
from game.ato.flight import Flight
|
||||||
from game.coalition import Coalition
|
from game.coalition import Coalition
|
||||||
from game.transfers import MultiGroupTransport
|
from game.transfers import MultiGroupTransport
|
||||||
from game.theater.theatergroundobject import GroundUnit, GroundGroup
|
from game.theater.theatergroup import TheaterGroup
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StrikeTarget:
|
class StrikeTarget:
|
||||||
name: str
|
name: str
|
||||||
target: Union[TheaterGroundObject, GroundGroup, GroundUnit, MultiGroupTransport]
|
target: Union[TheaterGroundObject, TheaterGroup, TheaterUnit, MultiGroupTransport]
|
||||||
|
|
||||||
|
|
||||||
class WaypointBuilder:
|
class WaypointBuilder:
|
||||||
|
|||||||
@ -100,28 +100,28 @@ class GroundPlanner:
|
|||||||
# 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:
|
||||||
unit_class = unit_type.unit_class
|
unit_class = unit_type.unit_class
|
||||||
if unit_class is UnitClass.Tank:
|
if unit_class is UnitClass.TANK:
|
||||||
collection = self.tank_groups
|
collection = self.tank_groups
|
||||||
role = CombatGroupRole.TANK
|
role = CombatGroupRole.TANK
|
||||||
elif unit_class is UnitClass.Apc:
|
elif unit_class is UnitClass.APC:
|
||||||
collection = self.apc_group
|
collection = self.apc_group
|
||||||
role = CombatGroupRole.APC
|
role = CombatGroupRole.APC
|
||||||
elif unit_class is UnitClass.Artillery:
|
elif unit_class is UnitClass.ARTILLERY:
|
||||||
collection = self.art_group
|
collection = self.art_group
|
||||||
role = CombatGroupRole.ARTILLERY
|
role = CombatGroupRole.ARTILLERY
|
||||||
elif unit_class is UnitClass.Ifv:
|
elif unit_class is UnitClass.IFV:
|
||||||
collection = self.ifv_group
|
collection = self.ifv_group
|
||||||
role = CombatGroupRole.IFV
|
role = CombatGroupRole.IFV
|
||||||
elif unit_class is UnitClass.Logistics:
|
elif unit_class is UnitClass.LOGISTICS:
|
||||||
collection = self.logi_groups
|
collection = self.logi_groups
|
||||||
role = CombatGroupRole.LOGI
|
role = CombatGroupRole.LOGI
|
||||||
elif unit_class is UnitClass.Atgm:
|
elif unit_class is UnitClass.ATGM:
|
||||||
collection = self.atgm_group
|
collection = self.atgm_group
|
||||||
role = CombatGroupRole.ATGM
|
role = CombatGroupRole.ATGM
|
||||||
elif unit_class is UnitClass.SHORAD:
|
elif unit_class is UnitClass.SHORAD:
|
||||||
collection = self.shorad_groups
|
collection = self.shorad_groups
|
||||||
role = CombatGroupRole.SHORAD
|
role = CombatGroupRole.SHORAD
|
||||||
elif unit_class is UnitClass.Recon:
|
elif unit_class is UnitClass.RECON:
|
||||||
collection = self.recon_groups
|
collection = self.recon_groups
|
||||||
role = CombatGroupRole.RECON
|
role = CombatGroupRole.RECON
|
||||||
else:
|
else:
|
||||||
|
|||||||
716
gen/templates.py
716
gen/templates.py
@ -1,716 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
import logging
|
|
||||||
import random
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Iterator, Any, TYPE_CHECKING, Optional, Union
|
|
||||||
|
|
||||||
import dcs
|
|
||||||
import yaml
|
|
||||||
from dcs import Point
|
|
||||||
from dcs.unit import Unit
|
|
||||||
from dcs.unitgroup import StaticGroup
|
|
||||||
from dcs.unittype import VehicleType, ShipType
|
|
||||||
from game.data.radar_db import UNITS_WITH_RADAR
|
|
||||||
from game.data.units import UnitClass
|
|
||||||
from game.data.groups import GroupRole, GroupTask, ROLE_TASKINGS
|
|
||||||
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
|
||||||
from game.dcs.shipunittype import ShipUnitType
|
|
||||||
from game.dcs.unittype import UnitType
|
|
||||||
from game.theater.theatergroundobject import (
|
|
||||||
SamGroundObject,
|
|
||||||
EwrGroundObject,
|
|
||||||
BuildingGroundObject,
|
|
||||||
GroundGroup,
|
|
||||||
MissileSiteGroundObject,
|
|
||||||
ShipGroundObject,
|
|
||||||
CarrierGroundObject,
|
|
||||||
LhaGroundObject,
|
|
||||||
CoastalSiteGroundObject,
|
|
||||||
VehicleGroupGroundObject,
|
|
||||||
IadsGroundObject,
|
|
||||||
GroundUnit,
|
|
||||||
)
|
|
||||||
from game.point_with_heading import PointWithHeading
|
|
||||||
|
|
||||||
from game.utils import Heading
|
|
||||||
from game import db
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from game import Game
|
|
||||||
from game.factions.faction import Faction
|
|
||||||
from game.theater import TheaterGroundObject, ControlPoint
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UnitTemplate:
|
|
||||||
name: str
|
|
||||||
position: Point
|
|
||||||
heading: int
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_unit(unit: Unit) -> UnitTemplate:
|
|
||||||
return UnitTemplate(
|
|
||||||
unit.name,
|
|
||||||
Point(int(unit.position.x), int(unit.position.y)),
|
|
||||||
int(unit.heading),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GroupTemplate:
|
|
||||||
name: str
|
|
||||||
units: list[UnitTemplate]
|
|
||||||
|
|
||||||
# Is Static group
|
|
||||||
static: bool = False
|
|
||||||
|
|
||||||
# The group this template will be merged into
|
|
||||||
group: int = 1
|
|
||||||
|
|
||||||
# Define the amount of random units to be created by the randomizer.
|
|
||||||
# This can be a fixed int or a random value from a range of two ints as tuple
|
|
||||||
unit_count: list[int] = field(default_factory=list)
|
|
||||||
|
|
||||||
# defintion which unit types are supported
|
|
||||||
unit_types: list[str] = field(default_factory=list)
|
|
||||||
unit_classes: list[UnitClass] = field(default_factory=list)
|
|
||||||
alternative_classes: list[UnitClass] = field(default_factory=list)
|
|
||||||
|
|
||||||
# Defines if this groupTemplate is required or not
|
|
||||||
optional: bool = False
|
|
||||||
|
|
||||||
# Used to determine if the group should be generated or not
|
|
||||||
_possible_types: list[str] = field(default_factory=list)
|
|
||||||
_enabled: bool = True
|
|
||||||
_unit_type: Optional[str] = None
|
|
||||||
_unit_counter: Optional[int] = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def should_be_generated(self) -> bool:
|
|
||||||
if self.optional:
|
|
||||||
return self._enabled
|
|
||||||
return True
|
|
||||||
|
|
||||||
def enable(self) -> None:
|
|
||||||
self._enabled = True
|
|
||||||
|
|
||||||
def disable(self) -> None:
|
|
||||||
self._enabled = False
|
|
||||||
|
|
||||||
def set_enabled(self, enabled: bool) -> None:
|
|
||||||
self._enabled = enabled
|
|
||||||
|
|
||||||
def is_enabled(self) -> bool:
|
|
||||||
return self._enabled
|
|
||||||
|
|
||||||
def set_unit_type(self, unit_type: str) -> None:
|
|
||||||
self._unit_type = unit_type
|
|
||||||
|
|
||||||
def set_possible_types(self, unit_types: list[str]) -> None:
|
|
||||||
self._possible_types = unit_types
|
|
||||||
|
|
||||||
def can_use_unit(self, unit_type: UnitType[Any]) -> bool:
|
|
||||||
return (
|
|
||||||
self.can_use_unit_type(unit_type.dcs_id)
|
|
||||||
or unit_type.unit_class in self.unit_classes
|
|
||||||
)
|
|
||||||
|
|
||||||
def can_use_unit_type(self, unit_type: str) -> bool:
|
|
||||||
return unit_type in self.unit_types
|
|
||||||
|
|
||||||
def reset_unit_counter(self) -> None:
|
|
||||||
count = len(self.units)
|
|
||||||
if self.unit_count:
|
|
||||||
if len(self.unit_count) == 1:
|
|
||||||
count = self.unit_count[0]
|
|
||||||
else:
|
|
||||||
count = random.choice(range(min(self.unit_count), max(self.unit_count)))
|
|
||||||
self._unit_counter = count
|
|
||||||
|
|
||||||
@property
|
|
||||||
def unit_type(self) -> str:
|
|
||||||
if self._unit_type:
|
|
||||||
# Forced type
|
|
||||||
return self._unit_type
|
|
||||||
unit_types = self._possible_types or self.unit_types
|
|
||||||
if unit_types:
|
|
||||||
# Random type
|
|
||||||
return random.choice(unit_types)
|
|
||||||
raise RuntimeError("TemplateGroup has no unit_type")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def size(self) -> int:
|
|
||||||
if not self._unit_counter:
|
|
||||||
self.reset_unit_counter()
|
|
||||||
assert self._unit_counter is not None
|
|
||||||
return self._unit_counter
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_size(self) -> int:
|
|
||||||
return len(self.units)
|
|
||||||
|
|
||||||
def use_unit(self) -> None:
|
|
||||||
if self._unit_counter is None:
|
|
||||||
self.reset_unit_counter()
|
|
||||||
if self._unit_counter and self._unit_counter > 0:
|
|
||||||
self._unit_counter -= 1
|
|
||||||
else:
|
|
||||||
raise IndexError
|
|
||||||
|
|
||||||
@property
|
|
||||||
def can_be_modified(self) -> bool:
|
|
||||||
return len(self._possible_types) > 1 or len(self.unit_count) > 1
|
|
||||||
|
|
||||||
@property
|
|
||||||
def statics(self) -> Iterator[str]:
|
|
||||||
for unit_type in self._possible_types or self.unit_types:
|
|
||||||
if db.static_type_from_name(unit_type):
|
|
||||||
yield unit_type
|
|
||||||
|
|
||||||
@property
|
|
||||||
def possible_units(self) -> Iterator[UnitType[Any]]:
|
|
||||||
for unit_type in self._possible_types or self.unit_types:
|
|
||||||
dcs_unit_type = db.unit_type_from_name(unit_type)
|
|
||||||
if dcs_unit_type is None:
|
|
||||||
raise RuntimeError(f"Unit Type {unit_type} not a valid dcs type")
|
|
||||||
try:
|
|
||||||
if issubclass(dcs_unit_type, VehicleType):
|
|
||||||
yield next(GroundUnitType.for_dcs_type(dcs_unit_type))
|
|
||||||
elif issubclass(dcs_unit_type, ShipType):
|
|
||||||
yield next(ShipUnitType.for_dcs_type(dcs_unit_type))
|
|
||||||
except StopIteration:
|
|
||||||
continue
|
|
||||||
|
|
||||||
def generate_units(self, go: TheaterGroundObject) -> list[GroundUnit]:
|
|
||||||
self.reset_unit_counter()
|
|
||||||
units = []
|
|
||||||
unit_type = self.unit_type
|
|
||||||
for u_id, unit in enumerate(self.units):
|
|
||||||
tgo_unit = GroundUnit.from_template(u_id, unit_type, unit, go)
|
|
||||||
try:
|
|
||||||
# Check if unit can be assigned
|
|
||||||
self.use_unit()
|
|
||||||
except IndexError:
|
|
||||||
# Do not generate the unit as no more units are available
|
|
||||||
continue
|
|
||||||
units.append(tgo_unit)
|
|
||||||
return units
|
|
||||||
|
|
||||||
|
|
||||||
class GroundObjectTemplate:
|
|
||||||
def __init__(self, name: str, description: str = "") -> None:
|
|
||||||
self.name = name
|
|
||||||
self.description = description
|
|
||||||
self.tasks: list[GroupTask] = [] # The supported tasks
|
|
||||||
self.groups: list[GroupTemplate] = []
|
|
||||||
self.category: str = "" # Only used for building templates
|
|
||||||
|
|
||||||
# If the template is generic it will be used the generate the general
|
|
||||||
# UnitGroups during faction initialization. Generic Groups allow to be mixed
|
|
||||||
self.generic: bool = False
|
|
||||||
|
|
||||||
def generate(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: PointWithHeading,
|
|
||||||
control_point: ControlPoint,
|
|
||||||
game: Game,
|
|
||||||
merge_groups: bool = True,
|
|
||||||
) -> TheaterGroundObject:
|
|
||||||
|
|
||||||
# Create the ground_object based on the type
|
|
||||||
ground_object = self._create_ground_object(name, position, control_point)
|
|
||||||
# Generate all groups using the randomization if it defined
|
|
||||||
for g_id, group in enumerate(self.groups):
|
|
||||||
if not group.should_be_generated:
|
|
||||||
continue
|
|
||||||
# Static and non Static groups have to be separated
|
|
||||||
unit_count = 0
|
|
||||||
group_id = (group.group - 1) if merge_groups else g_id
|
|
||||||
if not merge_groups or len(ground_object.groups) <= group_id:
|
|
||||||
# Requested group was not yet created
|
|
||||||
ground_group = GroundGroup.from_template(
|
|
||||||
game.next_group_id(),
|
|
||||||
group,
|
|
||||||
ground_object,
|
|
||||||
)
|
|
||||||
# Set Group Name
|
|
||||||
ground_group.name = f"{self.name} {group_id}"
|
|
||||||
ground_object.groups.append(ground_group)
|
|
||||||
units = ground_group.units
|
|
||||||
else:
|
|
||||||
ground_group = ground_object.groups[group_id]
|
|
||||||
units = group.generate_units(ground_object)
|
|
||||||
unit_count = len(ground_group.units)
|
|
||||||
ground_group.units.extend(units)
|
|
||||||
|
|
||||||
# Assign UniqueID, name and align relative to ground_object
|
|
||||||
for u_id, unit in enumerate(units):
|
|
||||||
unit.id = game.next_unit_id()
|
|
||||||
unit.name = f"{self.name} {group_id}-{unit_count + u_id}"
|
|
||||||
unit.position = PointWithHeading.from_point(
|
|
||||||
Point(
|
|
||||||
ground_object.position.x + unit.position.x,
|
|
||||||
ground_object.position.y + unit.position.y,
|
|
||||||
),
|
|
||||||
# Align heading to GroundObject defined by the campaign designer
|
|
||||||
unit.position.heading + ground_object.heading,
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
isinstance(self, AntiAirTemplate)
|
|
||||||
and unit.unit_type
|
|
||||||
and unit.unit_type.dcs_unit_type in UNITS_WITH_RADAR
|
|
||||||
):
|
|
||||||
# Head Radars towards the center of the conflict
|
|
||||||
unit.position.heading = (
|
|
||||||
game.theater.heading_to_conflict_from(unit.position)
|
|
||||||
or unit.position.heading
|
|
||||||
)
|
|
||||||
# Rotate unit around the center to align the orientation of the group
|
|
||||||
unit.position.rotate(ground_object.position, ground_object.heading)
|
|
||||||
|
|
||||||
return ground_object
|
|
||||||
|
|
||||||
def _create_ground_object(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: PointWithHeading,
|
|
||||||
control_point: ControlPoint,
|
|
||||||
) -> TheaterGroundObject:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def add_group(self, new_group: GroupTemplate, index: int = 0) -> None:
|
|
||||||
"""Adds a group in the correct order to the template"""
|
|
||||||
if len(self.groups) > index:
|
|
||||||
self.groups.insert(index, new_group)
|
|
||||||
else:
|
|
||||||
self.groups.append(new_group)
|
|
||||||
|
|
||||||
def estimated_price_for(self, go: TheaterGroundObject) -> float:
|
|
||||||
# Price can only be estimated because of randomization
|
|
||||||
price = 0
|
|
||||||
for group in self.groups:
|
|
||||||
if group.should_be_generated:
|
|
||||||
for unit in group.generate_units(go):
|
|
||||||
if unit.unit_type:
|
|
||||||
price += unit.unit_type.price
|
|
||||||
return price
|
|
||||||
|
|
||||||
@property
|
|
||||||
def size(self) -> int:
|
|
||||||
return sum([len(group.units) for group in self.groups])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def statics(self) -> Iterator[str]:
|
|
||||||
for group in self.groups:
|
|
||||||
yield from group.statics
|
|
||||||
|
|
||||||
@property
|
|
||||||
def units(self) -> Iterator[UnitType[Any]]:
|
|
||||||
for group in self.groups:
|
|
||||||
yield from group.possible_units
|
|
||||||
|
|
||||||
|
|
||||||
class AntiAirTemplate(GroundObjectTemplate):
|
|
||||||
def _create_ground_object(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: PointWithHeading,
|
|
||||||
control_point: ControlPoint,
|
|
||||||
) -> IadsGroundObject:
|
|
||||||
|
|
||||||
if GroupTask.EWR in self.tasks:
|
|
||||||
return EwrGroundObject(name, position, position.heading, control_point)
|
|
||||||
elif any(tasking in self.tasks for tasking in ROLE_TASKINGS[GroupRole.AntiAir]):
|
|
||||||
return SamGroundObject(name, position, position.heading, control_point)
|
|
||||||
raise RuntimeError(
|
|
||||||
f" No Template for AntiAir tasking ({', '.join(task.value for task in self.tasks)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BuildingTemplate(GroundObjectTemplate):
|
|
||||||
def _create_ground_object(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: PointWithHeading,
|
|
||||||
control_point: ControlPoint,
|
|
||||||
) -> BuildingGroundObject:
|
|
||||||
return BuildingGroundObject(
|
|
||||||
name,
|
|
||||||
self.category,
|
|
||||||
position,
|
|
||||||
Heading.from_degrees(0),
|
|
||||||
control_point,
|
|
||||||
self.category == "fob",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NavalTemplate(GroundObjectTemplate):
|
|
||||||
def _create_ground_object(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: PointWithHeading,
|
|
||||||
control_point: ControlPoint,
|
|
||||||
) -> TheaterGroundObject:
|
|
||||||
if GroupTask.Navy in self.tasks:
|
|
||||||
return ShipGroundObject(name, position, control_point)
|
|
||||||
elif GroupTask.AircraftCarrier in self.tasks:
|
|
||||||
return CarrierGroundObject(name, control_point)
|
|
||||||
elif GroupTask.HelicopterCarrier in self.tasks:
|
|
||||||
return LhaGroundObject(name, control_point)
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class DefensesTemplate(GroundObjectTemplate):
|
|
||||||
def _create_ground_object(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: PointWithHeading,
|
|
||||||
control_point: ControlPoint,
|
|
||||||
) -> TheaterGroundObject:
|
|
||||||
if GroupTask.Missile in self.tasks:
|
|
||||||
return MissileSiteGroundObject(
|
|
||||||
name, position, position.heading, control_point
|
|
||||||
)
|
|
||||||
elif GroupTask.Coastal in self.tasks:
|
|
||||||
return CoastalSiteGroundObject(
|
|
||||||
name, position, control_point, position.heading
|
|
||||||
)
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class GroundForceTemplate(GroundObjectTemplate):
|
|
||||||
def _create_ground_object(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
position: PointWithHeading,
|
|
||||||
control_point: ControlPoint,
|
|
||||||
) -> TheaterGroundObject:
|
|
||||||
return VehicleGroupGroundObject(name, position, position.heading, control_point)
|
|
||||||
|
|
||||||
|
|
||||||
TEMPLATE_TYPES = {
|
|
||||||
GroupRole.AntiAir: AntiAirTemplate,
|
|
||||||
GroupRole.Building: BuildingTemplate,
|
|
||||||
GroupRole.Naval: NavalTemplate,
|
|
||||||
GroupRole.GroundForce: GroundForceTemplate,
|
|
||||||
GroupRole.Defenses: DefensesTemplate,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GroupTemplateMapping:
|
|
||||||
# The group name used in the template.miz
|
|
||||||
name: str
|
|
||||||
|
|
||||||
# Defines if the group is required for the template or can be skipped
|
|
||||||
optional: bool = False
|
|
||||||
|
|
||||||
# All static units for the group
|
|
||||||
statics: list[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
# Defines to which tgo group the groupTemplate will be added
|
|
||||||
# This allows to merge groups back together. Default: Merge all to group 1
|
|
||||||
group: int = field(default=1)
|
|
||||||
|
|
||||||
# Randomization settings. If left empty the template will be generated with the
|
|
||||||
# exact values (amount of units and unit_type) defined in the template.miz
|
|
||||||
# How many units the template should generate. Will be used for randomization
|
|
||||||
unit_count: list[int] = field(default_factory=list)
|
|
||||||
# All unit types the template supports.
|
|
||||||
unit_types: list[str] = field(default_factory=list)
|
|
||||||
# All unit classes the template supports.
|
|
||||||
unit_classes: list[UnitClass] = field(default_factory=list)
|
|
||||||
alternative_classes: list[UnitClass] = field(default_factory=list)
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
d = self.__dict__
|
|
||||||
if not self.optional:
|
|
||||||
d.pop("optional")
|
|
||||||
if not self.statics:
|
|
||||||
d.pop("statics")
|
|
||||||
if not self.unit_types:
|
|
||||||
d.pop("unit_types")
|
|
||||||
if not self.unit_classes:
|
|
||||||
d.pop("unit_classes")
|
|
||||||
else:
|
|
||||||
d["unit_classes"] = [unit_class.value for unit_class in self.unit_classes]
|
|
||||||
if not self.alternative_classes:
|
|
||||||
d.pop("alternative_classes")
|
|
||||||
else:
|
|
||||||
d["alternative_classes"] = [
|
|
||||||
unit_class.value for unit_class in self.alternative_classes
|
|
||||||
]
|
|
||||||
if not self.unit_count:
|
|
||||||
d.pop("unit_count")
|
|
||||||
return d
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(d: dict[str, Any]) -> GroupTemplateMapping:
|
|
||||||
optional = d["optional"] if "optional" in d else False
|
|
||||||
statics = d["statics"] if "statics" in d else []
|
|
||||||
unit_count = d["unit_count"] if "unit_count" in d else []
|
|
||||||
unit_types = d["unit_types"] if "unit_types" in d else []
|
|
||||||
group = d["group"] if "group" in d else 1
|
|
||||||
unit_classes = (
|
|
||||||
[UnitClass(u) for u in d["unit_classes"]] if "unit_classes" in d else []
|
|
||||||
)
|
|
||||||
alternative_classes = (
|
|
||||||
[UnitClass(u) for u in d["alternative_classes"]]
|
|
||||||
if "alternative_classes" in d
|
|
||||||
else []
|
|
||||||
)
|
|
||||||
return GroupTemplateMapping(
|
|
||||||
d["name"],
|
|
||||||
optional,
|
|
||||||
statics,
|
|
||||||
group,
|
|
||||||
unit_count,
|
|
||||||
unit_types,
|
|
||||||
unit_classes,
|
|
||||||
alternative_classes,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TemplateMapping:
|
|
||||||
# The name of the Template
|
|
||||||
name: str
|
|
||||||
|
|
||||||
# An optional description to give more information about the template
|
|
||||||
description: str
|
|
||||||
|
|
||||||
# An optional description to give more information about the template
|
|
||||||
category: str
|
|
||||||
|
|
||||||
# Optional field to define if the template can be used to create generic groups
|
|
||||||
generic: bool
|
|
||||||
|
|
||||||
# The role the template can be used for
|
|
||||||
role: GroupRole
|
|
||||||
|
|
||||||
# All taskings the template can be used for
|
|
||||||
tasks: list[GroupTask]
|
|
||||||
|
|
||||||
# All Groups the template has
|
|
||||||
groups: list[GroupTemplateMapping] = field(default_factory=list)
|
|
||||||
|
|
||||||
# Define the miz file for the template. Optional. If empty use the mapping name
|
|
||||||
template_file: str = field(default="")
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
|
||||||
d = {
|
|
||||||
"name": self.name,
|
|
||||||
"description": self.description,
|
|
||||||
"category": self.category,
|
|
||||||
"generic": self.generic,
|
|
||||||
"role": self.role.value,
|
|
||||||
"tasks": [task.value for task in self.tasks],
|
|
||||||
"groups": [group.to_dict() for group in self.groups],
|
|
||||||
"template_file": self.template_file,
|
|
||||||
}
|
|
||||||
if not self.description:
|
|
||||||
d.pop("description")
|
|
||||||
if not self.category:
|
|
||||||
d.pop("category")
|
|
||||||
if not self.generic:
|
|
||||||
# Only save if true
|
|
||||||
d.pop("generic")
|
|
||||||
if not self.template_file:
|
|
||||||
d.pop("template_file")
|
|
||||||
return d
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_dict(d: dict[str, Any], file_name: str) -> TemplateMapping:
|
|
||||||
groups = [GroupTemplateMapping.from_dict(group) for group in d["groups"]]
|
|
||||||
description = d["description"] if "description" in d else ""
|
|
||||||
category = d["category"] if "category" in d else ""
|
|
||||||
generic = d["generic"] if "generic" in d else False
|
|
||||||
template_file = (
|
|
||||||
d["template_file"]
|
|
||||||
if "template_file" in d
|
|
||||||
else file_name.replace("yaml", "miz")
|
|
||||||
)
|
|
||||||
tasks = [GroupTask(task) for task in d["tasks"]]
|
|
||||||
return TemplateMapping(
|
|
||||||
d["name"],
|
|
||||||
description,
|
|
||||||
category,
|
|
||||||
generic,
|
|
||||||
GroupRole(d["role"]),
|
|
||||||
tasks,
|
|
||||||
groups,
|
|
||||||
template_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
def export(self, mapping_folder: str) -> None:
|
|
||||||
file_name = self.name
|
|
||||||
for char in ["\\", "/", " ", "'", '"']:
|
|
||||||
file_name = file_name.replace(char, "_")
|
|
||||||
|
|
||||||
f = mapping_folder + file_name + ".yaml"
|
|
||||||
with open(f, "w", encoding="utf-8") as data_file:
|
|
||||||
yaml.dump(self.to_dict(), data_file, Dumper=MappingDumper, sort_keys=False)
|
|
||||||
|
|
||||||
|
|
||||||
# Custom Dumper to fix pyyaml indent https://github.com/yaml/pyyaml/issues/234
|
|
||||||
class MappingDumper(yaml.Dumper):
|
|
||||||
def increase_indent(self, flow: bool = False, *args: Any, **kwargs: Any) -> None:
|
|
||||||
return super().increase_indent(flow=flow, indentless=False)
|
|
||||||
|
|
||||||
|
|
||||||
class GroundObjectTemplates:
|
|
||||||
# list of templates per category. e.g. AA or similar
|
|
||||||
_templates: dict[GroupRole, list[GroundObjectTemplate]]
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._templates = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def templates(self) -> Iterator[tuple[GroupRole, GroundObjectTemplate]]:
|
|
||||||
for category, templates in self._templates.items():
|
|
||||||
for template in templates:
|
|
||||||
yield category, template
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_folder(cls, folder: str) -> GroundObjectTemplates:
|
|
||||||
templates = GroundObjectTemplates()
|
|
||||||
mappings: dict[str, list[TemplateMapping]] = {}
|
|
||||||
for file in Path(folder).rglob("*.yaml"):
|
|
||||||
if not file.is_file():
|
|
||||||
continue
|
|
||||||
with file.open("r", encoding="utf-8") as f:
|
|
||||||
mapping_dict = yaml.safe_load(f)
|
|
||||||
|
|
||||||
template_map = TemplateMapping.from_dict(mapping_dict, f.name)
|
|
||||||
|
|
||||||
if template_map.template_file in mappings:
|
|
||||||
mappings[template_map.template_file].append(template_map)
|
|
||||||
else:
|
|
||||||
mappings[template_map.template_file] = [template_map]
|
|
||||||
|
|
||||||
for miz, maps in mappings.items():
|
|
||||||
for role, template in cls.load_from_miz(miz, maps).templates:
|
|
||||||
templates.add_template(role, template)
|
|
||||||
return templates
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mapping_for_group(
|
|
||||||
mappings: list[TemplateMapping], group_name: str
|
|
||||||
) -> tuple[TemplateMapping, int, GroupTemplateMapping]:
|
|
||||||
for mapping in mappings:
|
|
||||||
for g_id, group_mapping in enumerate(mapping.groups):
|
|
||||||
if (
|
|
||||||
group_mapping.name == group_name
|
|
||||||
or group_name in group_mapping.statics
|
|
||||||
):
|
|
||||||
return mapping, g_id, group_mapping
|
|
||||||
raise KeyError
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def load_from_miz(
|
|
||||||
cls, miz: str, mappings: list[TemplateMapping]
|
|
||||||
) -> GroundObjectTemplates:
|
|
||||||
template_position: dict[str, Point] = {}
|
|
||||||
templates = GroundObjectTemplates()
|
|
||||||
temp_mis = dcs.Mission()
|
|
||||||
temp_mis.load_file(miz)
|
|
||||||
|
|
||||||
for country in itertools.chain(
|
|
||||||
temp_mis.coalition["red"].countries.values(),
|
|
||||||
temp_mis.coalition["blue"].countries.values(),
|
|
||||||
):
|
|
||||||
for dcs_group in itertools.chain(
|
|
||||||
temp_mis.country(country.name).vehicle_group,
|
|
||||||
temp_mis.country(country.name).ship_group,
|
|
||||||
temp_mis.country(country.name).static_group,
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
mapping, group_id, group_mapping = cls.mapping_for_group(
|
|
||||||
mappings, dcs_group.name
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
logging.error(f"No mapping for dcs group {dcs_group.name}")
|
|
||||||
continue
|
|
||||||
template = templates.by_name(mapping.name)
|
|
||||||
if not template:
|
|
||||||
template = TEMPLATE_TYPES[mapping.role](
|
|
||||||
mapping.name, mapping.description
|
|
||||||
)
|
|
||||||
template.category = mapping.category
|
|
||||||
template.generic = mapping.generic
|
|
||||||
template.tasks = mapping.tasks
|
|
||||||
templates.add_template(mapping.role, template)
|
|
||||||
|
|
||||||
for i, unit in enumerate(dcs_group.units):
|
|
||||||
group_template = None
|
|
||||||
for group in template.groups:
|
|
||||||
if group.name == dcs_group.name or (
|
|
||||||
isinstance(dcs_group, StaticGroup)
|
|
||||||
and dcs_group.units[0].type in group.unit_types
|
|
||||||
):
|
|
||||||
# MovingGroups are matched by name, statics by unit_type
|
|
||||||
group_template = group
|
|
||||||
if not group_template:
|
|
||||||
group_template = GroupTemplate(
|
|
||||||
dcs_group.name,
|
|
||||||
[],
|
|
||||||
True if isinstance(dcs_group, StaticGroup) else False,
|
|
||||||
group_mapping.group,
|
|
||||||
group_mapping.unit_count,
|
|
||||||
group_mapping.unit_types,
|
|
||||||
group_mapping.unit_classes,
|
|
||||||
group_mapping.alternative_classes,
|
|
||||||
)
|
|
||||||
group_template.optional = group_mapping.optional
|
|
||||||
# Add the group at the correct position
|
|
||||||
template.add_group(group_template, group_id)
|
|
||||||
unit_template = UnitTemplate.from_unit(unit)
|
|
||||||
if i == 0 and template.name not in template_position:
|
|
||||||
template_position[template.name] = unit.position
|
|
||||||
unit_template.position = (
|
|
||||||
unit_template.position - template_position[template.name]
|
|
||||||
)
|
|
||||||
group_template.units.append(unit_template)
|
|
||||||
|
|
||||||
return templates
|
|
||||||
|
|
||||||
@property
|
|
||||||
def all(self) -> Iterator[GroundObjectTemplate]:
|
|
||||||
for templates in self._templates.values():
|
|
||||||
yield from templates
|
|
||||||
|
|
||||||
def by_name(self, template_name: str) -> Optional[GroundObjectTemplate]:
|
|
||||||
for template in self.all:
|
|
||||||
if template.name == template_name:
|
|
||||||
return template
|
|
||||||
return None
|
|
||||||
|
|
||||||
def add_template(self, role: GroupRole, template: GroundObjectTemplate) -> None:
|
|
||||||
if role not in self._templates:
|
|
||||||
self._templates[role] = [template]
|
|
||||||
else:
|
|
||||||
self._templates[role].append(template)
|
|
||||||
|
|
||||||
def for_role_and_task(
|
|
||||||
self, group_role: GroupRole, group_task: Optional[GroupTask] = None
|
|
||||||
) -> Iterator[GroundObjectTemplate]:
|
|
||||||
if group_role not in self._templates:
|
|
||||||
return None
|
|
||||||
for template in self._templates[group_role]:
|
|
||||||
if not group_task or group_task in template.tasks:
|
|
||||||
yield template
|
|
||||||
|
|
||||||
def for_role_and_tasks(
|
|
||||||
self, group_role: GroupRole, group_tasks: list[GroupTask]
|
|
||||||
) -> Iterator[GroundObjectTemplate]:
|
|
||||||
unique_templates = []
|
|
||||||
for group_task in group_tasks:
|
|
||||||
for template in self.for_role_and_task(group_role, group_task):
|
|
||||||
if template not in unique_templates:
|
|
||||||
unique_templates.append(template)
|
|
||||||
yield from unique_templates
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
from PySide2.QtGui import QStandardItem, QStandardItemModel
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.theater import ControlPointType, BuildingGroundObject
|
from game.theater.controlpoint import ControlPointType
|
||||||
from game.theater.theatergroundobject import IadsGroundObject
|
from game.theater.theatergroundobject import IadsGroundObject, BuildingGroundObject
|
||||||
from game.utils import Distance
|
from game.utils import Distance
|
||||||
from game.missiongenerator.frontlineconflictdescription import (
|
from game.missiongenerator.frontlineconflictdescription import (
|
||||||
FrontLineConflictDescription,
|
FrontLineConflictDescription,
|
||||||
@ -131,7 +131,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
"["
|
"["
|
||||||
+ str(ground_object.obj_name)
|
+ str(ground_object.obj_name)
|
||||||
+ "] : "
|
+ "] : "
|
||||||
+ u.type
|
+ u.name
|
||||||
+ " #"
|
+ " #"
|
||||||
+ str(j)
|
+ str(j)
|
||||||
)
|
)
|
||||||
@ -140,9 +140,9 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
|
|||||||
wpt.obj_name = ground_object.obj_name
|
wpt.obj_name = ground_object.obj_name
|
||||||
wpt.waypoint_type = FlightWaypointType.CUSTOM
|
wpt.waypoint_type = FlightWaypointType.CUSTOM
|
||||||
if cp.captured:
|
if cp.captured:
|
||||||
wpt.description = "Friendly unit : " + u.type
|
wpt.description = "Friendly unit: " + u.name
|
||||||
else:
|
else:
|
||||||
wpt.description = "Enemy unit : " + u.type
|
wpt.description = "Enemy unit: " + u.name
|
||||||
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
i = add_model_item(i, model, wpt.pretty_name, wpt)
|
||||||
|
|
||||||
if self.include_airbases:
|
if self.include_airbases:
|
||||||
|
|||||||
@ -49,10 +49,10 @@ class QStrikeTargetInfoView(QGroupBox):
|
|||||||
if len(self.strike_target_infos.units) > 0:
|
if len(self.strike_target_infos.units) > 0:
|
||||||
dic = {}
|
dic = {}
|
||||||
for u in self.strike_target_infos.units:
|
for u in self.strike_target_infos.units:
|
||||||
if u.type in dic.keys():
|
if u.type.id in dic.keys():
|
||||||
dic[u.type] = dic[u.type] + 1
|
dic[u.type.id] = dic[u.type.id] + 1
|
||||||
else:
|
else:
|
||||||
dic[u.type] = 1
|
dic[u.type.id] = 1
|
||||||
for k, v in dic.items():
|
for k, v in dic.items():
|
||||||
model.appendRow(QStandardItem(k + " x " + str(v)))
|
model.appendRow(QStandardItem(k + " x " + str(v)))
|
||||||
print(k + " x " + str(v))
|
print(k + " x " + str(v))
|
||||||
|
|||||||
@ -18,7 +18,7 @@ from PySide2.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
import qt_ui.uiconstants as CONST
|
import qt_ui.uiconstants as CONST
|
||||||
from game import Game, VERSION, persistency
|
from game import Game, VERSION, persistency, db
|
||||||
from game.debriefing import Debriefing
|
from game.debriefing import Debriefing
|
||||||
from game.server import EventStream, GameContext
|
from game.server import EventStream, GameContext
|
||||||
from game.server.security import ApiKeyManager
|
from game.server.security import ApiKeyManager
|
||||||
@ -178,6 +178,9 @@ class QLiberationWindow(QMainWindow):
|
|||||||
self.openNotesAction.setIcon(CONST.ICONS["Notes"])
|
self.openNotesAction.setIcon(CONST.ICONS["Notes"])
|
||||||
self.openNotesAction.triggered.connect(self.showNotesDialog)
|
self.openNotesAction.triggered.connect(self.showNotesDialog)
|
||||||
|
|
||||||
|
self.importTemplatesAction = QAction("Import Layouts", self)
|
||||||
|
self.importTemplatesAction.triggered.connect(self.import_templates)
|
||||||
|
|
||||||
self.enable_game_actions(False)
|
self.enable_game_actions(False)
|
||||||
|
|
||||||
def enable_game_actions(self, enabled: bool):
|
def enable_game_actions(self, enabled: bool):
|
||||||
@ -220,6 +223,9 @@ class QLiberationWindow(QMainWindow):
|
|||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
file_menu.addAction("E&xit", self.close)
|
file_menu.addAction("E&xit", self.close)
|
||||||
|
|
||||||
|
tools_menu = self.menu.addMenu("&Developer tools")
|
||||||
|
tools_menu.addAction(self.importTemplatesAction)
|
||||||
|
|
||||||
help_menu = self.menu.addMenu("&Help")
|
help_menu = self.menu.addMenu("&Help")
|
||||||
help_menu.addAction(self.openDiscordAction)
|
help_menu.addAction(self.openDiscordAction)
|
||||||
help_menu.addAction(self.openGithubAction)
|
help_menu.addAction(self.openGithubAction)
|
||||||
@ -393,6 +399,9 @@ class QLiberationWindow(QMainWindow):
|
|||||||
self.dialog = QNotesWindow(self.game)
|
self.dialog = QNotesWindow(self.game)
|
||||||
self.dialog.show()
|
self.dialog.show()
|
||||||
|
|
||||||
|
def import_templates(self):
|
||||||
|
db.LAYOUTS.import_templates()
|
||||||
|
|
||||||
def showLogsDialog(self):
|
def showLogsDialog(self):
|
||||||
self.dialog = QLogsWindow()
|
self.dialog = QLogsWindow()
|
||||||
self.dialog.show()
|
self.dialog.show()
|
||||||
|
|||||||
@ -2,22 +2,22 @@ import os
|
|||||||
|
|
||||||
from PySide2.QtGui import QPixmap
|
from PySide2.QtGui import QPixmap
|
||||||
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout
|
from PySide2.QtWidgets import QGroupBox, QHBoxLayout, QLabel, QVBoxLayout
|
||||||
from game.theater import GroundUnit
|
from game.theater import TheaterUnit
|
||||||
|
|
||||||
from game.config import REWARDS
|
from game.config import REWARDS
|
||||||
|
|
||||||
|
|
||||||
class QBuildingInfo(QGroupBox):
|
class QBuildingInfo(QGroupBox):
|
||||||
def __init__(self, building, ground_object):
|
def __init__(self, building: TheaterUnit, ground_object):
|
||||||
super(QBuildingInfo, self).__init__()
|
super(QBuildingInfo, self).__init__()
|
||||||
self.building: GroundUnit = building
|
self.building = building
|
||||||
self.ground_object = ground_object
|
self.ground_object = ground_object
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
self.header = QLabel()
|
self.header = QLabel()
|
||||||
path = os.path.join(
|
path = os.path.join(
|
||||||
"./resources/ui/units/buildings/" + self.building.type + ".png"
|
"./resources/ui/units/buildings/" + self.building.icon + ".png"
|
||||||
)
|
)
|
||||||
if not self.building.alive:
|
if not self.building.alive:
|
||||||
pixmap = QPixmap("./resources/ui/units/buildings/dead.png")
|
pixmap = QPixmap("./resources/ui/units/buildings/dead.png")
|
||||||
@ -26,17 +26,13 @@ class QBuildingInfo(QGroupBox):
|
|||||||
else:
|
else:
|
||||||
pixmap = QPixmap("./resources/ui/units/buildings/missing.png")
|
pixmap = QPixmap("./resources/ui/units/buildings/missing.png")
|
||||||
self.header.setPixmap(pixmap)
|
self.header.setPixmap(pixmap)
|
||||||
name = "<b>{}</b> {}".format(
|
self.name = QLabel(self.building.short_name)
|
||||||
self.building.type[0:18],
|
|
||||||
"[DEAD]" if not self.building.alive else "",
|
|
||||||
)
|
|
||||||
self.name = QLabel(name)
|
|
||||||
self.name.setProperty("style", "small")
|
self.name.setProperty("style", "small")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.addWidget(self.header)
|
layout.addWidget(self.header)
|
||||||
layout.addWidget(self.name)
|
layout.addWidget(self.name)
|
||||||
|
|
||||||
if self.ground_object.category in REWARDS.keys():
|
if self.ground_object.category in REWARDS:
|
||||||
income_label_text = (
|
income_label_text = (
|
||||||
"Value: " + str(REWARDS[self.ground_object.category]) + "M"
|
"Value: " + str(REWARDS[self.ground_object.category]) + "M"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from PySide2.QtCore import Signal
|
from PySide2.QtCore import Signal
|
||||||
from PySide2.QtGui import Qt
|
from PySide2.QtGui import Qt
|
||||||
@ -9,95 +10,116 @@ from PySide2.QtWidgets import (
|
|||||||
QGridLayout,
|
QGridLayout,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
QLabel,
|
QLabel,
|
||||||
QMessageBox,
|
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QSpinBox,
|
QSpinBox,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
)
|
)
|
||||||
|
from dcs.unittype import UnitType
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.data.groups import GroupRole, ROLE_TASKINGS, GroupTask
|
from game.armedforces.forcegroup import ForceGroup
|
||||||
|
from game.data.groups import GroupRole, GroupTask
|
||||||
from game.point_with_heading import PointWithHeading
|
from game.point_with_heading import PointWithHeading
|
||||||
from game.theater import TheaterGroundObject
|
from game.theater import TheaterGroundObject
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
VehicleGroupGroundObject,
|
VehicleGroupGroundObject,
|
||||||
SamGroundObject,
|
SamGroundObject,
|
||||||
EwrGroundObject,
|
EwrGroundObject,
|
||||||
GroundGroup,
|
|
||||||
)
|
)
|
||||||
from gen.templates import (
|
from game.theater.theatergroup import TheaterGroup
|
||||||
GroundObjectTemplate,
|
from game.layout.layout import (
|
||||||
GroupTemplate,
|
TheaterLayout,
|
||||||
|
GroupLayout,
|
||||||
)
|
)
|
||||||
from qt_ui.uiconstants import EVENT_ICONS
|
from qt_ui.uiconstants import EVENT_ICONS
|
||||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QGroupLayout:
|
||||||
|
layout: GroupLayout
|
||||||
|
dcs_unit_type: Type[UnitType]
|
||||||
|
amount: int
|
||||||
|
unit_price: int
|
||||||
|
enabled: bool = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price(self) -> int:
|
||||||
|
return self.amount * self.unit_price if self.enabled else 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class QLayout:
|
||||||
|
layout: TheaterLayout
|
||||||
|
force_group: ForceGroup
|
||||||
|
group_layouts: list[QGroupLayout] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def price(self) -> int:
|
||||||
|
return sum(group.price for group in self.group_layouts)
|
||||||
|
|
||||||
|
|
||||||
class QGroundObjectGroupTemplate(QGroupBox):
|
class QGroundObjectGroupTemplate(QGroupBox):
|
||||||
group_template_changed = Signal(GroupTemplate)
|
group_template_changed = Signal()
|
||||||
# UI to show one GroupTemplate and configure the TemplateRandomizer for it
|
|
||||||
# one row: [Required | Unit Selector | Amount | Price]
|
|
||||||
# If the group is not randomizable: Just view labels instead of edit fields
|
|
||||||
|
|
||||||
def __init__(self, group_id: int, group_template: GroupTemplate) -> None:
|
def __init__(
|
||||||
super(QGroundObjectGroupTemplate, self).__init__(
|
self, group_id: int, force_group: ForceGroup, group_layout: GroupLayout
|
||||||
f"{group_id + 1}: {group_template.name}"
|
) -> None:
|
||||||
)
|
super().__init__(f"{group_id + 1}: {group_layout.name}")
|
||||||
self.group_template = group_template
|
self.grid_layout = QGridLayout()
|
||||||
|
self.setLayout(self.grid_layout)
|
||||||
self.group_layout = QGridLayout()
|
|
||||||
self.setLayout(self.group_layout)
|
|
||||||
|
|
||||||
self.amount_selector = QSpinBox()
|
self.amount_selector = QSpinBox()
|
||||||
self.unit_selector = QComboBox()
|
self.unit_selector = QComboBox()
|
||||||
self.group_selector = QCheckBox()
|
self.group_selector = QCheckBox()
|
||||||
|
|
||||||
self.group_selector.setChecked(self.group_template.should_be_generated)
|
# Add all possible units with the price
|
||||||
self.group_selector.setEnabled(self.group_template.optional)
|
for unit_type in force_group.unit_types_for_group(group_layout):
|
||||||
|
self.unit_selector.addItem(
|
||||||
if self.group_template.can_be_modified:
|
f"{unit_type.name} [${unit_type.price}M]",
|
||||||
# Group can be modified (more than 1 possible unit_type for the group)
|
userData=(unit_type.dcs_unit_type, unit_type.price),
|
||||||
for unit in self.group_template.possible_units:
|
|
||||||
self.unit_selector.addItem(f"{unit} [${unit.price}M]", userData=unit)
|
|
||||||
self.group_layout.addWidget(
|
|
||||||
self.unit_selector, 0, 0, alignment=Qt.AlignRight
|
|
||||||
)
|
)
|
||||||
self.group_layout.addWidget(
|
# Add all possible statics with price = 0
|
||||||
self.amount_selector, 0, 1, alignment=Qt.AlignRight
|
for static_type in force_group.statics_for_group(group_layout):
|
||||||
|
self.unit_selector.addItem(
|
||||||
|
f"{static_type} (Static)", userData=(static_type, 0)
|
||||||
)
|
)
|
||||||
|
self.unit_selector.setEnabled(self.unit_selector.count() > 1)
|
||||||
|
self.grid_layout.addWidget(self.unit_selector, 0, 0, alignment=Qt.AlignRight)
|
||||||
|
self.grid_layout.addWidget(self.amount_selector, 0, 1, alignment=Qt.AlignRight)
|
||||||
|
|
||||||
self.amount_selector.setMinimum(1)
|
unit_type, price = self.unit_selector.itemData(
|
||||||
self.amount_selector.setMaximum(self.group_template.max_size)
|
self.unit_selector.currentIndex()
|
||||||
self.amount_selector.setValue(self.group_template.size)
|
)
|
||||||
|
|
||||||
self.on_group_changed()
|
self.group_layout = QGroupLayout(
|
||||||
else:
|
group_layout, unit_type, group_layout.unit_counter, price
|
||||||
# Group can not be randomized so just show the group info
|
)
|
||||||
group_info = QVBoxLayout()
|
|
||||||
try:
|
|
||||||
unit_name = next(self.group_template.possible_units)
|
|
||||||
except StopIteration:
|
|
||||||
unit_name = self.group_template.unit_type
|
|
||||||
group_info.addWidget(
|
|
||||||
QLabel(f"{self.group_template.size}x {unit_name}"),
|
|
||||||
alignment=Qt.AlignLeft,
|
|
||||||
)
|
|
||||||
self.group_layout.addLayout(group_info, 0, 0, 1, 2)
|
|
||||||
|
|
||||||
self.group_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
|
self.group_selector.setChecked(self.group_layout.enabled)
|
||||||
|
self.group_selector.setEnabled(self.group_layout.layout.optional)
|
||||||
|
|
||||||
|
self.amount_selector.setMinimum(1)
|
||||||
|
self.amount_selector.setMaximum(self.group_layout.layout.max_size)
|
||||||
|
self.amount_selector.setValue(self.group_layout.amount)
|
||||||
|
self.amount_selector.setEnabled(self.group_layout.layout.max_size > 1)
|
||||||
|
|
||||||
|
self.grid_layout.addWidget(self.group_selector, 0, 2, alignment=Qt.AlignRight)
|
||||||
|
|
||||||
self.amount_selector.valueChanged.connect(self.on_group_changed)
|
self.amount_selector.valueChanged.connect(self.on_group_changed)
|
||||||
self.unit_selector.currentIndexChanged.connect(self.on_group_changed)
|
self.unit_selector.currentIndexChanged.connect(self.on_group_changed)
|
||||||
self.group_selector.stateChanged.connect(self.on_group_changed)
|
self.group_selector.stateChanged.connect(self.on_group_changed)
|
||||||
|
|
||||||
def on_group_changed(self) -> None:
|
def on_group_changed(self) -> None:
|
||||||
self.group_template.set_enabled(self.group_selector.isChecked())
|
self.group_layout.enabled = self.group_selector.isChecked()
|
||||||
if self.group_template.can_be_modified:
|
unit_type, price = self.unit_selector.itemData(
|
||||||
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
|
self.unit_selector.currentIndex()
|
||||||
self.group_template.unit_count = [self.amount_selector.value()]
|
)
|
||||||
self.group_template.set_unit_type(unit_type.dcs_id)
|
self.group_layout.dcs_unit_type = unit_type
|
||||||
self.group_template_changed.emit(self.group_template)
|
self.group_layout.unit_price = price
|
||||||
|
self.group_layout.amount = self.amount_selector.value()
|
||||||
|
self.group_template_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
class QGroundObjectTemplateLayout(QGroupBox):
|
class QGroundObjectTemplateLayout(QGroupBox):
|
||||||
@ -105,20 +127,22 @@ class QGroundObjectTemplateLayout(QGroupBox):
|
|||||||
self,
|
self,
|
||||||
game: Game,
|
game: Game,
|
||||||
ground_object: TheaterGroundObject,
|
ground_object: TheaterGroundObject,
|
||||||
template_changed_signal: Signal(GroundObjectTemplate),
|
layout: QLayout,
|
||||||
|
layout_changed_signal: Signal(QLayout),
|
||||||
current_group_value: int,
|
current_group_value: int,
|
||||||
):
|
):
|
||||||
super(QGroundObjectTemplateLayout, self).__init__("Groups:")
|
super().__init__("Groups:")
|
||||||
# Connect to the signal to handle template updates
|
# Connect to the signal to handle template updates
|
||||||
self.game = game
|
self.game = game
|
||||||
self.ground_object = ground_object
|
self.ground_object = ground_object
|
||||||
self.template_changed_signal = template_changed_signal
|
self.layout_changed_signal = layout_changed_signal
|
||||||
self.template_changed_signal.connect(self.load_for_template)
|
self.layout_model = layout
|
||||||
self.template: Optional[GroundObjectTemplate] = None
|
self.layout_changed_signal.connect(self.load_for_layout)
|
||||||
|
|
||||||
self.current_group_value = current_group_value
|
self.current_group_value = current_group_value
|
||||||
|
|
||||||
self.buy_button = QPushButton("Buy")
|
self.buy_button = QPushButton("Buy")
|
||||||
|
self.buy_button.setEnabled(False)
|
||||||
self.buy_button.clicked.connect(self.buy_group)
|
self.buy_button.clicked.connect(self.buy_group)
|
||||||
|
|
||||||
self.template_layout = QGridLayout()
|
self.template_layout = QGridLayout()
|
||||||
@ -131,78 +155,73 @@ class QGroundObjectTemplateLayout(QGroupBox):
|
|||||||
stretch.addStretch()
|
stretch.addStretch()
|
||||||
self.template_layout.addLayout(stretch, 2, 0)
|
self.template_layout.addLayout(stretch, 2, 0)
|
||||||
|
|
||||||
def load_for_template(self, template: GroundObjectTemplate) -> None:
|
# Load Layout
|
||||||
self.template = template
|
self.load_for_layout(self.layout_model)
|
||||||
|
|
||||||
|
def load_for_layout(self, layout: QLayout) -> None:
|
||||||
|
self.layout_model = layout
|
||||||
# Clean the current grid
|
# Clean the current grid
|
||||||
for id in range(self.template_grid.count()):
|
for id in range(self.template_grid.count()):
|
||||||
self.template_grid.itemAt(id).widget().deleteLater()
|
self.template_grid.itemAt(id).widget().deleteLater()
|
||||||
|
for g_id, layout_group in enumerate(self.layout_model.layout.groups):
|
||||||
for g_id, group in enumerate(template.groups):
|
group_row = QGroundObjectGroupTemplate(
|
||||||
group_row = QGroundObjectGroupTemplate(g_id, group)
|
g_id, self.layout_model.force_group, layout_group
|
||||||
|
)
|
||||||
|
self.layout_model.group_layouts.append(group_row.group_layout)
|
||||||
group_row.group_template_changed.connect(self.group_template_changed)
|
group_row.group_template_changed.connect(self.group_template_changed)
|
||||||
self.template_grid.addWidget(group_row)
|
self.template_grid.addWidget(group_row)
|
||||||
|
|
||||||
self.update_price()
|
self.group_template_changed()
|
||||||
|
|
||||||
def group_template_changed(self, group_template: GroupTemplate) -> None:
|
def group_template_changed(self) -> None:
|
||||||
self.update_price()
|
price = self.layout_model.price
|
||||||
|
self.buy_button.setText(f"Buy [${price}M][-${self.current_group_value}M]")
|
||||||
def update_price(self) -> None:
|
self.buy_button.setEnabled(price <= self.game.blue.budget)
|
||||||
price = "$" + str(self.template.estimated_price_for(self.ground_object))
|
if self.buy_button.isEnabled():
|
||||||
self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]")
|
self.buy_button.setToolTip("Buy the group")
|
||||||
|
else:
|
||||||
|
self.buy_button.setToolTip("Not enough money to buy this group")
|
||||||
|
|
||||||
def buy_group(self):
|
def buy_group(self):
|
||||||
if not self.template:
|
if not self.layout:
|
||||||
return
|
raise RuntimeError("No template selected. GroundObject can not be bought.")
|
||||||
groups = self.generate_groups()
|
|
||||||
|
|
||||||
price = 0
|
|
||||||
for group in groups:
|
|
||||||
for unit in group.units:
|
|
||||||
if unit.unit_type:
|
|
||||||
price += unit.unit_type.price
|
|
||||||
|
|
||||||
price -= self.current_group_value
|
|
||||||
|
|
||||||
|
price = self.layout_model.price
|
||||||
if price > self.game.blue.budget:
|
if price > self.game.blue.budget:
|
||||||
self.error_money()
|
# Somethin went wrong. Buy button should be disabled!
|
||||||
self.close()
|
logging.error("Not enough money to buy the group")
|
||||||
return
|
return
|
||||||
else:
|
self.game.blue.budget -= price - self.current_group_value
|
||||||
self.game.blue.budget -= price
|
self.ground_object.groups = self.generate_groups()
|
||||||
|
|
||||||
self.ground_object.groups = groups
|
|
||||||
|
|
||||||
# Replan redfor missions
|
# Replan redfor missions
|
||||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||||
|
|
||||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||||
|
|
||||||
def error_money(self):
|
def generate_groups(self) -> list[TheaterGroup]:
|
||||||
msg = QMessageBox()
|
go = self.layout_model.layout.create_ground_object(
|
||||||
msg.setIcon(QMessageBox.Information)
|
|
||||||
msg.setText("Not enough money to buy these units !")
|
|
||||||
msg.setWindowTitle("Not enough money")
|
|
||||||
msg.setStandardButtons(QMessageBox.Ok)
|
|
||||||
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
||||||
msg.exec_()
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def generate_groups(self) -> list[GroundGroup]:
|
|
||||||
go = self.template.generate(
|
|
||||||
self.ground_object.name,
|
self.ground_object.name,
|
||||||
PointWithHeading.from_point(
|
PointWithHeading.from_point(
|
||||||
self.ground_object.position, self.ground_object.heading
|
self.ground_object.position, self.ground_object.heading
|
||||||
),
|
),
|
||||||
self.ground_object.control_point,
|
self.ground_object.control_point,
|
||||||
self.game,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for group in self.layout_model.group_layouts:
|
||||||
|
self.layout_model.force_group.create_theater_group_for_tgo(
|
||||||
|
go,
|
||||||
|
group.layout,
|
||||||
|
self.ground_object.name,
|
||||||
|
self.game,
|
||||||
|
group.dcs_unit_type, # Forced Type
|
||||||
|
group.amount, # Forced Amount
|
||||||
|
)
|
||||||
|
|
||||||
return go.groups
|
return go.groups
|
||||||
|
|
||||||
|
|
||||||
class QGroundObjectBuyMenu(QDialog):
|
class QGroundObjectBuyMenu(QDialog):
|
||||||
template_changed_signal = Signal(GroundObjectTemplate)
|
layout_changed_signal = Signal(QLayout)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -211,7 +230,7 @@ class QGroundObjectBuyMenu(QDialog):
|
|||||||
game: Game,
|
game: Game,
|
||||||
current_group_value: int,
|
current_group_value: int,
|
||||||
):
|
):
|
||||||
super(QGroundObjectBuyMenu, self).__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.setMinimumWidth(350)
|
self.setMinimumWidth(350)
|
||||||
|
|
||||||
@ -221,66 +240,90 @@ class QGroundObjectBuyMenu(QDialog):
|
|||||||
self.mainLayout = QGridLayout()
|
self.mainLayout = QGridLayout()
|
||||||
self.setLayout(self.mainLayout)
|
self.setLayout(self.mainLayout)
|
||||||
|
|
||||||
self.unit_group_selector = QComboBox()
|
self.force_group_selector = QComboBox()
|
||||||
self.template_selector = QComboBox()
|
self.layout_selector = QComboBox()
|
||||||
self.template_selector.setEnabled(False)
|
self.layout_selector.setEnabled(False)
|
||||||
|
|
||||||
# Get the templates and fill the combobox
|
# Get the layouts and fill the combobox
|
||||||
template_sub_category = None
|
|
||||||
tasks = []
|
tasks = []
|
||||||
if isinstance(ground_object, SamGroundObject):
|
if isinstance(ground_object, SamGroundObject):
|
||||||
role = GroupRole.AntiAir
|
role = GroupRole.AIR_DEFENSE
|
||||||
elif isinstance(ground_object, VehicleGroupGroundObject):
|
elif isinstance(ground_object, VehicleGroupGroundObject):
|
||||||
role = GroupRole.GroundForce
|
role = GroupRole.GROUND_FORCE
|
||||||
elif isinstance(ground_object, EwrGroundObject):
|
elif isinstance(ground_object, EwrGroundObject):
|
||||||
role = GroupRole.AntiAir
|
role = GroupRole.AIR_DEFENSE
|
||||||
tasks.append(GroupTask.EWR)
|
tasks.append(GroupTask.EARLY_WARNING_RADAR)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError
|
raise NotImplementedError(f"Unhandled TGO type {ground_object.__class__}")
|
||||||
|
|
||||||
if not tasks:
|
if not tasks:
|
||||||
tasks = ROLE_TASKINGS[role]
|
tasks = role.tasks
|
||||||
|
|
||||||
for unit_group in game.blue.faction.groups_for_role_and_tasks(role, tasks):
|
for group in game.blue.armed_forces.groups_for_tasks(tasks):
|
||||||
self.unit_group_selector.addItem(unit_group.name, userData=unit_group)
|
self.force_group_selector.addItem(group.name, userData=group)
|
||||||
|
self.force_group_selector.setEnabled(self.force_group_selector.count() > 1)
|
||||||
|
|
||||||
self.template_selector.currentIndexChanged.connect(self.template_changed)
|
force_group = self.force_group_selector.itemData(
|
||||||
self.unit_group_selector.currentIndexChanged.connect(self.unit_group_changed)
|
self.force_group_selector.currentIndex()
|
||||||
|
)
|
||||||
|
|
||||||
|
for layout in force_group.layouts:
|
||||||
|
self.layout_selector.addItem(layout.name, userData=layout)
|
||||||
|
|
||||||
|
selected_template = self.layout_selector.itemData(
|
||||||
|
self.layout_selector.currentIndex()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.layout_model = QLayout(selected_template, force_group)
|
||||||
|
|
||||||
|
self.layout_selector.currentIndexChanged.connect(self.layout_changed)
|
||||||
|
self.force_group_selector.currentIndexChanged.connect(self.force_group_changed)
|
||||||
|
|
||||||
template_selector_layout = QGridLayout()
|
template_selector_layout = QGridLayout()
|
||||||
template_selector_layout.addWidget(QLabel("UnitGroup :"), 0, 0, Qt.AlignLeft)
|
|
||||||
template_selector_layout.addWidget(
|
template_selector_layout.addWidget(
|
||||||
self.unit_group_selector, 0, 1, alignment=Qt.AlignRight
|
QLabel("Armed Forces Group:"), 0, 0, Qt.AlignLeft
|
||||||
)
|
)
|
||||||
template_selector_layout.addWidget(QLabel("Template :"), 1, 0, Qt.AlignLeft)
|
|
||||||
template_selector_layout.addWidget(
|
template_selector_layout.addWidget(
|
||||||
self.template_selector, 1, 1, alignment=Qt.AlignRight
|
self.force_group_selector, 0, 1, alignment=Qt.AlignRight
|
||||||
|
)
|
||||||
|
template_selector_layout.addWidget(QLabel("Layout:"), 1, 0, Qt.AlignLeft)
|
||||||
|
template_selector_layout.addWidget(
|
||||||
|
self.layout_selector, 1, 1, alignment=Qt.AlignRight
|
||||||
)
|
)
|
||||||
self.mainLayout.addLayout(template_selector_layout, 0, 0)
|
self.mainLayout.addLayout(template_selector_layout, 0, 0)
|
||||||
|
|
||||||
self.template_layout = QGroundObjectTemplateLayout(
|
self.template_layout = QGroundObjectTemplateLayout(
|
||||||
game, ground_object, self.template_changed_signal, current_group_value
|
game,
|
||||||
|
ground_object,
|
||||||
|
self.layout_model,
|
||||||
|
self.layout_changed_signal,
|
||||||
|
current_group_value,
|
||||||
)
|
)
|
||||||
self.mainLayout.addWidget(self.template_layout, 1, 0)
|
self.mainLayout.addWidget(self.template_layout, 1, 0)
|
||||||
self.setLayout(self.mainLayout)
|
self.setLayout(self.mainLayout)
|
||||||
|
|
||||||
# Update UI
|
def force_group_changed(self) -> None:
|
||||||
self.unit_group_changed()
|
# Prevent ComboBox from firing change Events
|
||||||
|
self.layout_selector.blockSignals(True)
|
||||||
def unit_group_changed(self) -> None:
|
unit_group = self.force_group_selector.itemData(
|
||||||
unit_group = self.unit_group_selector.itemData(
|
self.force_group_selector.currentIndex()
|
||||||
self.unit_group_selector.currentIndex()
|
|
||||||
)
|
)
|
||||||
self.template_selector.clear()
|
self.layout_selector.clear()
|
||||||
if unit_group.templates:
|
for layout in unit_group.layouts:
|
||||||
for template in unit_group.templates:
|
self.layout_selector.addItem(layout.name, userData=layout)
|
||||||
self.template_selector.addItem(template.name, userData=template)
|
|
||||||
# Enable if more than one template is available
|
# Enable if more than one template is available
|
||||||
self.template_selector.setEnabled(len(unit_group.templates) > 1)
|
self.layout_selector.setEnabled(len(unit_group.layouts) > 1)
|
||||||
|
# Enable Combobox Signals again
|
||||||
|
self.layout_selector.blockSignals(False)
|
||||||
|
self.layout_changed()
|
||||||
|
|
||||||
def template_changed(self):
|
def layout_changed(self):
|
||||||
template = self.template_selector.itemData(
|
self.layout()
|
||||||
self.template_selector.currentIndex()
|
self.layout_model.layout = self.layout_selector.itemData(
|
||||||
|
self.layout_selector.currentIndex()
|
||||||
)
|
)
|
||||||
if template is not None:
|
self.layout_model.force_group = self.force_group_selector.itemData(
|
||||||
self.template_changed_signal.emit(template)
|
self.force_group_selector.currentIndex()
|
||||||
|
)
|
||||||
|
self.layout_model.group_layouts = []
|
||||||
|
self.layout_changed_signal.emit(self.layout_model)
|
||||||
|
|||||||
@ -9,13 +9,11 @@ from PySide2.QtWidgets import (
|
|||||||
QPushButton,
|
QPushButton,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
)
|
)
|
||||||
from dcs import Point, vehicles
|
from dcs import Point
|
||||||
|
|
||||||
from game import Game
|
from game import Game
|
||||||
from game.config import REWARDS
|
from game.config import REWARDS
|
||||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||||
from game.data.units import UnitClass
|
|
||||||
from game.dcs.groundunittype import GroundUnitType
|
|
||||||
from game.theater import ControlPoint, TheaterGroundObject
|
from game.theater import ControlPoint, TheaterGroundObject
|
||||||
from game.theater.theatergroundobject import (
|
from game.theater.theatergroundobject import (
|
||||||
BuildingGroundObject,
|
BuildingGroundObject,
|
||||||
@ -101,7 +99,7 @@ class QGroundObjectMenu(QDialog):
|
|||||||
QLabel(f"<b>Unit {str(unit.display_name)}</b>"), i, 0
|
QLabel(f"<b>Unit {str(unit.display_name)}</b>"), i, 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if not unit.alive and self.cp.captured:
|
if not unit.alive and unit.repairable and self.cp.captured:
|
||||||
price = unit.unit_type.price if unit.unit_type else 0
|
price = unit.unit_type.price if unit.unit_type else 0
|
||||||
repair = QPushButton(f"Repair [{price}M]")
|
repair = QPushButton(f"Repair [{price}M]")
|
||||||
repair.setProperty("style", "btn-success")
|
repair.setProperty("style", "btn-success")
|
||||||
@ -176,19 +174,13 @@ class QGroundObjectMenu(QDialog):
|
|||||||
self.update_total_value()
|
self.update_total_value()
|
||||||
|
|
||||||
def update_total_value(self):
|
def update_total_value(self):
|
||||||
total_value = 0
|
|
||||||
if not self.ground_object.purchasable:
|
if not self.ground_object.purchasable:
|
||||||
return
|
return
|
||||||
for u in self.ground_object.units:
|
self.total_value = sum(
|
||||||
# Hack: Unknown variant.
|
u.unit_type.price for u in self.ground_object.units if u.unit_type
|
||||||
if u.type in vehicles.vehicle_map:
|
)
|
||||||
unit_type = next(
|
|
||||||
GroundUnitType.for_dcs_type(vehicles.vehicle_map[u.type])
|
|
||||||
)
|
|
||||||
total_value += unit_type.price
|
|
||||||
if self.sell_all_button is not None:
|
if self.sell_all_button is not None:
|
||||||
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
|
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
|
||||||
self.total_value = total_value
|
|
||||||
|
|
||||||
def repair_unit(self, unit, price):
|
def repair_unit(self, unit, price):
|
||||||
if self.game.blue.budget > price:
|
if self.game.blue.budget > price:
|
||||||
@ -203,7 +195,7 @@ class QGroundObjectMenu(QDialog):
|
|||||||
if p.distance_to_point(unit.position) < 15:
|
if p.distance_to_point(unit.position) < 15:
|
||||||
destroyed_units.remove(d)
|
destroyed_units.remove(d)
|
||||||
logging.info("Removed destroyed units " + str(d))
|
logging.info("Removed destroyed units " + str(d))
|
||||||
logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type))
|
logging.info(f"Repaired unit: {unit.unit_name}")
|
||||||
|
|
||||||
self.do_refresh_layout()
|
self.do_refresh_layout()
|
||||||
|
|
||||||
|
|||||||
@ -20,4 +20,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- Logistics
|
- Logistics
|
||||||
template_file: resources/templates/anti_air/AAA.miz
|
layout_file: resources/layouts/anti_air/AAA.miz
|
||||||
@ -26,4 +26,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- Logistics
|
- Logistics
|
||||||
template_file: resources/templates/anti_air/AAA.miz
|
layout_file: resources/layouts/anti_air/AAA.miz
|
||||||
@ -20,4 +20,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- Logistics
|
- Logistics
|
||||||
template_file: resources/templates/anti_air/AAA.miz
|
layout_file: resources/layouts/anti_air/AAA.miz
|
||||||
@ -33,4 +33,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- Logistics
|
- Logistics
|
||||||
template_file: resources/templates/anti_air/flak.miz
|
layout_file: resources/layouts/anti_air/flak.miz
|
||||||
@ -12,4 +12,4 @@ groups:
|
|||||||
alternative_classes:
|
alternative_classes:
|
||||||
- SearchRadar
|
- SearchRadar
|
||||||
- SearchTrackRadar
|
- SearchTrackRadar
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -48,4 +48,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- Blitz_36-6700A
|
- Blitz_36-6700A
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -48,4 +48,4 @@ groups:
|
|||||||
- 3
|
- 3
|
||||||
unit_types:
|
unit_types:
|
||||||
- soldier_mauser98
|
- soldier_mauser98
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -21,4 +21,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_types:
|
unit_types:
|
||||||
- Ural-375 ZU-23
|
- Ural-375 ZU-23
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -33,4 +33,4 @@ groups:
|
|||||||
- 1
|
- 1
|
||||||
unit_types:
|
unit_types:
|
||||||
- Vulcan
|
- Vulcan
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -18,4 +18,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- NASAMS_LN_B
|
- NASAMS_LN_B
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -18,4 +18,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- NASAMS_LN_C
|
- NASAMS_LN_C
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -53,4 +53,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- SHORAD
|
- SHORAD
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -18,4 +18,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_types:
|
unit_types:
|
||||||
- rapier_fsa_launcher
|
- rapier_fsa_launcher
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -18,4 +18,4 @@ groups:
|
|||||||
- 1
|
- 1
|
||||||
unit_types:
|
unit_types:
|
||||||
- M 818
|
- M 818
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -18,4 +18,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- SA-11 Buk LN 9A310M1
|
- SA-11 Buk LN 9A310M1
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -18,4 +18,4 @@ groups:
|
|||||||
- 3
|
- 3
|
||||||
unit_types:
|
unit_types:
|
||||||
- SA-17 Buk M1-2 LN 9A310M1-2
|
- SA-17 Buk M1-2 LN 9A310M1-2
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -18,4 +18,4 @@ groups:
|
|||||||
- 6
|
- 6
|
||||||
unit_types:
|
unit_types:
|
||||||
- S_75M_Volhov
|
- S_75M_Volhov
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -18,4 +18,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- 5p73 s-125 ln
|
- 5p73 s-125 ln
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -23,4 +23,4 @@ groups:
|
|||||||
- 6
|
- 6
|
||||||
unit_types:
|
unit_types:
|
||||||
- S-200_Launcher
|
- S-200_Launcher
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -13,4 +13,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- Kub 2P25 ln
|
- Kub 2P25 ln
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -16,4 +16,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- Logistics
|
- Logistics
|
||||||
template_file: resources/templates/anti_air/shorad.miz
|
layout_file: resources/layouts/anti_air/shorad.miz
|
||||||
@ -38,4 +38,4 @@ groups:
|
|||||||
- 1
|
- 1
|
||||||
unit_types:
|
unit_types:
|
||||||
- Bedford_MWD
|
- Bedford_MWD
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -13,4 +13,4 @@ groups:
|
|||||||
- 1
|
- 1
|
||||||
unit_types:
|
unit_types:
|
||||||
- Blitz_36-6700A
|
- Blitz_36-6700A
|
||||||
template_file: resources/templates/anti_air/legacy_ground_templates.miz
|
layout_file: resources/layouts/anti_air/legacy_ground_templates.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: allycamp1
|
name: allycamp1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -80,4 +81,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- house2arm
|
- house2arm
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: ammo1
|
name: ammo1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- Ammo
|
- Ammo
|
||||||
@ -18,4 +19,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_types:
|
unit_types:
|
||||||
- Hangar B
|
- Hangar B
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: comms
|
name: comms
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -12,4 +13,4 @@ groups:
|
|||||||
unit_types:
|
unit_types:
|
||||||
- TV tower
|
- TV tower
|
||||||
- Comms tower M
|
- Comms tower M
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: derrick1
|
name: derrick1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -27,4 +28,4 @@ groups:
|
|||||||
- 1
|
- 1
|
||||||
unit_types:
|
unit_types:
|
||||||
- Subsidiary structure 2
|
- Subsidiary structure 2
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: factory1
|
name: factory1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- Factory
|
- Factory
|
||||||
@ -19,4 +20,4 @@ groups:
|
|||||||
- 3
|
- 3
|
||||||
unit_types:
|
unit_types:
|
||||||
- Tech hangar A
|
- Tech hangar A
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: farp1
|
name: farp1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -37,4 +38,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_types:
|
unit_types:
|
||||||
- FARP Fuel Depot
|
- FARP Fuel Depot
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: fob1
|
name: fob1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- FOB
|
- FOB
|
||||||
@ -27,4 +28,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_types:
|
unit_types:
|
||||||
- Garage small B
|
- Garage small B
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: fuel1
|
name: fuel1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -24,4 +25,4 @@ groups:
|
|||||||
- 2
|
- 2
|
||||||
unit_types:
|
unit_types:
|
||||||
- Tank 3
|
- Tank 3
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: oil1
|
name: oil1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- OffShoreStrikeTarget
|
- OffShoreStrikeTarget
|
||||||
@ -14,4 +15,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- Oil platform
|
- Oil platform
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: power1
|
name: power1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -33,4 +34,4 @@ groups:
|
|||||||
- 1
|
- 1
|
||||||
unit_types:
|
unit_types:
|
||||||
- Farm B
|
- Farm B
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: village1
|
name: village1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -35,4 +36,4 @@ groups:
|
|||||||
- 3
|
- 3
|
||||||
unit_types:
|
unit_types:
|
||||||
- Small house 1B
|
- Small house 1B
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: ware1
|
name: ware1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -20,4 +21,4 @@ groups:
|
|||||||
- 3
|
- 3
|
||||||
unit_types:
|
unit_types:
|
||||||
- Hangar A
|
- Hangar A
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: ww2bunker1
|
name: ww2bunker1
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -30,4 +31,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_types:
|
unit_types:
|
||||||
- SK_C_28_naval_gun
|
- SK_C_28_naval_gun
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -1,4 +1,5 @@
|
|||||||
name: ww2bunker2
|
name: ww2bunker2
|
||||||
|
generic: true
|
||||||
role: Building
|
role: Building
|
||||||
tasks:
|
tasks:
|
||||||
- StrikeTarget
|
- StrikeTarget
|
||||||
@ -53,4 +54,4 @@ groups:
|
|||||||
- 7
|
- 7
|
||||||
unit_types:
|
unit_types:
|
||||||
- Czech hedgehogs 1
|
- Czech hedgehogs 1
|
||||||
template_file: resources/templates/buildings/buildings.miz
|
layout_file: resources/layouts/buildings/buildings.miz
|
||||||
@ -31,4 +31,4 @@ groups:
|
|||||||
- 1
|
- 1
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- SHORAD
|
- SHORAD
|
||||||
template_file: resources/templates/defenses/defenses.miz
|
layout_file: resources/layouts/defenses/defenses.miz
|
||||||
@ -26,4 +26,4 @@ groups:
|
|||||||
- 1
|
- 1
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- SHORAD
|
- SHORAD
|
||||||
template_file: resources/templates/defenses/defenses.miz
|
layout_file: resources/layouts/defenses/defenses.miz
|
||||||
@ -14,4 +14,4 @@ groups:
|
|||||||
- ATGM
|
- ATGM
|
||||||
- IFV
|
- IFV
|
||||||
- Tank
|
- Tank
|
||||||
template_file: resources/templates/ground_forces/ground_forces.miz
|
layout_file: resources/layouts/ground_forces/ground_forces.miz
|
||||||
@ -23,4 +23,4 @@ groups:
|
|||||||
- AAA
|
- AAA
|
||||||
- SHORAD
|
- SHORAD
|
||||||
- Manpad
|
- Manpad
|
||||||
template_file: resources/templates/ground_forces/ground_forces.miz
|
layout_file: resources/layouts/ground_forces/ground_forces.miz
|
||||||
@ -16,4 +16,4 @@ groups:
|
|||||||
- 4
|
- 4
|
||||||
unit_classes:
|
unit_classes:
|
||||||
- Destroyer
|
- Destroyer
|
||||||
template_file: resources/templates/naval/legacy_naval_templates.miz
|
layout_file: resources/layouts/naval/legacy_naval_templates.miz
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user