mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Implement Template System for ground objects
- Factored out the current generators to use a better approach with Templates build from the dcs mission editor. - This information is extended with a template-mapping in a json file which allows to logically group together multiple dcs groups and even statics to one template - The combination of mapping and miz will be serialized to a template.json which is only used for loading. - Factions now load templates during initialization and hold all the templates they can really use. This is based around the faction file. - Implemented a template randomizer which allows to add some randomization to templates - Each Template Group can have 1 randomizer which randomizes unit_type and size based on the mapping definition. Larger groups need to be devided in more fine detailed groups as we can now handle them better due to the change from dcs group types to our own classes. - Rewritten the ArmorGroup, Naval and EWR template handling Rework GroundObjectBuyMenu to support templates
This commit is contained in:
parent
d154069877
commit
5febcdd4e4
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
@ -24,6 +25,7 @@ from game.data.doctrine import (
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from gen.templates import GroundObjectTemplates, TemplateCategory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.start_generator import ModSettings
|
||||
@ -72,7 +74,7 @@ class Faction:
|
||||
air_defenses: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible EWR generators for this faction.
|
||||
ewrs: List[str] = field(default_factory=list)
|
||||
ewrs: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Possible Missile site generators for this faction
|
||||
missiles: List[str] = field(default_factory=list)
|
||||
@ -137,12 +139,68 @@ class Faction:
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool:
|
||||
# All possible templates which can be generated by the faction
|
||||
templates: GroundObjectTemplates = field(default=GroundObjectTemplates())
|
||||
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
return getattr(self, item)
|
||||
|
||||
def has_access_to_unit_type(self, unit_type: str) -> bool:
|
||||
# Supports all GroundUnit lists and AirDefenses
|
||||
for unit in self.ground_units:
|
||||
if unit_type == unit.dcs_id:
|
||||
return True
|
||||
return unit_type in self.air_defenses
|
||||
|
||||
def has_access_to_unit_class(self, unit_class: GroundUnitClass) -> bool:
|
||||
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
|
||||
if vehicle.unit_class is unit_class:
|
||||
return True
|
||||
return False
|
||||
|
||||
def load_templates(self, all_templates: GroundObjectTemplates) -> None:
|
||||
# This loads all faction possible sam templates and the default ones
|
||||
# For legacy reasons this allows to check for template names. This can be
|
||||
# improved in the future to have more control about the possible Templates.
|
||||
# For example it can be possible to define the unit_types and check if all
|
||||
# requirements for the template are fulfilled.
|
||||
for category, template in all_templates.templates:
|
||||
if (
|
||||
(
|
||||
category == TemplateCategory.AirDefence
|
||||
and (
|
||||
# Check if faction has the template name or ALL required
|
||||
# unit_types in the list air_defenses. For legacy reasons this
|
||||
# allows both and also the EWR template
|
||||
template.name in self.air_defenses
|
||||
or all(
|
||||
self.has_access_to_unit_type(required_unit)
|
||||
for required_unit in template.required_units
|
||||
)
|
||||
or template.template_type == "EWR"
|
||||
)
|
||||
)
|
||||
or template.name in self.navy_generators
|
||||
or template.name in self.missiles
|
||||
or template.name in self.coastal_defenses
|
||||
or (
|
||||
template.template_type
|
||||
in self.building_set + ["fob", "ammo", "factory"]
|
||||
)
|
||||
or (template.template_type == "carrier" and self.aircraft_carrier)
|
||||
or (template.template_type == "lha" and self.helicopter_carrier)
|
||||
or category == TemplateCategory.Armor
|
||||
):
|
||||
# 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)
|
||||
# Initialize all randomizers
|
||||
for group_template in faction_template.groups:
|
||||
if group_template.randomizer:
|
||||
group_template.randomizer.init_randomization_for_faction(self)
|
||||
self.templates.add_template(category, faction_template)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
faction = Faction(locales=json.get("locales"))
|
||||
@ -182,8 +240,7 @@ class Faction:
|
||||
faction.logistics_units = [
|
||||
GroundUnitType.named(n) for n in json.get("logistics_units", [])
|
||||
]
|
||||
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
faction.ewrs = [GroundUnitType.named(n) for n in json.get("ewrs", [])]
|
||||
|
||||
faction.air_defenses = json.get("air_defenses", [])
|
||||
# Compatibility for older factions. All air defenses now belong to a
|
||||
@ -245,6 +302,8 @@ class Faction:
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
# Templates
|
||||
faction.templates = GroundObjectTemplates()
|
||||
return faction
|
||||
|
||||
@property
|
||||
@ -320,13 +379,13 @@ class Faction:
|
||||
self.remove_vehicle("KORNET")
|
||||
# high digit sams
|
||||
if not mod_settings.high_digit_sams:
|
||||
self.remove_air_defenses("SA10BGenerator")
|
||||
self.remove_air_defenses("SA12Generator")
|
||||
self.remove_air_defenses("SA20Generator")
|
||||
self.remove_air_defenses("SA20BGenerator")
|
||||
self.remove_air_defenses("SA23Generator")
|
||||
self.remove_air_defenses("SA17Generator")
|
||||
self.remove_air_defenses("KS19Generator")
|
||||
self.remove_air_defenses("SA-10B/S-300PS Battery")
|
||||
self.remove_air_defenses("SA-12/S-300V Battery")
|
||||
self.remove_air_defenses("SA-20/S-300PMU-1 Battery")
|
||||
self.remove_air_defenses("SA-20B/S-300PMU-2 Battery")
|
||||
self.remove_air_defenses("SA-23/S-300VM Battery")
|
||||
self.remove_air_defenses("SA-17 Grizzly Battery")
|
||||
self.remove_air_defenses("KS-19 AAA Site")
|
||||
return self
|
||||
|
||||
def remove_aircraft(self, name: str) -> None:
|
||||
|
||||
@ -123,7 +123,7 @@ class GroundObjectGenerator:
|
||||
if not u.alive:
|
||||
continue
|
||||
|
||||
unit_type = static_type_from_name(u.type)
|
||||
unit_type = unit_type_from_name(u.type)
|
||||
if not unit_type:
|
||||
raise RuntimeError(
|
||||
f"Unit type {u.type} is not a valid dcs unit type"
|
||||
|
||||
@ -158,7 +158,7 @@ class ProcurementAi:
|
||||
worst_balanced: Optional[GroundUnitClass] = None
|
||||
worst_fulfillment = math.inf
|
||||
for unit_class in GroundUnitClass:
|
||||
if not self.faction.has_access_to_unittype(unit_class):
|
||||
if not self.faction.has_access_to_unit_class(unit_class):
|
||||
continue
|
||||
|
||||
current_ratio = self.cost_ratio_of_ground_unit(cp, unit_class)
|
||||
|
||||
@ -1,45 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import pickle
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, List, Set
|
||||
|
||||
from dcs.mapping import Point
|
||||
from typing import List
|
||||
|
||||
from game import Game
|
||||
from game.factions.faction import Faction
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.theater import PointWithHeading
|
||||
from game.theater.theatergroundobject import (
|
||||
AirDefenseRange,
|
||||
BuildingGroundObject,
|
||||
CarrierGroundObject,
|
||||
EwrGroundObject,
|
||||
FactoryGroundObject,
|
||||
LhaGroundObject,
|
||||
MissileSiteGroundObject,
|
||||
SamGroundObject,
|
||||
ShipGroundObject,
|
||||
SceneryGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
CoastalSiteGroundObject,
|
||||
SceneryGroundUnit,
|
||||
GroundGroup,
|
||||
)
|
||||
from game.utils import Heading
|
||||
from game.version import VERSION
|
||||
from gen.coastal.coastal_group_generator import generate_coastal_group
|
||||
from gen.defenses.armor_group_generator import generate_armor_group
|
||||
from gen.fleet.ship_group_generator import (
|
||||
generate_carrier_group,
|
||||
generate_lha_group,
|
||||
generate_ship_group,
|
||||
)
|
||||
from gen.missiles.missiles_group_generator import generate_missile_group
|
||||
from gen.templates import GroundObjectTemplates, TemplateCategory, GroundObjectTemplate
|
||||
from gen.naming import namegen
|
||||
from gen.sam.airdefensegroupgenerator import AirDefenseRange
|
||||
from gen.sam.ewr_group_generator import generate_ewr_group
|
||||
from gen.sam.sam_group_generator import generate_anti_air_group
|
||||
from . import (
|
||||
ConflictTheater,
|
||||
ControlPoint,
|
||||
@ -51,8 +31,6 @@ from ..campaignloader.campaignairwingconfig import CampaignAirWingConfig
|
||||
from ..profiling import logged_duration
|
||||
from ..settings import Settings
|
||||
|
||||
GroundObjectTemplates = Dict[str, Dict[str, Any]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GeneratorSettings:
|
||||
@ -97,6 +75,9 @@ class GameGenerator:
|
||||
self.settings = settings
|
||||
self.generator_settings = generator_settings
|
||||
|
||||
with logged_duration(f"Initializing templates"):
|
||||
self.load_templates()
|
||||
|
||||
def generate(self) -> Game:
|
||||
with logged_duration("TGO population"):
|
||||
# Reset name generator
|
||||
@ -142,6 +123,13 @@ class GameGenerator:
|
||||
for cp in to_remove:
|
||||
self.theater.controlpoints.remove(cp)
|
||||
|
||||
def load_templates(self) -> None:
|
||||
templates = GroundObjectTemplates.from_json(
|
||||
"resources/templates/templates.json"
|
||||
)
|
||||
self.player.load_templates(templates)
|
||||
self.enemy.load_templates(templates)
|
||||
|
||||
|
||||
class ControlPointGroundObjectGenerator:
|
||||
def __init__(
|
||||
@ -170,6 +158,26 @@ class ControlPointGroundObjectGenerator:
|
||||
|
||||
return True
|
||||
|
||||
def generate_random_from_templates(
|
||||
self, templates: list[GroundObjectTemplate], position: PointWithHeading
|
||||
) -> None:
|
||||
try:
|
||||
template = random.choice(templates)
|
||||
with logged_duration(
|
||||
f"Ground Object generation from template {template.name}"
|
||||
):
|
||||
ground_object = template.generate(
|
||||
namegen.random_objective_name(),
|
||||
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")
|
||||
|
||||
def generate_navy(self) -> None:
|
||||
skip_player_navy = self.generator_settings.no_player_navy
|
||||
if self.control_point.captured and skip_player_navy:
|
||||
@ -179,21 +187,12 @@ class ControlPointGroundObjectGenerator:
|
||||
if not self.control_point.captured and skip_enemy_navy:
|
||||
return
|
||||
|
||||
for position in self.control_point.preset_locations.ships:
|
||||
self.generate_ship_at(position)
|
||||
|
||||
def generate_ship_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = ShipGroundObject(
|
||||
namegen.random_objective_name(), group_id, position, self.control_point
|
||||
templates = list(
|
||||
self.faction.templates.for_category(TemplateCategory.Naval, "ship")
|
||||
)
|
||||
|
||||
group = generate_ship_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
for position in self.control_point.preset_locations.ships:
|
||||
self.generate_random_from_templates(templates, position)
|
||||
|
||||
|
||||
class NoOpGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
@ -214,16 +213,16 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
)
|
||||
return False
|
||||
|
||||
# Create ground object group
|
||||
group_id = self.game.next_group_id()
|
||||
g = CarrierGroundObject(
|
||||
namegen.random_objective_name(), group_id, self.control_point
|
||||
templates = list(
|
||||
self.faction.templates.for_category(TemplateCategory.Naval, "carrier")
|
||||
)
|
||||
|
||||
self.generate_random_from_templates(
|
||||
templates,
|
||||
PointWithHeading.from_point(
|
||||
self.control_point.position, self.control_point.heading
|
||||
),
|
||||
)
|
||||
group = generate_carrier_group(self.faction_name, self.game, g)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
self.control_point.name = random.choice(carrier_names)
|
||||
return True
|
||||
|
||||
@ -241,16 +240,16 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
)
|
||||
return False
|
||||
|
||||
# Create ground object group
|
||||
group_id = self.game.next_group_id()
|
||||
g = LhaGroundObject(
|
||||
namegen.random_objective_name(), group_id, self.control_point
|
||||
templates = list(
|
||||
self.faction.templates.for_category(TemplateCategory.Naval, "lha")
|
||||
)
|
||||
|
||||
self.generate_random_from_templates(
|
||||
templates,
|
||||
PointWithHeading.from_point(
|
||||
self.control_point.position, self.control_point.heading
|
||||
),
|
||||
)
|
||||
group = generate_lha_group(self.faction_name, self.game, g)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
self.control_point.name = random.choice(lha_names)
|
||||
return True
|
||||
|
||||
@ -261,10 +260,8 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
game: Game,
|
||||
generator_settings: GeneratorSettings,
|
||||
control_point: ControlPoint,
|
||||
templates: GroundObjectTemplates,
|
||||
) -> None:
|
||||
super().__init__(game, generator_settings, control_point)
|
||||
self.templates = templates
|
||||
|
||||
def generate(self) -> bool:
|
||||
if not super().generate():
|
||||
@ -291,156 +288,102 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.generate_coastal_sites()
|
||||
|
||||
def generate_armor_groups(self) -> None:
|
||||
for position in self.control_point.preset_locations.armor_groups:
|
||||
self.generate_armor_at(position)
|
||||
|
||||
def generate_armor_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = VehicleGroupGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
)
|
||||
|
||||
group = generate_armor_group(self.faction_name, self.game, g)
|
||||
if group is None:
|
||||
logging.error(
|
||||
"Could not generate armor group for %s at %s",
|
||||
g.name,
|
||||
self.control_point,
|
||||
)
|
||||
templates = list(self.faction.templates.for_category(TemplateCategory.Armor))
|
||||
if not templates:
|
||||
logging.error(f"{self.faction_name} has no access to Armor templates")
|
||||
return
|
||||
g.groups = [group]
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
for position in self.control_point.preset_locations.armor_groups:
|
||||
self.generate_random_from_templates(templates, position)
|
||||
|
||||
def generate_aa(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.long_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[
|
||||
{AirDefenseRange.Long},
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
{AirDefenseRange.AAA},
|
||||
[
|
||||
AirDefenseRange.Long,
|
||||
AirDefenseRange.Medium,
|
||||
AirDefenseRange.Short,
|
||||
AirDefenseRange.AAA,
|
||||
],
|
||||
)
|
||||
for position in presets.medium_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[
|
||||
{AirDefenseRange.Medium},
|
||||
{AirDefenseRange.Short},
|
||||
{AirDefenseRange.AAA},
|
||||
[
|
||||
AirDefenseRange.Medium,
|
||||
AirDefenseRange.Short,
|
||||
AirDefenseRange.AAA,
|
||||
],
|
||||
)
|
||||
for position in presets.short_range_sams:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}],
|
||||
[AirDefenseRange.Short, AirDefenseRange.AAA],
|
||||
)
|
||||
for position in presets.aaa:
|
||||
self.generate_aa_at(
|
||||
position,
|
||||
ranges=[{AirDefenseRange.AAA}],
|
||||
[AirDefenseRange.AAA],
|
||||
)
|
||||
|
||||
def generate_ewrs(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
for position in presets.ewrs:
|
||||
self.generate_ewr_at(position)
|
||||
templates = list(
|
||||
self.faction.templates.for_category(TemplateCategory.AirDefence, "EWR")
|
||||
)
|
||||
if not templates:
|
||||
logging.error(f"{self.faction_name} has no access to EWR templates")
|
||||
return
|
||||
|
||||
def generate_strike_target_at(self, category: str, position: Point) -> None:
|
||||
for position in self.control_point.preset_locations.ewrs:
|
||||
self.generate_random_from_templates(templates, position)
|
||||
|
||||
obj_name = namegen.random_objective_name()
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
|
||||
object_id = 0
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
# TODO: Create only one TGO per objective, each with multiple units.
|
||||
for unit in template:
|
||||
object_id += 1
|
||||
|
||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||
g = BuildingGroundObject(
|
||||
obj_name,
|
||||
category,
|
||||
group_id,
|
||||
object_id,
|
||||
position + template_point,
|
||||
Heading.from_degrees(unit["heading"]),
|
||||
self.control_point,
|
||||
unit["type"],
|
||||
def generate_building_at(
|
||||
self,
|
||||
template_category: TemplateCategory,
|
||||
building_category: str,
|
||||
position: PointWithHeading,
|
||||
) -> None:
|
||||
templates = list(
|
||||
self.faction.templates.for_category(template_category, building_category)
|
||||
)
|
||||
if templates:
|
||||
self.generate_random_from_templates(templates, position)
|
||||
else:
|
||||
logging.error(
|
||||
f"{self.faction_name} has no access to Building type {building_category}"
|
||||
)
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_ammunition_depots(self) -> None:
|
||||
for position in self.control_point.preset_locations.ammunition_depots:
|
||||
self.generate_strike_target_at(category="ammo", position=position)
|
||||
self.generate_building_at(TemplateCategory.Building, "ammo", position)
|
||||
|
||||
def generate_factories(self) -> None:
|
||||
for position in self.control_point.preset_locations.factories:
|
||||
self.generate_factory_at(position)
|
||||
|
||||
def generate_factory_at(self, point: PointWithHeading) -> None:
|
||||
obj_name = namegen.random_objective_name()
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = FactoryGroundObject(
|
||||
obj_name,
|
||||
group_id,
|
||||
point,
|
||||
point.heading,
|
||||
self.control_point,
|
||||
)
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
self.generate_building_at(TemplateCategory.Building, "factory", position)
|
||||
|
||||
def generate_aa_at(
|
||||
self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
|
||||
self, position: PointWithHeading, ranges: list[AirDefenseRange]
|
||||
) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = SamGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
)
|
||||
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
|
||||
if not groups:
|
||||
templates = []
|
||||
for aa_range in ranges:
|
||||
for template in self.faction.templates.for_category(
|
||||
TemplateCategory.AirDefence, aa_range.name
|
||||
):
|
||||
templates.append(template)
|
||||
if len(templates) > 0:
|
||||
# Only take next (smaller) aa_range when no template available for the
|
||||
# most requested range. Otherwise break the loop and continue
|
||||
break
|
||||
|
||||
if templates:
|
||||
self.generate_random_from_templates(templates, position)
|
||||
else:
|
||||
logging.error(
|
||||
"Could not generate air defense group for %s at %s",
|
||||
g.name,
|
||||
self.control_point,
|
||||
f"{self.faction_name} has no access to SAM Templates ({', '.join([range.name for range in ranges])})"
|
||||
)
|
||||
return
|
||||
g.groups = groups
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_ewr_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = EwrGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
)
|
||||
group = generate_ewr_group(self.game, g, self.faction)
|
||||
if group is None:
|
||||
logging.error(
|
||||
"Could not generate ewr group for %s at %s",
|
||||
g.name,
|
||||
self.control_point,
|
||||
)
|
||||
return
|
||||
g.groups = [group]
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_scenery_sites(self) -> None:
|
||||
presets = self.control_point.preset_locations
|
||||
@ -448,143 +391,93 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
|
||||
self.generate_tgo_for_scenery(scenery_group)
|
||||
|
||||
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
|
||||
|
||||
obj_name = namegen.random_objective_name()
|
||||
category = scenery.category
|
||||
group_id = self.game.next_group_id()
|
||||
object_id = 0
|
||||
|
||||
# Special Handling for scenery Objects based on trigger zones
|
||||
g = BuildingGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
scenery.category,
|
||||
scenery.position,
|
||||
Heading.from_degrees(0),
|
||||
self.control_point,
|
||||
)
|
||||
ground_group = GroundGroup(
|
||||
self.game.next_group_id(),
|
||||
scenery.zone_def.name,
|
||||
PointWithHeading.from_point(scenery.position, Heading.from_degrees(0)),
|
||||
[],
|
||||
g,
|
||||
)
|
||||
ground_group.static_group = True
|
||||
g.groups.append(ground_group)
|
||||
# Each nested trigger zone is a target/building/unit for an objective.
|
||||
for zone in scenery.zones:
|
||||
|
||||
object_id += 1
|
||||
local_position = zone.position
|
||||
local_dcs_identifier = zone.name
|
||||
|
||||
g = SceneryGroundObject(
|
||||
obj_name,
|
||||
category,
|
||||
group_id,
|
||||
object_id,
|
||||
local_position,
|
||||
self.control_point,
|
||||
local_dcs_identifier,
|
||||
zone,
|
||||
scenery_unit = SceneryGroundUnit(
|
||||
zone.id,
|
||||
zone.name,
|
||||
"",
|
||||
PointWithHeading.from_point(zone.position, Heading.from_degrees(0)),
|
||||
g,
|
||||
)
|
||||
scenery_unit.zone = zone
|
||||
ground_group.units.append(scenery_unit)
|
||||
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
return
|
||||
self.control_point.connected_objectives.append(g)
|
||||
|
||||
def generate_missile_sites(self) -> None:
|
||||
templates = list(self.faction.templates.for_category(TemplateCategory.Missile))
|
||||
if not templates:
|
||||
logging.error(f"{self.faction_name} has no access to Missile templates")
|
||||
return
|
||||
for position in self.control_point.preset_locations.missile_sites:
|
||||
self.generate_missile_site_at(position)
|
||||
|
||||
def generate_missile_site_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = MissileSiteGroundObject(
|
||||
namegen.random_objective_name(), group_id, position, self.control_point
|
||||
)
|
||||
group = generate_missile_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
return
|
||||
self.generate_random_from_templates(templates, position)
|
||||
|
||||
def generate_coastal_sites(self) -> None:
|
||||
templates = list(self.faction.templates.for_category(TemplateCategory.Coastal))
|
||||
if not templates:
|
||||
logging.error(f"{self.faction_name} has no access to Coastal templates")
|
||||
return
|
||||
for position in self.control_point.preset_locations.coastal_defenses:
|
||||
self.generate_coastal_site_at(position)
|
||||
|
||||
def generate_coastal_site_at(self, position: PointWithHeading) -> None:
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
g = CoastalSiteGroundObject(
|
||||
namegen.random_objective_name(),
|
||||
group_id,
|
||||
position,
|
||||
self.control_point,
|
||||
position.heading,
|
||||
)
|
||||
group = generate_coastal_group(self.game, g, self.faction_name)
|
||||
g.groups = []
|
||||
if group is not None:
|
||||
g.groups.append(group)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
return
|
||||
self.generate_random_from_templates(templates, position)
|
||||
|
||||
def generate_strike_targets(self) -> None:
|
||||
building_set = list(set(self.faction.building_set) - {"oil"})
|
||||
if not building_set:
|
||||
logging.error("Faction has no buildings defined")
|
||||
logging.error(f"{self.faction_name} has no buildings defined")
|
||||
return
|
||||
for position in self.control_point.preset_locations.strike_locations:
|
||||
category = random.choice(building_set)
|
||||
self.generate_strike_target_at(category, position)
|
||||
self.generate_building_at(TemplateCategory.Building, category, position)
|
||||
|
||||
def generate_offshore_strike_targets(self) -> None:
|
||||
if "oil" not in self.faction.building_set:
|
||||
logging.error("Faction does not support offshore strike targets")
|
||||
logging.error(
|
||||
f"{self.faction_name} does not support offshore strike targets"
|
||||
)
|
||||
return
|
||||
for position in self.control_point.preset_locations.offshore_strike_locations:
|
||||
self.generate_strike_target_at("oil", position)
|
||||
self.generate_building_at(TemplateCategory.Building, "oil", position)
|
||||
|
||||
|
||||
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
|
||||
def generate(self) -> bool:
|
||||
self.generate_fob()
|
||||
self.generate_armor_groups()
|
||||
self.generate_factories()
|
||||
self.generate_ammunition_depots()
|
||||
self.generate_aa()
|
||||
self.generate_ewrs()
|
||||
self.generate_scenery_sites()
|
||||
self.generate_strike_targets()
|
||||
self.generate_offshore_strike_targets()
|
||||
|
||||
if self.faction.missiles:
|
||||
self.generate_missile_sites()
|
||||
|
||||
if self.faction.coastal_defenses:
|
||||
self.generate_coastal_sites()
|
||||
|
||||
return True
|
||||
if super(FobGroundObjectGenerator, self).generate():
|
||||
self.generate_fob()
|
||||
return True
|
||||
return False
|
||||
|
||||
def generate_fob(self) -> None:
|
||||
category = "fob"
|
||||
obj_name = self.control_point.name
|
||||
template = random.choice(list(self.templates[category].values()))
|
||||
point = self.control_point.position
|
||||
# Pick from preset locations
|
||||
object_id = 0
|
||||
group_id = self.game.next_group_id()
|
||||
|
||||
# TODO: Create only one TGO per objective, each with multiple units.
|
||||
for unit in template:
|
||||
object_id += 1
|
||||
|
||||
template_point = Point(unit["offset"].x, unit["offset"].y)
|
||||
g = BuildingGroundObject(
|
||||
obj_name,
|
||||
category,
|
||||
group_id,
|
||||
object_id,
|
||||
point + template_point,
|
||||
Heading.from_degrees(unit["heading"]),
|
||||
self.control_point,
|
||||
unit["type"],
|
||||
is_fob_structure=True,
|
||||
)
|
||||
self.control_point.connected_objectives.append(g)
|
||||
self.generate_building_at(
|
||||
TemplateCategory.Building,
|
||||
"fob",
|
||||
PointWithHeading.from_point(
|
||||
self.control_point.position, self.control_point.heading
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class GroundObjectGenerator:
|
||||
def __init__(self, game: Game, generator_settings: GeneratorSettings) -> None:
|
||||
self.game = game
|
||||
self.generator_settings = generator_settings
|
||||
with open("resources/groundobject_templates.p", "rb") as f:
|
||||
self.templates: GroundObjectTemplates = pickle.load(f)
|
||||
|
||||
def generate(self) -> None:
|
||||
# Copied so we can remove items from the original list without breaking
|
||||
@ -610,10 +503,10 @@ class GroundObjectGenerator:
|
||||
)
|
||||
elif isinstance(control_point, Fob):
|
||||
generator = FobGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point, self.templates
|
||||
self.game, self.generator_settings, control_point
|
||||
)
|
||||
else:
|
||||
generator = AirbaseGroundObjectGenerator(
|
||||
self.game, self.generator_settings, control_point, self.templates
|
||||
self.game, self.generator_settings, control_point
|
||||
)
|
||||
return generator.generate()
|
||||
|
||||
@ -6,21 +6,23 @@ from abc import ABC
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union, Optional
|
||||
from typing import Iterator, List, TYPE_CHECKING, Union, Optional, Any
|
||||
|
||||
from dcs.vehicles import vehicle_map
|
||||
from dcs.ships import ship_map
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.triggers import TriggerZone
|
||||
from dcs.unit import Unit
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.dcs.helpers import unit_type_from_name
|
||||
from ..data.radar_db import LAUNCHER_TRACKER_PAIRS, TELARS, TRACK_RADARS
|
||||
from ..dcs.groundunittype import GroundUnitType
|
||||
from ..dcs.unittype import UnitType
|
||||
from ..point_with_heading import PointWithHeading
|
||||
from ..utils import Distance, Heading, meters
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.templates import UnitTemplate, GroupTemplate, TemplateRandomizer
|
||||
from gen.templates import UnitTemplate, GroupTemplate
|
||||
from .controlpoint import ControlPoint
|
||||
from ..ato.flighttype import FlightType
|
||||
|
||||
@ -85,7 +87,7 @@ class GroundUnit:
|
||||
position: PointWithHeading
|
||||
ground_object: TheaterGroundObject
|
||||
alive: bool = True
|
||||
_dcs_type: Optional[GroundUnitType] = None
|
||||
_unit_type: Optional[UnitType[Any]] = None
|
||||
|
||||
@staticmethod
|
||||
def from_template(id: int, t: UnitTemplate, go: TheaterGroundObject) -> GroundUnit:
|
||||
@ -98,17 +100,24 @@ class GroundUnit:
|
||||
)
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Optional[GroundUnitType]:
|
||||
if not self._dcs_type:
|
||||
def unit_type(self) -> Optional[UnitType[Any]]:
|
||||
if not self._unit_type:
|
||||
try:
|
||||
if self.type in vehicle_map:
|
||||
dcs_unit_type = vehicle_map[self.type]
|
||||
vehicle_type = db.vehicle_type_from_name(self.type)
|
||||
self._unit_type = next(GroundUnitType.for_dcs_type(vehicle_type))
|
||||
elif self.type in ship_map:
|
||||
ship_type = db.ship_type_from_name(self.type)
|
||||
# TODO Allow handling of Ships. This requires extension of UnitType
|
||||
return None
|
||||
elif (static_type := db.static_type_from_name(self.type)) is not None:
|
||||
# TODO Allow handling of Statics
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
self._dcs_type = next(GroundUnitType.for_dcs_type(dcs_unit_type))
|
||||
except StopIteration:
|
||||
return None
|
||||
return self._dcs_type
|
||||
return self._unit_type
|
||||
|
||||
def kill(self) -> None:
|
||||
self.alive = False
|
||||
|
||||
576
gen/templates.py
Normal file
576
gen/templates.py
Normal file
@ -0,0 +1,576 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Iterator, Any, TYPE_CHECKING, Optional, Tuple, Union
|
||||
|
||||
from dcs import Point
|
||||
from dcs.ships import ship_map
|
||||
from dcs.unit import Unit
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater.theatergroundobject import (
|
||||
SamGroundObject,
|
||||
EwrGroundObject,
|
||||
BuildingGroundObject,
|
||||
GroundGroup,
|
||||
MissileSiteGroundObject,
|
||||
ShipGroundObject,
|
||||
CarrierGroundObject,
|
||||
LhaGroundObject,
|
||||
CoastalSiteGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
IadsGroundObject,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
class TemplateEncoder(json.JSONEncoder):
|
||||
def default(self, obj: Any) -> dict[str, Any]:
|
||||
if hasattr(obj, "to_json"):
|
||||
return obj.to_json()
|
||||
else:
|
||||
return obj.__dict__
|
||||
|
||||
|
||||
class TemplateCategory(Enum):
|
||||
AirDefence = "AirDefence" # Has subcategories for the AARange
|
||||
Building = "Building" # Has subcategories from
|
||||
Naval = "Naval" # Has subcategories lha, carrier, ship
|
||||
Armor = "Armor"
|
||||
Missile = "Missile"
|
||||
Coastal = "Coastal"
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnitTemplate:
|
||||
name: str
|
||||
type: str
|
||||
position: Point
|
||||
heading: int
|
||||
|
||||
@staticmethod
|
||||
def from_unit(unit: Unit) -> UnitTemplate:
|
||||
return UnitTemplate(
|
||||
unit.name,
|
||||
unit.type,
|
||||
Point(int(unit.position.x), int(unit.position.y)),
|
||||
int(unit.heading),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d_unit: dict[str, Any]) -> UnitTemplate:
|
||||
return UnitTemplate(
|
||||
d_unit["name"],
|
||||
d_unit["type"],
|
||||
Point(d_unit["position"]["x"], d_unit["position"]["y"]),
|
||||
d_unit["heading"],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupTemplate:
|
||||
name: str
|
||||
units: list[UnitTemplate]
|
||||
|
||||
# Is Static group
|
||||
static: bool = False
|
||||
|
||||
# Every group can only have one Randomizer
|
||||
randomizer: Optional[TemplateRandomizer] = None
|
||||
|
||||
# Defines if this groupTemplate is required or not
|
||||
optional: bool = False
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d_group: dict[str, Any]) -> GroupTemplate:
|
||||
units = [UnitTemplate.from_dict(unit) for unit in d_group["units"]]
|
||||
randomizer = (
|
||||
TemplateRandomizer.from_dict(d_group["randomizer"])
|
||||
if d_group["randomizer"]
|
||||
else None
|
||||
)
|
||||
return GroupTemplate(
|
||||
d_group["name"], units, d_group["static"], randomizer, d_group["optional"]
|
||||
)
|
||||
|
||||
@property
|
||||
def unit_types_count(self) -> dict[str, int]:
|
||||
units: dict[str, int] = {}
|
||||
for unit in self.units:
|
||||
if unit.type in units:
|
||||
units[unit.type] += 1
|
||||
else:
|
||||
units[unit.type] = 1
|
||||
return units
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateRandomizer:
|
||||
# Selection of units to apply the randomization.
|
||||
# If left empty the randomizer will be applied to all unit of the group
|
||||
units: list[int] = field(default_factory=list)
|
||||
|
||||
# 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
|
||||
count: Union[int, list[int]] = field(default=1)
|
||||
|
||||
# The randomizer can pick a random unit type from a faction list like
|
||||
# frontline_units or air_defenses to allow faction sensitive randomization
|
||||
faction_types: list[str] = field(default_factory=list)
|
||||
|
||||
# Only works for vehicle units. Allows to specify the type class of the unit.
|
||||
# For example this allows to select frontline_units as faction_type and also define
|
||||
# Shorads as class to only pick AntiAir from the list
|
||||
type_classes: list[str] = field(default_factory=list)
|
||||
|
||||
# Allows to define the exact UnitTypes the randomizer picks from. these have to be
|
||||
# the dcs_unit_types found in the pydcs arrays
|
||||
unit_types: list[str] = field(default_factory=list)
|
||||
|
||||
# Runtime Attributes
|
||||
_initialized: bool = False
|
||||
_possible_types: list[str] = field(default_factory=list)
|
||||
_random_unit_type: Optional[str] = None
|
||||
_forced_unit_type: Optional[str] = None
|
||||
_unit_counter: Optional[int] = None
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
d = self.__dict__
|
||||
# Do not serialize the runtime attributes
|
||||
d.pop("_initialized", None)
|
||||
d.pop("_possible_types", None)
|
||||
d.pop("_random_unit_type", None)
|
||||
d.pop("_forced_unit_type", None)
|
||||
d.pop("_unit_counter", None)
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, Any]) -> TemplateRandomizer:
|
||||
return TemplateRandomizer(
|
||||
d["units"],
|
||||
d["count"],
|
||||
d["faction_types"],
|
||||
d["type_classes"],
|
||||
d["unit_types"],
|
||||
)
|
||||
|
||||
@property
|
||||
def possible_ground_units(self) -> Iterator[GroundUnitType]:
|
||||
for unit_type in self._possible_types:
|
||||
if unit_type in vehicle_map:
|
||||
dcs_unit_type = vehicle_map[unit_type]
|
||||
try:
|
||||
yield next(GroundUnitType.for_dcs_type(dcs_unit_type))
|
||||
except StopIteration:
|
||||
continue
|
||||
|
||||
def force_type(self, type: str) -> None:
|
||||
self._forced_unit_type = type
|
||||
|
||||
def randomize(self) -> None:
|
||||
self.randomize_unit_type()
|
||||
self.reset_unit_counter()
|
||||
|
||||
def reset_unit_counter(self) -> None:
|
||||
if isinstance(self.count, list):
|
||||
count = random.choice(range(self.count[0], self.count[1]))
|
||||
elif isinstance(self.count, int):
|
||||
count = self.count
|
||||
self._unit_counter = count
|
||||
|
||||
def init_randomization_for_faction(self, faction: Faction) -> None:
|
||||
# Initializes the randomization
|
||||
# This sets the random_unit_type and the random_unit_count
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
type_list = []
|
||||
for faction_type in self.faction_types:
|
||||
for unit_type in faction[faction_type]:
|
||||
if isinstance(unit_type, GroundUnitType):
|
||||
# GroundUnitType
|
||||
type_list.append(unit_type.dcs_id)
|
||||
elif issubclass(unit_type, UnitType):
|
||||
# DCS Unit Type object
|
||||
type_list.append(unit_type.id)
|
||||
elif db.unit_type_from_name(unit_type):
|
||||
# DCS Unit Type as string
|
||||
type_list.append(unit_type)
|
||||
else:
|
||||
raise KeyError
|
||||
|
||||
if self.unit_types and self.faction_types:
|
||||
# If Faction types were defined use unit_types as filter
|
||||
filtered_type_list = [
|
||||
unit_type for unit_type in type_list if unit_type in self.unit_types
|
||||
]
|
||||
type_list = filtered_type_list
|
||||
else:
|
||||
# If faction_types is not defined append the unit_types
|
||||
for unit_type in self.unit_types:
|
||||
type_list.append(unit_type)
|
||||
|
||||
if self.type_classes:
|
||||
filtered_type_list = []
|
||||
for unit_type in type_list:
|
||||
if unit_type in vehicle_map:
|
||||
dcs_type = vehicle_map[unit_type]
|
||||
else:
|
||||
continue
|
||||
try:
|
||||
ground_unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
|
||||
except (KeyError, StopIteration):
|
||||
logging.error(f"Unit {unit_type} has no GroundUnitType")
|
||||
continue
|
||||
if (
|
||||
ground_unit_type.unit_class
|
||||
and ground_unit_type.unit_class.value in self.type_classes
|
||||
):
|
||||
filtered_type_list.append(unit_type)
|
||||
type_list = filtered_type_list
|
||||
self._possible_types = type_list
|
||||
if self.randomize_unit_type():
|
||||
self.reset_unit_counter()
|
||||
self._initialized = True
|
||||
|
||||
@property
|
||||
def unit_type(self) -> Optional[str]:
|
||||
return self._random_unit_type
|
||||
|
||||
def randomize_unit_type(self) -> bool:
|
||||
try:
|
||||
self._random_unit_type = self._forced_unit_type or random.choice(
|
||||
self._possible_types
|
||||
)
|
||||
except IndexError:
|
||||
logging.warning("Can not initialize randomizer")
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def unit_count(self) -> int:
|
||||
if not self._unit_counter:
|
||||
self.reset_unit_counter()
|
||||
return self._unit_counter or 1
|
||||
|
||||
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 unit_range(self) -> list[int]:
|
||||
if len(self.units) > 1:
|
||||
return list(range(self.units[0], self.units[1] + 1))
|
||||
return self.units
|
||||
|
||||
|
||||
class GroundObjectTemplate(ABC):
|
||||
def __init__(
|
||||
self, name: str, template_type: str = "", description: str = ""
|
||||
) -> None:
|
||||
self.name = name
|
||||
self.template_type = template_type
|
||||
self.description = description
|
||||
self.groups: list[GroupTemplate] = []
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d_object: dict[str, Any]) -> GroundObjectTemplate:
|
||||
template = cls(
|
||||
d_object["name"], d_object["template_type"], d_object["description"]
|
||||
)
|
||||
for d_group in d_object["groups"]:
|
||||
template.groups.append(GroupTemplate.from_dict(d_group))
|
||||
return template
|
||||
|
||||
def generate(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
game: Game,
|
||||
randomization: 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):
|
||||
tgo_group = GroundGroup.from_template(
|
||||
game.next_group_id(),
|
||||
group,
|
||||
ground_object,
|
||||
randomization,
|
||||
)
|
||||
# Set Group Name
|
||||
tgo_group.name = f"{self.name} {g_id}"
|
||||
|
||||
# Assign UniqueID, name and align relative to ground_object
|
||||
for u_id, unit in enumerate(tgo_group.units):
|
||||
unit.id = game.next_unit_id()
|
||||
unit.name = f"{self.name} {g_id}-{u_id}"
|
||||
if isinstance(self, AirDefenceTemplate):
|
||||
# Head SAM and EWR towards the center of the conflict
|
||||
unit.position.heading = (
|
||||
game.theater.heading_to_conflict_from(unit.position)
|
||||
or unit.position.heading
|
||||
)
|
||||
unit.position = PointWithHeading.from_point(
|
||||
Point(
|
||||
ground_object.position.x + unit.position.x,
|
||||
ground_object.position.y + unit.position.y,
|
||||
),
|
||||
unit.position.heading,
|
||||
)
|
||||
ground_object.groups.append(tgo_group)
|
||||
|
||||
return ground_object
|
||||
|
||||
@abstractmethod
|
||||
def _create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def randomizable(self) -> bool:
|
||||
# Returns True if any group of the template has a randomizer
|
||||
return any(group_template.randomizer for group_template in self.groups)
|
||||
|
||||
def estimated_price_for(self, go: TheaterGroundObject) -> float:
|
||||
# Price can only be estimated because of randomization
|
||||
template_price = 0
|
||||
for g_id, group in enumerate(self.groups):
|
||||
tgo_group = GroundGroup.from_template(g_id, group, go)
|
||||
for unit in tgo_group.units:
|
||||
if unit.type in vehicle_map:
|
||||
dcs_type = vehicle_map[unit.type]
|
||||
try:
|
||||
unit_type = next(GroundUnitType.for_dcs_type(dcs_type))
|
||||
except StopIteration:
|
||||
continue
|
||||
template_price = template_price + unit_type.price
|
||||
return template_price
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return sum([len(group.units) for group in self.groups])
|
||||
|
||||
@property
|
||||
def min_size(self) -> int:
|
||||
return self._size_for_randomized(True)
|
||||
|
||||
@property
|
||||
def max_size(self) -> int:
|
||||
return self._size_for_randomized(False)
|
||||
|
||||
def _size_for_randomized(self, min_size: bool) -> int:
|
||||
size = 0
|
||||
for group in self.groups:
|
||||
for unit_id, unit in enumerate(group.units):
|
||||
if group.randomizer and unit_id in group.randomizer.units:
|
||||
if isinstance(group.randomizer.count, int):
|
||||
size = size + group.randomizer.count
|
||||
else:
|
||||
size = size + group.randomizer.count[0 if min_size else 1]
|
||||
else:
|
||||
size = size + 1
|
||||
return size
|
||||
|
||||
@property
|
||||
def required_units(self) -> list[str]:
|
||||
"""returns all required unit types by theyre dcs type id"""
|
||||
# todo take care for randomizer
|
||||
unit_types = []
|
||||
for group in self.groups:
|
||||
# this completly excludes randomized groups
|
||||
if not group.optional and not group.randomizer:
|
||||
for unit in group.units:
|
||||
if unit.type not in unit_types:
|
||||
unit_types.append(unit.type)
|
||||
return unit_types
|
||||
|
||||
|
||||
class AirDefenceTemplate(GroundObjectTemplate):
|
||||
def _create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> IadsGroundObject:
|
||||
if self.template_type == "EWR":
|
||||
return EwrGroundObject(name, position, control_point)
|
||||
elif self.template_type in ["Long", "Medium", "Short", "AAA"]:
|
||||
return SamGroundObject(name, position, control_point)
|
||||
raise RuntimeError(
|
||||
f" No Template Definition for AirDefence with subcategory {self.template_type}"
|
||||
)
|
||||
|
||||
|
||||
class BuildingTemplate(GroundObjectTemplate):
|
||||
def _create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> BuildingGroundObject:
|
||||
return BuildingGroundObject(
|
||||
name,
|
||||
self.template_type,
|
||||
position,
|
||||
Heading.from_degrees(0),
|
||||
control_point,
|
||||
self.template_type == "fob",
|
||||
)
|
||||
|
||||
|
||||
class NavalTemplate(GroundObjectTemplate):
|
||||
def _create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
if self.template_type == "ship":
|
||||
return ShipGroundObject(name, position, control_point)
|
||||
elif self.template_type == "carrier":
|
||||
return CarrierGroundObject(name, control_point)
|
||||
elif self.template_type == "lha":
|
||||
return LhaGroundObject(name, control_point)
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CoastalTemplate(GroundObjectTemplate):
|
||||
def _create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
return CoastalSiteGroundObject(name, position, control_point, position.heading)
|
||||
|
||||
|
||||
class ArmorTemplate(GroundObjectTemplate):
|
||||
def _create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
return VehicleGroupGroundObject(name, position, control_point)
|
||||
|
||||
|
||||
class MissileTemplate(GroundObjectTemplate):
|
||||
def _create_ground_object(
|
||||
self,
|
||||
name: str,
|
||||
position: PointWithHeading,
|
||||
control_point: ControlPoint,
|
||||
) -> TheaterGroundObject:
|
||||
return MissileSiteGroundObject(name, position, control_point)
|
||||
|
||||
|
||||
TEMPLATE_TYPES = {
|
||||
TemplateCategory.AirDefence: AirDefenceTemplate,
|
||||
TemplateCategory.Building: BuildingTemplate,
|
||||
TemplateCategory.Naval: NavalTemplate,
|
||||
TemplateCategory.Armor: ArmorTemplate,
|
||||
TemplateCategory.Missile: MissileTemplate,
|
||||
TemplateCategory.Coastal: CoastalTemplate,
|
||||
}
|
||||
|
||||
|
||||
class GroundObjectTemplates:
|
||||
# list of templates per category. e.g. AA or similar
|
||||
_templates: dict[TemplateCategory, list[GroundObjectTemplate]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._templates = {}
|
||||
|
||||
@property
|
||||
def templates(self) -> Iterator[tuple[TemplateCategory, GroundObjectTemplate]]:
|
||||
for category, templates in self._templates.items():
|
||||
for template in templates:
|
||||
yield category, template
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, template_file: str) -> GroundObjectTemplates:
|
||||
# Rebuild the TemplatesObject from the json dict
|
||||
|
||||
obj = GroundObjectTemplates()
|
||||
with open(template_file, "r") as f:
|
||||
json_templates: dict[str, list[dict[str, Any]]] = json.load(f)
|
||||
for category, templates in json_templates.items():
|
||||
for d_template in templates:
|
||||
template = TEMPLATE_TYPES[TemplateCategory(category)].from_dict(
|
||||
d_template
|
||||
)
|
||||
obj.add_template(TemplateCategory(category), template)
|
||||
return obj
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
return {
|
||||
category.value: templates for category, templates in self._templates.items()
|
||||
}
|
||||
|
||||
@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 by_category_and_name(
|
||||
self, category: TemplateCategory, template_name: str
|
||||
) -> Optional[GroundObjectTemplate]:
|
||||
if category in self._templates:
|
||||
for template in self._templates[category]:
|
||||
if template.name == template_name:
|
||||
return template
|
||||
return None
|
||||
|
||||
def add_template(
|
||||
self, category: TemplateCategory, template: GroundObjectTemplate
|
||||
) -> None:
|
||||
if category not in self._templates:
|
||||
self._templates[category] = [template]
|
||||
else:
|
||||
self._templates[category].append(template)
|
||||
|
||||
def for_category(
|
||||
self, category: TemplateCategory, sub_category: Optional[str] = None
|
||||
) -> Iterator[GroundObjectTemplate]:
|
||||
|
||||
if category not in self._templates:
|
||||
return None
|
||||
|
||||
for template in self._templates[category]:
|
||||
if not sub_category or template.template_type == sub_category:
|
||||
yield template
|
||||
259
qt_ui/windows/groundobject/QGroundObjectBuyMenu.py
Normal file
259
qt_ui/windows/groundobject/QGroundObjectBuyMenu.py
Normal file
@ -0,0 +1,259 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PySide2.QtCore import Signal
|
||||
from PySide2.QtGui import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QVBoxLayout,
|
||||
QCheckBox,
|
||||
)
|
||||
|
||||
from game import Game
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.theater import TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
VehicleGroupGroundObject,
|
||||
SamGroundObject,
|
||||
EwrGroundObject,
|
||||
GroundGroup,
|
||||
)
|
||||
from gen.templates import (
|
||||
TemplateCategory,
|
||||
GroundObjectTemplate,
|
||||
GroupTemplate,
|
||||
)
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
|
||||
|
||||
class QGroundObjectGroupTemplate(QGroupBox):
|
||||
group_template_changed = Signal(GroupTemplate)
|
||||
# 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:
|
||||
super(QGroundObjectGroupTemplate, self).__init__(str(group_id + 1))
|
||||
self.group_template = group_template
|
||||
|
||||
self.group_layout = QGridLayout()
|
||||
self.setLayout(self.group_layout)
|
||||
|
||||
self.amount_selector = QSpinBox()
|
||||
self.unit_selector = QComboBox()
|
||||
self.group_selector = QCheckBox()
|
||||
|
||||
self.group_selector.setChecked(True)
|
||||
self.group_selector.setEnabled(self.group_template.optional)
|
||||
|
||||
if self.group_template.randomizer:
|
||||
# Group can be randomized
|
||||
for unit in self.group_template.randomizer.possible_ground_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(
|
||||
self.amount_selector, 0, 1, alignment=Qt.AlignRight
|
||||
)
|
||||
|
||||
self.amount_selector.setMinimum(1)
|
||||
self.amount_selector.setMaximum(len(self.group_template.units))
|
||||
self.amount_selector.setValue(self.group_template.randomizer.unit_count)
|
||||
|
||||
self.on_group_changed()
|
||||
else:
|
||||
# Group can not be randomized so just show the group info
|
||||
group_info = QVBoxLayout()
|
||||
for unit_type, count in self.group_template.unit_types_count.items():
|
||||
group_info.addWidget(
|
||||
QLabel(f"{count}x {unit_type}"), 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.amount_selector.valueChanged.connect(self.on_group_changed)
|
||||
self.unit_selector.currentIndexChanged.connect(self.on_group_changed)
|
||||
self.group_selector.stateChanged.connect(self.on_group_changed)
|
||||
|
||||
def on_group_changed(self) -> None:
|
||||
unit_type = self.unit_selector.itemData(self.unit_selector.currentIndex())
|
||||
count = self.amount_selector.value() if self.group_selector.isChecked() else 0
|
||||
self.group_template.randomizer.count = count
|
||||
self.group_template.randomizer.force_type(unit_type.dcs_id)
|
||||
self.group_template_changed.emit(self.group_template)
|
||||
|
||||
|
||||
class QGroundObjectTemplateLayout(QGroupBox):
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
ground_object: TheaterGroundObject,
|
||||
template_changed_signal: Signal(GroundObjectTemplate),
|
||||
current_group_value: int,
|
||||
):
|
||||
super(QGroundObjectTemplateLayout, self).__init__("Groups:")
|
||||
# Connect to the signal to handle template updates
|
||||
self.game = game
|
||||
self.ground_object = ground_object
|
||||
self.template_changed_signal = template_changed_signal
|
||||
self.template_changed_signal.connect(self.load_for_template)
|
||||
self.template: Optional[GroundObjectTemplate] = None
|
||||
|
||||
self.current_group_value = current_group_value
|
||||
|
||||
self.buy_button = QPushButton("Buy")
|
||||
self.buy_button.clicked.connect(self.buy_group)
|
||||
|
||||
self.template_layout = QGridLayout()
|
||||
self.setLayout(self.template_layout)
|
||||
|
||||
self.template_grid = QGridLayout()
|
||||
self.template_layout.addLayout(self.template_grid, 0, 0, 1, 2)
|
||||
self.template_layout.addWidget(self.buy_button, 1, 1)
|
||||
stretch = QVBoxLayout()
|
||||
stretch.addStretch()
|
||||
self.template_layout.addLayout(stretch, 2, 0)
|
||||
|
||||
def load_for_template(self, template: GroundObjectTemplate) -> None:
|
||||
self.template = template
|
||||
|
||||
# Clean the current grid
|
||||
for id in range(self.template_grid.count()):
|
||||
self.template_grid.itemAt(id).widget().deleteLater()
|
||||
|
||||
for g_id, group in enumerate(template.groups):
|
||||
group_row = QGroundObjectGroupTemplate(g_id, group)
|
||||
group_row.group_template_changed.connect(self.group_template_changed)
|
||||
self.template_grid.addWidget(group_row)
|
||||
|
||||
self.update_price()
|
||||
|
||||
def group_template_changed(self, group_template: GroupTemplate) -> None:
|
||||
self.update_price()
|
||||
|
||||
def update_price(self) -> None:
|
||||
price = "$" + str(self.template.estimated_price_for(self.ground_object))
|
||||
if self.template.randomizable:
|
||||
price = "~" + price
|
||||
self.buy_button.setText(f"Buy [{price}M][-${self.current_group_value}M]")
|
||||
|
||||
def buy_group(self):
|
||||
if not self.template:
|
||||
return
|
||||
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
|
||||
|
||||
if price > self.game.blue.budget:
|
||||
self.error_money()
|
||||
self.close()
|
||||
return
|
||||
else:
|
||||
self.game.blue.budget -= price
|
||||
|
||||
self.ground_object.groups = groups
|
||||
|
||||
# Replan redfor missions
|
||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def error_money(self):
|
||||
msg = QMessageBox()
|
||||
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,
|
||||
PointWithHeading.from_point(
|
||||
self.ground_object.position, self.ground_object.heading
|
||||
),
|
||||
self.ground_object.control_point,
|
||||
self.game,
|
||||
)
|
||||
return go.groups
|
||||
|
||||
|
||||
class QGroundObjectBuyMenu(QDialog):
|
||||
template_changed_signal = Signal(GroundObjectTemplate)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
ground_object: TheaterGroundObject,
|
||||
game: Game,
|
||||
current_group_value: int,
|
||||
):
|
||||
super(QGroundObjectBuyMenu, self).__init__(parent)
|
||||
|
||||
self.setMinimumWidth(350)
|
||||
|
||||
self.setWindowTitle("Buy ground object @ " + ground_object.obj_name)
|
||||
self.setWindowIcon(EVENT_ICONS["capture"])
|
||||
|
||||
self.mainLayout = QGridLayout()
|
||||
self.setLayout(self.mainLayout)
|
||||
|
||||
self.template_selector = QComboBox()
|
||||
self.template_selector.currentIndexChanged.connect(self.template_changed)
|
||||
|
||||
# Get the templates and fill the combobox
|
||||
template_sub_category = None
|
||||
if isinstance(ground_object, SamGroundObject):
|
||||
template_category = TemplateCategory.AirDefence
|
||||
elif isinstance(ground_object, VehicleGroupGroundObject):
|
||||
template_category = TemplateCategory.Armor
|
||||
elif isinstance(ground_object, EwrGroundObject):
|
||||
template_category = TemplateCategory.AirDefence
|
||||
template_sub_category = "EWR"
|
||||
else:
|
||||
raise RuntimeError
|
||||
|
||||
for template in game.blue.faction.templates.for_category(
|
||||
template_category, template_sub_category
|
||||
):
|
||||
self.template_selector.addItem(template.name, userData=template)
|
||||
|
||||
template_selector_layout = QGridLayout()
|
||||
template_selector_layout.addWidget(QLabel("Template :"), 0, 0, Qt.AlignLeft)
|
||||
template_selector_layout.addWidget(
|
||||
self.template_selector, 0, 1, alignment=Qt.AlignRight
|
||||
)
|
||||
self.mainLayout.addLayout(template_selector_layout, 0, 0)
|
||||
|
||||
self.template_layout = QGroundObjectTemplateLayout(
|
||||
game, ground_object, self.template_changed_signal, current_group_value
|
||||
)
|
||||
self.mainLayout.addWidget(self.template_layout, 1, 0)
|
||||
self.setLayout(self.mainLayout)
|
||||
|
||||
# Update UI
|
||||
self.template_changed()
|
||||
|
||||
def template_changed(self):
|
||||
template = self.template_selector.itemData(
|
||||
self.template_selector.currentIndex()
|
||||
)
|
||||
self.template_changed_signal.emit(template)
|
||||
@ -1,17 +1,12 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide2.QtGui import Qt
|
||||
from PySide2.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSpinBox,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from dcs import Point, vehicles
|
||||
@ -19,6 +14,7 @@ from dcs import Point, vehicles
|
||||
from game import Game
|
||||
from game.config import REWARDS
|
||||
from game.data.building_data import FORTIFICATION_BUILDINGS
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater import ControlPoint, TheaterGroundObject
|
||||
from game.theater.theatergroundobject import (
|
||||
@ -27,13 +23,11 @@ from game.theater.theatergroundobject import (
|
||||
SamGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
)
|
||||
from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
|
||||
from gen.sam.ewr_group_generator import get_faction_possible_ewrs_generator
|
||||
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
|
||||
from qt_ui.uiconstants import EVENT_ICONS
|
||||
from qt_ui.widgets.QBudgetBox import QBudgetBox
|
||||
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
|
||||
from qt_ui.windows.groundobject.QBuildingInfo import QBuildingInfo
|
||||
from qt_ui.windows.groundobject.QGroundObjectBuyMenu import QGroundObjectBuyMenu
|
||||
|
||||
|
||||
class QGroundObjectMenu(QDialog):
|
||||
@ -41,17 +35,12 @@ class QGroundObjectMenu(QDialog):
|
||||
self,
|
||||
parent,
|
||||
ground_object: TheaterGroundObject,
|
||||
buildings: Optional[List[TheaterGroundObject]],
|
||||
cp: ControlPoint,
|
||||
game: Game,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setMinimumWidth(350)
|
||||
self.ground_object = ground_object
|
||||
if buildings is None:
|
||||
self.buildings = []
|
||||
else:
|
||||
self.buildings = buildings
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.setWindowTitle(
|
||||
@ -231,225 +220,7 @@ class QGroundObjectMenu(QDialog):
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def buy_group(self):
|
||||
self.subwindow = QBuyGroupForGroundObjectDialog(
|
||||
self, self.ground_object, self.cp, self.game, self.total_value
|
||||
self.subwindow = QGroundObjectBuyMenu(
|
||||
self, self.ground_object, self.game, self.total_value
|
||||
)
|
||||
self.subwindow.show()
|
||||
|
||||
|
||||
class QBuyGroupForGroundObjectDialog(QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
ground_object: TheaterGroundObject,
|
||||
cp: ControlPoint,
|
||||
game: Game,
|
||||
current_group_value: int,
|
||||
):
|
||||
super(QBuyGroupForGroundObjectDialog, self).__init__(parent)
|
||||
|
||||
self.setMinimumWidth(350)
|
||||
self.ground_object = ground_object
|
||||
self.cp = cp
|
||||
self.game = game
|
||||
self.current_group_value = current_group_value
|
||||
|
||||
self.setWindowTitle("Buy units @ " + self.ground_object.obj_name)
|
||||
self.setWindowIcon(EVENT_ICONS["capture"])
|
||||
|
||||
self.buySamButton = QPushButton("Buy")
|
||||
self.buyArmorButton = QPushButton("Buy")
|
||||
self.buySamLayout = QGridLayout()
|
||||
self.buyArmorLayout = QGridLayout()
|
||||
self.amount = QSpinBox()
|
||||
self.buyArmorCombo = QComboBox()
|
||||
self.samCombo = QComboBox()
|
||||
self.buySamBox = QGroupBox("Buy SAM site :")
|
||||
self.buyArmorBox = QGroupBox("Buy defensive position :")
|
||||
|
||||
faction = self.game.blue.faction
|
||||
|
||||
# Sams
|
||||
|
||||
possible_sams = get_faction_possible_sams_generator(faction)
|
||||
for sam in possible_sams:
|
||||
# Pre Generate SAM to get the real price
|
||||
generator = sam(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
self.samCombo.addItem(
|
||||
generator.name + " [$" + str(generator.price) + "M]", userData=generator
|
||||
)
|
||||
self.samCombo.currentIndexChanged.connect(self.samComboChanged)
|
||||
|
||||
self.buySamLayout.addWidget(QLabel("Site Type :"), 0, 0, Qt.AlignLeft)
|
||||
self.buySamLayout.addWidget(self.samCombo, 0, 1, alignment=Qt.AlignRight)
|
||||
self.buySamLayout.addWidget(self.buySamButton, 1, 1, alignment=Qt.AlignRight)
|
||||
stretch = QVBoxLayout()
|
||||
stretch.addStretch()
|
||||
self.buySamLayout.addLayout(stretch, 2, 0)
|
||||
|
||||
self.buySamButton.clicked.connect(self.buySam)
|
||||
|
||||
# EWRs
|
||||
|
||||
buy_ewr_box = QGroupBox("Buy EWR:")
|
||||
buy_ewr_layout = QGridLayout()
|
||||
buy_ewr_box.setLayout(buy_ewr_layout)
|
||||
|
||||
buy_ewr_layout.addWidget(QLabel("Radar type:"), 0, 0, Qt.AlignLeft)
|
||||
|
||||
self.ewr_selector = QComboBox()
|
||||
buy_ewr_layout.addWidget(self.ewr_selector, 0, 1, alignment=Qt.AlignRight)
|
||||
ewr_types = get_faction_possible_ewrs_generator(faction)
|
||||
for ewr_type in ewr_types:
|
||||
# Pre Generate to get the real price
|
||||
generator = ewr_type(self.game, self.ground_object)
|
||||
generator.generate()
|
||||
self.ewr_selector.addItem(
|
||||
generator.name() + " [$" + str(generator.price) + "M]",
|
||||
userData=generator,
|
||||
)
|
||||
self.ewr_selector.currentIndexChanged.connect(self.on_ewr_selection_changed)
|
||||
|
||||
self.buy_ewr_button = QPushButton("Buy")
|
||||
self.buy_ewr_button.clicked.connect(self.buy_ewr)
|
||||
buy_ewr_layout.addWidget(self.buy_ewr_button, 1, 1, alignment=Qt.AlignRight)
|
||||
stretch = QVBoxLayout()
|
||||
stretch.addStretch()
|
||||
buy_ewr_layout.addLayout(stretch, 2, 0)
|
||||
|
||||
# Armored units
|
||||
for unit in set(faction.ground_units):
|
||||
self.buyArmorCombo.addItem(f"{unit} [${unit.price}M]", userData=unit)
|
||||
self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged)
|
||||
|
||||
self.amount.setMinimum(2)
|
||||
self.amount.setMaximum(8)
|
||||
self.amount.setValue(2)
|
||||
self.amount.valueChanged.connect(self.amountComboChanged)
|
||||
|
||||
self.buyArmorLayout.addWidget(QLabel("Unit type :"), 0, 0, Qt.AlignLeft)
|
||||
self.buyArmorLayout.addWidget(self.buyArmorCombo, 0, 1, alignment=Qt.AlignRight)
|
||||
self.buyArmorLayout.addWidget(
|
||||
QLabel("Group size :"), 1, 0, alignment=Qt.AlignLeft
|
||||
)
|
||||
self.buyArmorLayout.addWidget(self.amount, 1, 1, alignment=Qt.AlignRight)
|
||||
self.buyArmorLayout.addWidget(
|
||||
self.buyArmorButton, 2, 1, alignment=Qt.AlignRight
|
||||
)
|
||||
stretch2 = QVBoxLayout()
|
||||
stretch2.addStretch()
|
||||
self.buyArmorLayout.addLayout(stretch2, 3, 0)
|
||||
|
||||
self.buyArmorButton.clicked.connect(self.buyArmor)
|
||||
|
||||
# Do layout
|
||||
self.buySamBox.setLayout(self.buySamLayout)
|
||||
self.buyArmorBox.setLayout(self.buyArmorLayout)
|
||||
|
||||
self.mainLayout = QHBoxLayout()
|
||||
|
||||
if isinstance(self.ground_object, SamGroundObject):
|
||||
self.mainLayout.addWidget(self.buySamBox)
|
||||
elif isinstance(self.ground_object, VehicleGroupGroundObject):
|
||||
self.mainLayout.addWidget(self.buyArmorBox)
|
||||
elif isinstance(self.ground_object, EwrGroundObject):
|
||||
self.mainLayout.addWidget(buy_ewr_box)
|
||||
|
||||
self.setLayout(self.mainLayout)
|
||||
|
||||
try:
|
||||
self.samComboChanged(0)
|
||||
self.armorComboChanged(0)
|
||||
self.on_ewr_selection_changed(0)
|
||||
except:
|
||||
pass
|
||||
|
||||
def samComboChanged(self, index):
|
||||
self.buySamButton.setText(
|
||||
"Buy [$"
|
||||
+ str(self.samCombo.itemData(index).price)
|
||||
+ "M] [-$"
|
||||
+ str(self.current_group_value)
|
||||
+ "M]"
|
||||
)
|
||||
|
||||
def on_ewr_selection_changed(self, index):
|
||||
ewr = self.ewr_selector.itemData(index)
|
||||
self.buy_ewr_button.setText(
|
||||
f"Buy [${ewr.price}M][-${self.current_group_value}M]"
|
||||
)
|
||||
|
||||
def armorComboChanged(self, index):
|
||||
unit_type = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
|
||||
price = unit_type.price * self.amount.value()
|
||||
self.buyArmorButton.setText(f"Buy [${price}M][-${self.current_group_value}M]")
|
||||
|
||||
def amountComboChanged(self):
|
||||
unit_type = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
|
||||
price = unit_type.price * self.amount.value()
|
||||
self.buyArmorButton.setText(f"Buy [${price}M][-${self.current_group_value}M]")
|
||||
|
||||
def buyArmor(self):
|
||||
logging.info("Buying Armor ")
|
||||
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
|
||||
price = utype.price * self.amount.value() - self.current_group_value
|
||||
if price > self.game.blue.budget:
|
||||
self.error_money()
|
||||
self.close()
|
||||
return
|
||||
else:
|
||||
self.game.blue.budget -= price
|
||||
|
||||
# Generate Armor
|
||||
group = generate_armor_group_of_type_and_size(
|
||||
self.game, self.ground_object, utype, int(self.amount.value())
|
||||
)
|
||||
self.ground_object.groups = [group]
|
||||
|
||||
# Replan redfor missions
|
||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def buySam(self):
|
||||
sam_generator = self.samCombo.itemData(self.samCombo.currentIndex())
|
||||
price = sam_generator.price - self.current_group_value
|
||||
if price > self.game.blue.budget:
|
||||
self.error_money()
|
||||
return
|
||||
else:
|
||||
self.game.blue.budget -= price
|
||||
|
||||
self.ground_object.groups = list(sam_generator.groups)
|
||||
|
||||
# Replan redfor missions
|
||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def buy_ewr(self):
|
||||
ewr_generator = self.ewr_selector.itemData(self.ewr_selector.currentIndex())
|
||||
price = ewr_generator.price - self.current_group_value
|
||||
if price > self.game.blue.budget:
|
||||
self.error_money()
|
||||
return
|
||||
else:
|
||||
self.game.blue.budget -= price
|
||||
|
||||
self.ground_object.groups = [ewr_generator.vg]
|
||||
|
||||
# Replan redfor missions
|
||||
self.game.initialize_turn(for_red=True, for_blue=False)
|
||||
|
||||
GameUpdateSignal.get_instance().updateGame(self.game)
|
||||
|
||||
def error_money(self):
|
||||
msg = QMessageBox()
|
||||
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()
|
||||
|
||||
Binary file not shown.
@ -1,59 +0,0 @@
|
||||
import pickle
|
||||
import typing
|
||||
|
||||
from dcs.mission import Mission
|
||||
from dcs.mapping import Point
|
||||
from dcs.unit import *
|
||||
from dcs.statics import warehouse_map, fortification_map
|
||||
|
||||
|
||||
def load_templates():
|
||||
temp_mis = Mission()
|
||||
temp_mis.load_file("resources/tools/groundobject_templates.miz")
|
||||
|
||||
groups = {} # type: typing.Dict[str, typing.Dict[int, typing.List[Static]]]
|
||||
|
||||
for static_group in (
|
||||
temp_mis.country("USA").static_group
|
||||
+ temp_mis.country("USAF Aggressors").static_group
|
||||
):
|
||||
for static in static_group.units:
|
||||
static_name = str(static.name).split()[0]
|
||||
tpl_name, tpl_idx = static_name[:-1], int(static_name[-1])
|
||||
|
||||
groups[tpl_name] = groups.get(tpl_name, {})
|
||||
groups[tpl_name][tpl_idx] = groups[tpl_name].get(tpl_idx, [])
|
||||
groups[tpl_name][tpl_idx].append(static)
|
||||
|
||||
tpls = {name: {idx: [] for idx in groups[name].keys()} for name in groups.keys()}
|
||||
for category_name, category_groups in groups.items():
|
||||
for idx, static_groups in category_groups.items():
|
||||
dist = -1
|
||||
a, b = None, None
|
||||
for aa in static_groups:
|
||||
for bb in static_groups:
|
||||
if aa.position.distance_to_point(bb.position) > dist:
|
||||
dist = aa.position.distance_to_point(bb.position)
|
||||
a = aa
|
||||
b = bb
|
||||
|
||||
center = a.position.point_from_heading(
|
||||
a.position.heading_between_point(b.position), dist / 2
|
||||
)
|
||||
for static in static_groups:
|
||||
tpls[category_name][idx].append(
|
||||
{
|
||||
"type": static.type,
|
||||
"offset": Point(
|
||||
center.x - static.position.x, center.y - static.position.y
|
||||
),
|
||||
"heading": static.heading,
|
||||
}
|
||||
)
|
||||
|
||||
tpls["aa"] = {0: [{"type": "AA", "offset": Point(0, 0), "heading": 0}]}
|
||||
return tpls
|
||||
|
||||
|
||||
with open("resources/groundobject_templates.p", "wb") as f:
|
||||
pickle.dump(load_templates(), f)
|
||||
785
resources/tools/template_helper.py
Normal file
785
resources/tools/template_helper.py
Normal file
@ -0,0 +1,785 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any, Iterator
|
||||
|
||||
from tabulate import tabulate
|
||||
|
||||
import dcs
|
||||
from dcs import Point
|
||||
|
||||
from game import Game
|
||||
from game.campaignloader import CampaignAirWingConfig
|
||||
from game.db import FACTIONS
|
||||
from game.missiongenerator.tgogenerator import (
|
||||
GroundObjectGenerator,
|
||||
)
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.settings import Settings
|
||||
from game.theater import CaucasusTheater, OffMapSpawn
|
||||
from game.unitmap import UnitMap
|
||||
from game.utils import Heading
|
||||
from gen.to_remove.armored_group_generator import (
|
||||
FixedSizeArmorGroupGenerator,
|
||||
FixedSizeArmorGroupGeneratorWithAA,
|
||||
)
|
||||
from gen.to_remove.carrier_group import (
|
||||
CarrierGroupGenerator,
|
||||
CarrierStrikeGroup8Generator,
|
||||
)
|
||||
from gen.to_remove.lha_group import LHAGroupGenerator
|
||||
from gen.to_remove.ship_group_generator import SHIP_MAP
|
||||
from gen.to_remove.coastal_group_generator import COASTAL_MAP
|
||||
from gen.templates import (
|
||||
GroundObjectTemplates,
|
||||
TemplateCategory,
|
||||
GroupTemplate,
|
||||
UnitTemplate,
|
||||
TemplateEncoder,
|
||||
MissileTemplate,
|
||||
BuildingTemplate,
|
||||
CoastalTemplate,
|
||||
NavalTemplate,
|
||||
TemplateRandomizer,
|
||||
ArmorTemplate,
|
||||
TEMPLATE_TYPES,
|
||||
AirDefenceTemplate,
|
||||
)
|
||||
from gen.to_remove.missiles_group_generator import MISSILES_MAP
|
||||
from gen.to_remove.airdefensegroupgenerator import AirDefenseGroupGenerator
|
||||
from gen.to_remove.ewr_group_generator import EWR_MAP
|
||||
from gen.to_remove.ewrs import EwrGenerator
|
||||
from gen.to_remove.sam_group_generator import SAM_MAP
|
||||
from gen.naming import namegen
|
||||
from qt_ui import liberation_install
|
||||
|
||||
|
||||
TEMPLATES_MIZ = "resources/templates/templates.miz"
|
||||
MIGRATE_MIZ = "resources/tools/groundobject_templates.miz"
|
||||
TEMPLATES_BLOB = "resources/templates/templates.json"
|
||||
TEMPLATES_MAP = "resources/templates/template_map.json"
|
||||
TABLE_FILE = "doc/templates/template_list.md"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupTemplateMapping:
|
||||
name: str
|
||||
optional: bool
|
||||
unit_types: list[str]
|
||||
unit_classes: list[UnitClass]
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
d = self.__dict__
|
||||
if not self.optional:
|
||||
d.pop("optional")
|
||||
if not self.unit_types:
|
||||
d.pop("unit_types")
|
||||
if not self.unit_classes:
|
||||
d.pop("unit_classes")
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, Any]) -> GroupTemplateMapping:
|
||||
optional = d["optional"] if "optional" in d else False
|
||||
return GroupTemplateMapping(
|
||||
d["name"], optional, d["unit_types"], d["unit_classes"]
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TemplateMapping:
|
||||
name: str
|
||||
template_type: str
|
||||
description: str
|
||||
groups: list[GroupTemplateMapping]
|
||||
statics: list[str]
|
||||
static_mapping: Optional[GroupTemplateMapping] = None
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
d = self.__dict__
|
||||
if not self.static_mapping:
|
||||
d.pop("static_mapping")
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def from_dict(d: dict[str, Any]) -> TemplateMapping:
|
||||
groups = [GroupTemplateMapping.from_dict(group) for group in d["groups"]]
|
||||
static_mapping = (
|
||||
GroupTemplateMapping.from_dict(d["static_mapping"])
|
||||
if "static_mapping" in d
|
||||
else None
|
||||
)
|
||||
return TemplateMapping(
|
||||
d["name"],
|
||||
d["template_type"],
|
||||
d["description"],
|
||||
groups,
|
||||
d["statics"],
|
||||
static_mapping,
|
||||
)
|
||||
|
||||
|
||||
class TemplateMap:
|
||||
_map: dict[str, list[TemplateMapping]] = {}
|
||||
|
||||
def __init__(self):
|
||||
self._map = {}
|
||||
|
||||
def __getitem__(self, item: str) -> list[TemplateMapping]:
|
||||
return self._map[item]
|
||||
|
||||
def __setitem__(self, key, value) -> None:
|
||||
self._map[key] = value
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
return self._map
|
||||
|
||||
def mapping_for_template(self, template_name: str) -> tuple[str, TemplateMapping]:
|
||||
for category, mappings in self._map.items():
|
||||
for mapping in mappings:
|
||||
if mapping.name == template_name:
|
||||
return category, mapping
|
||||
raise RuntimeError(f"No mapping for template {template_name}")
|
||||
|
||||
def mapping_for_group(
|
||||
self, group_name: str
|
||||
) -> tuple[str, TemplateMapping, Optional[GroupTemplateMapping]]:
|
||||
for category, mappings in self._map.items():
|
||||
for mapping in mappings:
|
||||
for group_mapping in mapping.groups:
|
||||
if group_mapping.name == group_name:
|
||||
return category, mapping, group_mapping
|
||||
if group_name in mapping.statics:
|
||||
return category, mapping, mapping.static_mapping
|
||||
raise RuntimeError(f"No mapping for group {group_name}")
|
||||
|
||||
@property
|
||||
def mappings(self) -> Iterator[TemplateMapping]:
|
||||
for mapping in self._map.values():
|
||||
yield mapping
|
||||
|
||||
|
||||
def import_templates(
|
||||
miz_file: str, template_map_file: str, target_file: str, table_file: str
|
||||
) -> None:
|
||||
"""Imports the template miz and the template_map as json into liberation"""
|
||||
temp_mis = dcs.Mission()
|
||||
temp_mis.load_file(miz_file)
|
||||
|
||||
templates = GroundObjectTemplates()
|
||||
|
||||
with open(template_map_file, "r") as f:
|
||||
template_map_json = json.load(f)
|
||||
|
||||
template_map = TemplateMap()
|
||||
for name, mappings in template_map_json.items():
|
||||
template_map[TemplateCategory(name)] = []
|
||||
for mapping in mappings:
|
||||
template_map[TemplateCategory(name)].append(
|
||||
TemplateMapping.from_dict(mapping)
|
||||
)
|
||||
|
||||
template_position: dict[str, Point] = {}
|
||||
for static_group in temp_mis.country("USA").static_group:
|
||||
category, mapping, group_mapping = template_map.mapping_for_group(
|
||||
static_group.name
|
||||
)
|
||||
template = templates.by_category_and_name(
|
||||
TemplateCategory(category), mapping.name
|
||||
)
|
||||
static_template = None
|
||||
if not template:
|
||||
template = BuildingTemplate(
|
||||
mapping.name, mapping.template_type, mapping.description
|
||||
)
|
||||
static_template = GroupTemplate(mapping.name, [], True)
|
||||
if group_mapping:
|
||||
static_template.randomizer = group_mapping.randomizer
|
||||
static_template.optional = group_mapping.optional
|
||||
template.groups.append(static_template)
|
||||
templates.add_template(TemplateCategory(category), template)
|
||||
|
||||
else:
|
||||
for group in template.groups:
|
||||
if mapping.name in group.name:
|
||||
static_template = group
|
||||
|
||||
if not static_template:
|
||||
raise RuntimeError(f"No mapping for group {static_group.name}")
|
||||
|
||||
for i, unit in enumerate(static_group.units):
|
||||
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]
|
||||
)
|
||||
static_template.units.append(unit_template)
|
||||
pass
|
||||
|
||||
for vehicle_group in (
|
||||
temp_mis.country("USA").vehicle_group + temp_mis.country("USA").ship_group
|
||||
):
|
||||
category, mapping, group_mapping = template_map.mapping_for_group(
|
||||
vehicle_group.name
|
||||
)
|
||||
template = templates.by_category_and_name(
|
||||
TemplateCategory(category), mapping.name
|
||||
)
|
||||
if not template:
|
||||
template = TEMPLATE_TYPES[TemplateCategory(category)](
|
||||
mapping.name, mapping.template_type, mapping.description
|
||||
)
|
||||
templates.add_template(TemplateCategory(category), template)
|
||||
for i, unit in enumerate(vehicle_group.units):
|
||||
group_template = None
|
||||
for group in template.groups:
|
||||
if group.name == vehicle_group.name:
|
||||
group_template = group
|
||||
if not group_template:
|
||||
group_template = GroupTemplate(
|
||||
vehicle_group.name,
|
||||
[],
|
||||
False,
|
||||
group_mapping.randomizer,
|
||||
group_mapping.optional,
|
||||
)
|
||||
template.groups.append(group_template)
|
||||
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)
|
||||
pass
|
||||
|
||||
with open(target_file, "w") as f:
|
||||
json.dump(templates.to_json(), f, cls=TemplateEncoder, indent="\t")
|
||||
|
||||
# print Table
|
||||
print_table(target_file, table_file)
|
||||
|
||||
|
||||
def export_templates(
|
||||
templates_file: str, miz_file: str, template_map_file: str
|
||||
) -> None:
|
||||
"""Exports the templates.json to the templates.miz and the mapping"""
|
||||
# This function will only be used on mass jobs or specific fine tuning
|
||||
|
||||
templates = GroundObjectTemplates.from_json(templates_file)
|
||||
|
||||
# Prepare game
|
||||
theater = CaucasusTheater()
|
||||
initial_ground_position = Point(-500000, 250000)
|
||||
initial_water_position = Point(-350000, 250000)
|
||||
control_point_ground = OffMapSpawn(1, "Spawn Ground", initial_ground_position, True)
|
||||
control_point_water = OffMapSpawn(2, "Spawn Water", initial_water_position, True)
|
||||
theater.add_controlpoint(control_point_ground)
|
||||
theater.add_controlpoint(control_point_water)
|
||||
|
||||
game = Game(
|
||||
FACTIONS["Bluefor Modern"],
|
||||
FACTIONS["Russia 2010"],
|
||||
theater,
|
||||
CampaignAirWingConfig({control_point_ground: [], control_point_water: []}),
|
||||
datetime.today(),
|
||||
Settings(),
|
||||
10000,
|
||||
10000,
|
||||
)
|
||||
|
||||
m = dcs.Mission(game.theater.terrain)
|
||||
country = m.country("USA")
|
||||
unit_map = UnitMap()
|
||||
|
||||
template_map = TemplateMap()
|
||||
offset_x = 0
|
||||
offset_y = 0
|
||||
|
||||
for count, template_category in enumerate(TemplateCategory):
|
||||
template_map[template_category.value] = []
|
||||
temmplates_for_category = list(templates.for_category(template_category))
|
||||
|
||||
# Define the offset / separation
|
||||
category_separation = 10000
|
||||
group_separation = 5000
|
||||
|
||||
if template_category in [TemplateCategory.Naval]:
|
||||
initial_position = initial_water_position
|
||||
control_point = control_point_water
|
||||
else:
|
||||
initial_position = initial_ground_position
|
||||
control_point = control_point_ground
|
||||
|
||||
current_separation = offset_y + category_separation
|
||||
offset_x = 0
|
||||
offset_y = current_separation
|
||||
max_cols = int(math.sqrt(len(temmplates_for_category)))
|
||||
for template in temmplates_for_category:
|
||||
mapping = TemplateMapping(
|
||||
template.name,
|
||||
template.template_type,
|
||||
template.description,
|
||||
[],
|
||||
[],
|
||||
)
|
||||
position = Point(
|
||||
initial_position.x + offset_x, initial_position.y + offset_y
|
||||
)
|
||||
|
||||
# Initialize the template
|
||||
for group_template in template.groups:
|
||||
if group_template.randomizer:
|
||||
group_template.randomizer.init_randomization_for_faction(
|
||||
game.blue.faction
|
||||
)
|
||||
|
||||
ground_object = template.generate(
|
||||
template.name,
|
||||
PointWithHeading.from_point(position, Heading.from_degrees(0)),
|
||||
control_point,
|
||||
game,
|
||||
False, # Do not use randomization during export!
|
||||
)
|
||||
|
||||
for g_id, group in enumerate(ground_object.groups):
|
||||
group.name = f"{template.name} {g_id}"
|
||||
for u_id, unit in enumerate(group.units):
|
||||
unit.name = f"{template.name} {g_id}-{u_id}"
|
||||
group_template = template.groups[g_id]
|
||||
group_mapping = GroupTemplateMapping(
|
||||
group.name, group_template.optional, group_template.randomizer
|
||||
)
|
||||
if group.static_group:
|
||||
for unit in group.units:
|
||||
mapping.statics.append(unit.name)
|
||||
# No mapping as we have no randomization yet
|
||||
else:
|
||||
mapping.groups.append(group_mapping)
|
||||
|
||||
generator = GroundObjectGenerator(ground_object, country, game, m, unit_map)
|
||||
generator.generate(unique_name=False) # Prevent the ID prefix
|
||||
|
||||
if ((offset_y - current_separation) / group_separation) < max_cols:
|
||||
offset_y += group_separation
|
||||
else:
|
||||
offset_y = current_separation
|
||||
offset_x += group_separation
|
||||
|
||||
template_map[template_category.name].append(mapping)
|
||||
# Dump the template_map as json
|
||||
with open(template_map_file, "w") as f:
|
||||
json.dump(template_map, f, cls=TemplateEncoder, sort_keys=True, indent="\t")
|
||||
|
||||
m.save(miz_file)
|
||||
|
||||
|
||||
def print_table(templates_file: str, table_file: str) -> None:
|
||||
# Category # Sub Category # Template Name # Unit_types
|
||||
|
||||
templates = GroundObjectTemplates.from_json(templates_file)
|
||||
table = []
|
||||
|
||||
for template_category in TemplateCategory:
|
||||
category_templates = list(templates.for_category(template_category))
|
||||
for template in sorted(
|
||||
category_templates, key=lambda x: (x.template_type, x.name)
|
||||
):
|
||||
groups = []
|
||||
options = []
|
||||
for group in template.groups:
|
||||
units = []
|
||||
group_options = []
|
||||
for unit in group.units:
|
||||
units.append(unit.type)
|
||||
groups.append("<li>" + ", ".join(units) + "</li>")
|
||||
if group.randomizer:
|
||||
# Add More info about the randomizer?
|
||||
group_options.append("Randomizer")
|
||||
if group.static:
|
||||
group_options.append("Static")
|
||||
if group_options:
|
||||
options.append("<li>" + ", ".join(group_options) + "</li>")
|
||||
table.append(
|
||||
[
|
||||
template_category.value,
|
||||
template.template_type,
|
||||
template.name,
|
||||
"<ul>" + "".join(groups) + "</ul>",
|
||||
"<ul>" + "".join(options) + "</ul>",
|
||||
]
|
||||
)
|
||||
|
||||
table_str = tabulate(
|
||||
table,
|
||||
headers=["Category", "Sub-Category", "Template", "Groups & Units", "Options"],
|
||||
tablefmt="github",
|
||||
)
|
||||
|
||||
with open(table_file, "w", encoding="utf-8") as fdata:
|
||||
fdata.write(table_str)
|
||||
|
||||
|
||||
def replace(string: str, replacement) -> str:
|
||||
for old_name, new_name in replacement:
|
||||
string = string.replace(old_name, new_name)
|
||||
return string
|
||||
|
||||
|
||||
def update_factions(generator_names) -> None:
|
||||
folder: Path = Path("./resources/factions/")
|
||||
factions = [f for f in folder.glob("*.json") if f.is_file()]
|
||||
for f in factions:
|
||||
with open(f, "r", encoding="utf-8") as fdata:
|
||||
data = json.load(fdata)
|
||||
|
||||
updated_faction = {}
|
||||
for key, item in data.items():
|
||||
if isinstance(item, list):
|
||||
updated = []
|
||||
for string in item:
|
||||
updated.append(replace(string, generator_names))
|
||||
updated_faction[key] = updated
|
||||
elif isinstance(item, str):
|
||||
updated_faction[key] = replace(item, generator_names)
|
||||
else:
|
||||
updated_faction[key] = item
|
||||
|
||||
with open(f, "w", encoding="utf-8") as fdata:
|
||||
json.dump(updated_faction, fdata, indent=2)
|
||||
|
||||
print("\n \n Faction Updates:")
|
||||
print(
|
||||
tabulate(
|
||||
generator_names,
|
||||
headers=["Previous Value", "New Value"],
|
||||
tablefmt="github",
|
||||
)
|
||||
)
|
||||
|
||||
# def migrate_generators_to_templates(
|
||||
# input_miz: str,
|
||||
# templates_file: str,
|
||||
# miz_file: str,
|
||||
# template_map_file: str,
|
||||
# table_file: str,
|
||||
# ) -> None:
|
||||
#
|
||||
# templates = GroundObjectTemplates()
|
||||
#
|
||||
# theater = CaucasusTheater()
|
||||
#
|
||||
# initial_position = Point(0, 0)
|
||||
# control_point = OffMapSpawn(1, "Spawn", initial_position, True)
|
||||
# theater.add_controlpoint(control_point)
|
||||
#
|
||||
# game = Game(
|
||||
# FACTIONS["Bluefor Modern"],
|
||||
# FACTIONS["Russia 2010"],
|
||||
# theater,
|
||||
# CampaignAirWingConfig({control_point: []}),
|
||||
# datetime.today(),
|
||||
# Settings(),
|
||||
# 10000,
|
||||
# 10000,
|
||||
# )
|
||||
#
|
||||
# generators: dict[TemplateCategory, dict[str, Any]] = {
|
||||
# TemplateCategory.AirDefence: SAM_MAP,
|
||||
# TemplateCategory.Naval: SHIP_MAP,
|
||||
# TemplateCategory.Missile: MISSILES_MAP,
|
||||
# TemplateCategory.Coastal: COASTAL_MAP,
|
||||
# TemplateCategory.Armor: {},
|
||||
# }
|
||||
#
|
||||
# # Only use one EWR generator. The differnt units will be placed as randomizer
|
||||
# generators[TemplateCategory.AirDefence]["EWRGenerator"] = EwrGenerator
|
||||
#
|
||||
# generators[TemplateCategory.Naval]["CarrierGroupGenerator"] = CarrierGroupGenerator
|
||||
# generators[TemplateCategory.Naval][
|
||||
# "CarrierStrikeGroup8Generator"
|
||||
# ] = CarrierStrikeGroup8Generator
|
||||
# generators[TemplateCategory.Naval]["LHAGroupGenerator"] = LHAGroupGenerator
|
||||
# generators[TemplateCategory.Armor][
|
||||
# "RandomArmorGroup"
|
||||
# ] = FixedSizeArmorGroupGenerator
|
||||
# generators[TemplateCategory.Armor][
|
||||
# "RandomArmorGroupWithAA"
|
||||
# ] = FixedSizeArmorGroupGeneratorWithAA
|
||||
#
|
||||
# generator_names = []
|
||||
#
|
||||
# for category, template_generators in generators.items():
|
||||
# for generator_name, generator_class in template_generators.items():
|
||||
# # Just reuse SamGroundObject to make it easy
|
||||
# ground_object = SamGroundObject(
|
||||
# namegen.random_objective_name(),
|
||||
# initial_position,
|
||||
# control_point,
|
||||
# )
|
||||
#
|
||||
# if category in (
|
||||
# TemplateCategory.Naval,
|
||||
# TemplateCategory.Missile,
|
||||
# TemplateCategory.Coastal,
|
||||
# ):
|
||||
# generator = generator_class(game, ground_object, game.blue.faction)
|
||||
# elif category == TemplateCategory.Armor:
|
||||
# unit_type = next(
|
||||
# GroundUnitType.for_dcs_type(dcs.vehicles.Armor.M_1_Abrams)
|
||||
# )
|
||||
# generator = generator_class(
|
||||
# game,
|
||||
# ground_object,
|
||||
# unit_type,
|
||||
# # Create a group of 8 Armored Vehicles
|
||||
# 8,
|
||||
# )
|
||||
# else:
|
||||
# generator = generator_class(game, ground_object)
|
||||
#
|
||||
# # Generate the DCS Groups
|
||||
# generator.generate()
|
||||
#
|
||||
# if isinstance(generator, EwrGenerator):
|
||||
# template = AirDefenceTemplate("Early-Warning Radar", "EWR")
|
||||
# elif isinstance(generator, AirDefenseGroupGenerator):
|
||||
# template = AirDefenceTemplate(generator.name, generator.range().name)
|
||||
# elif generator_name in MISSILES_MAP:
|
||||
# template = MissileTemplate(generator_name, category.name)
|
||||
# elif generator_name in COASTAL_MAP:
|
||||
# template = CoastalTemplate(generator_name, category.name)
|
||||
# elif category == TemplateCategory.Naval:
|
||||
# if generator_name == "CarrierGroupGenerator":
|
||||
# template = NavalTemplate("Carrier Group", "carrier")
|
||||
# elif generator_name == "CarrierStrikeGroup8Generator":
|
||||
# template = NavalTemplate("Carrier Strike Group 8", "carrier")
|
||||
# elif generator_name == "LHAGroupGenerator":
|
||||
# template = NavalTemplate("LHA Group", "lha")
|
||||
# else:
|
||||
# template = NavalTemplate(generator_name, "ship")
|
||||
# elif category == TemplateCategory.Armor:
|
||||
# if generator_name == "RandomArmorGroup":
|
||||
# template = ArmorTemplate("Armor Group", "armor")
|
||||
# elif generator_name == "RandomArmorGroupWithAA":
|
||||
# template = ArmorTemplate("Armor Group with Anti-Air", "armor_aa")
|
||||
# else:
|
||||
# raise RuntimeError("Generator handling missing")
|
||||
#
|
||||
# groups = list(generator.groups)
|
||||
#
|
||||
# # These have to be identical!
|
||||
# for i, group in enumerate(groups):
|
||||
# for j, unit in enumerate(group.units):
|
||||
# unit.name = f"{template.name} {i}-{j}"
|
||||
#
|
||||
# group_template = GroupTemplate(
|
||||
# f"{template.name} #{str(i)}",
|
||||
# [UnitTemplate.from_unit(unit) for unit in group.units],
|
||||
# )
|
||||
#
|
||||
# if generator_name in [
|
||||
# "CarrierGroupGenerator",
|
||||
# "CarrierStrikeGroup8Generator",
|
||||
# ]:
|
||||
# if i == 0:
|
||||
# group_template.randomizer = TemplateRandomizer(
|
||||
# [0], 1, ["aircraft_carrier"]
|
||||
# )
|
||||
# elif i == 1:
|
||||
# count = (
|
||||
# 5 if generator_name == "CarrierStrikeGroup8Generator" else 4
|
||||
# )
|
||||
# group_template.randomizer = TemplateRandomizer(
|
||||
# [], count, ["destroyers"]
|
||||
# )
|
||||
# elif generator_name == "LHAGroupGenerator":
|
||||
# if i == 0:
|
||||
# group_template.randomizer = TemplateRandomizer(
|
||||
# [0], 1, ["helicopter_carrier"]
|
||||
# )
|
||||
# elif i == 1:
|
||||
# group_template.randomizer = TemplateRandomizer(
|
||||
# [], 2, ["destroyers"]
|
||||
# )
|
||||
# elif generator_name == "RandomArmorGroup" and i == 0:
|
||||
# group_template.randomizer = TemplateRandomizer(
|
||||
# [],
|
||||
# [2, 6],
|
||||
# ["frontline_units"],
|
||||
# ["APC", "ATGM", "IFV", "Tank"],
|
||||
# )
|
||||
# elif generator_name == "RandomArmorGroupWithAA":
|
||||
# if i == 0:
|
||||
# group_template.randomizer = TemplateRandomizer(
|
||||
# [],
|
||||
# [2, 6],
|
||||
# ["frontline_units"],
|
||||
# ["APC", "ATGM", "IFV", "Tank"],
|
||||
# )
|
||||
# elif i == 1:
|
||||
# group_template.randomizer = TemplateRandomizer(
|
||||
# [],
|
||||
# [1, 2],
|
||||
# ["frontline_units", "infantry_units"],
|
||||
# ["SHORADS", "MANPADS"],
|
||||
# )
|
||||
# group_template.optional = True
|
||||
# elif generator_name == "EWRGenerator" and i == 0:
|
||||
# for ewr_generator_name, ewr_generator in EWR_MAP.items():
|
||||
# unit_type = next(
|
||||
# GroundUnitType.for_dcs_type(ewr_generator.unit_type)
|
||||
# )
|
||||
# # Update all factions from generator to unit_type
|
||||
# generator_names.append(
|
||||
# [ewr_generator.unit_type.name, str(unit_type)]
|
||||
# )
|
||||
# # Update old generator names
|
||||
# generator_names.append([ewr_generator_name, str(unit_type)])
|
||||
#
|
||||
# group_template.randomizer = TemplateRandomizer(
|
||||
# [],
|
||||
# 1,
|
||||
# ["ewrs"], # Randomization based on faction template
|
||||
# )
|
||||
#
|
||||
# template.groups.append(group_template)
|
||||
#
|
||||
# templates.add_template(category, template)
|
||||
# generator_names.append([generator_name, template.name])
|
||||
#
|
||||
# # Load the basic templates
|
||||
# temp_mis = dcs.Mission()
|
||||
# temp_mis.load_file(input_miz)
|
||||
#
|
||||
# position_for_template: dict[str, Point] = {}
|
||||
# group_for_template: dict[str, GroupTemplate] = {}
|
||||
# for static_group in (
|
||||
# temp_mis.country("USA").static_group
|
||||
# + temp_mis.country("USAF Aggressors").static_group
|
||||
# ):
|
||||
# # Naming is: fob1 #001 -> name: fob1, category fob, group_name: fob1 #001
|
||||
# template_name = str(static_group.name).split()[0]
|
||||
# category_name, idx = template_name[:-1], int(template_name[-1])
|
||||
#
|
||||
# template = templates.by_name(template_name)
|
||||
# if not template:
|
||||
# template = BuildingTemplate(template_name, category_name)
|
||||
#
|
||||
# # Store original position to make the template relative to TGO later
|
||||
# position_for_template[template_name] = static_group.position
|
||||
#
|
||||
# # Create Group Template for the satic group. Within Liberation we map
|
||||
# # static units in groups even if dcs can not handle this. The dcs specific
|
||||
# # handling will happpen later in miz generation again.
|
||||
# group_template = GroupTemplate(f"{template.name}", [], True)
|
||||
# group_for_template[template_name] = group_template
|
||||
# template.groups.append(group_template)
|
||||
#
|
||||
# templates.add_template(TemplateCategory.Building, template)
|
||||
#
|
||||
# for u_id, unit in enumerate(static_group.units):
|
||||
# unit_template = UnitTemplate.from_unit(unit)
|
||||
# unit_template.position = Point(
|
||||
# int(unit_template.position.x - position_for_template[template_name].x),
|
||||
# int(unit_template.position.y - position_for_template[template_name].y),
|
||||
# )
|
||||
# group_for_template[template_name].units.append(unit_template)
|
||||
#
|
||||
# # Dump the template_map as json
|
||||
# with open(templates_file, "w") as f:
|
||||
# json.dump(templates.to_json(), f, cls=TemplateEncoder, indent=4)
|
||||
#
|
||||
# # Update Faction files
|
||||
# update_factions(generator_names)
|
||||
#
|
||||
|
||||
print(
|
||||
"Migrate successful. To finish the migration also run export followed by an import again."
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
my_group = parser.add_mutually_exclusive_group(required=True)
|
||||
my_group.add_argument(
|
||||
"-i",
|
||||
"--import",
|
||||
dest="Import",
|
||||
help="Imports the templates from the miz into liberation",
|
||||
action="store_true",
|
||||
)
|
||||
my_group.add_argument(
|
||||
"-e",
|
||||
"--export",
|
||||
dest="Export",
|
||||
help="Exports the templates from liberation to the miz",
|
||||
action="store_true",
|
||||
)
|
||||
my_group.add_argument(
|
||||
"-m",
|
||||
"--migrate",
|
||||
dest="Migrate",
|
||||
help="Migrates generators and the current ground object templates to the new system",
|
||||
action="store_true",
|
||||
)
|
||||
my_group.add_argument(
|
||||
"-t",
|
||||
"--table",
|
||||
dest="Table",
|
||||
help="Prints a table of all templates",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--template_miz", default=TEMPLATES_MIZ, help="Set the template mission (.miz)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target_file", default=TEMPLATES_BLOB, help="Set the target file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--template_map", default=TEMPLATES_MAP, help="Set the template map"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--migrate_file", default=MIGRATE_MIZ, help="Set the migrate mission"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--table_file",
|
||||
default=TABLE_FILE,
|
||||
help="Output file for template documentation",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
liberation_install.init()
|
||||
|
||||
miz_file = args.template_miz
|
||||
target_file = args.target_file
|
||||
migrate_file = args.migrate_file
|
||||
template_map = args.template_map
|
||||
table_file = args.table_file
|
||||
|
||||
if args.Import:
|
||||
import_templates(miz_file, template_map, target_file, table_file)
|
||||
elif args.Export:
|
||||
export_templates(target_file, miz_file, template_map)
|
||||
elif args.Table:
|
||||
print_table(target_file, table_file)
|
||||
elif args.Migrate:
|
||||
migrate_generators_to_templates(
|
||||
migrate_file, target_file, miz_file, template_map, table_file
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user