Merge branch 'theater-refactor' into develop

This commit is contained in:
Dan Albert 2020-11-07 13:39:35 -08:00
commit ee113d080e
17 changed files with 265 additions and 232 deletions

View File

@ -87,8 +87,8 @@ class Debriefing:
for i, ground_object in enumerate(cp.ground_objects): for i, ground_object in enumerate(cp.ground_objects):
logging.info(unit) logging.info(unit)
logging.info(ground_object.string_identifier) logging.info(ground_object.group_name)
if ground_object.matches_string_identifier(unit): if ground_object.is_same_group(unit):
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier) unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
self.dead_buildings.append(unit) self.dead_buildings.append(unit)
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]: elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:

View File

@ -145,8 +145,8 @@ class Event:
if ground_object.is_dead: if ground_object.is_dead:
continue continue
if ground_object.matches_string_identifier(destroyed_ground_unit_name): if ground_object.is_same_group(destroyed_ground_unit_name):
logging.info("cp {} killing ground object {}".format(cp, ground_object.string_identifier)) logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
cp.ground_objects[i].is_dead = True cp.ground_objects[i].is_dead = True
info = Information("Building destroyed", info = Information("Building destroyed",

View File

@ -1298,7 +1298,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
target_group = self.package.target target_group = self.package.target
if isinstance(target_group, TheaterGroundObject): if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_identifier, search="match") # Match search is used due to TheaterGroundObject.name not matching tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
if tgroup is not None: # the Mission group name because of SkyNet prefixes. if tgroup is not None: # the Mission group name because of SkyNet prefixes.
task = AttackGroup(tgroup.id) task = AttackGroup(tgroup.id)
task.params["expend"] = "All" task.params["expend"] = "All"
@ -1309,7 +1309,7 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
task.params["groupAttack"] = True task.params["groupAttack"] = True
waypoint.tasks.append(task) waypoint.tasks.append(task)
else: else:
logging.error(f"Could not find group for DEAD mission {target_group.group_identifier}") logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
for i, t in enumerate(self.waypoint.targets): for i, t in enumerate(self.waypoint.targets):
if self.group.units[0].unit_type == JF_17 and i < 4: if self.group.units[0].unit_type == JF_17 and i < 4:
@ -1327,7 +1327,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
target_group = self.package.target target_group = self.package.target
if isinstance(target_group, TheaterGroundObject): if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_identifier, search="match") # Match search is used due to TheaterGroundObject.name not matching tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
if tgroup is not None: # the Mission group name because of SkyNet prefixes. if tgroup is not None: # the Mission group name because of SkyNet prefixes.
waypoint.add_task(EngageTargetsInZone( waypoint.add_task(EngageTargetsInZone(
position=tgroup.position, position=tgroup.position,
@ -1337,7 +1337,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
]) ])
) )
else: else:
logging.error(f"Could not find group for DEAD mission {target_group.group_identifier}") logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
for i, t in enumerate(self.waypoint.targets): for i, t in enumerate(self.waypoint.targets):
if self.group.units[0].unit_type == JF_17 and i < 4: if self.group.units[0].unit_type == JF_17 and i < 4:

View File

@ -135,7 +135,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
if not self.ground_object.is_dead: if not self.ground_object.is_dead:
self.m.vehicle_group( self.m.vehicle_group(
country=self.country, country=self.country,
name=self.ground_object.string_identifier, name=self.ground_object.group_name,
_type=unit_type, _type=unit_type,
position=self.ground_object.position, position=self.ground_object.position,
heading=self.ground_object.heading, heading=self.ground_object.heading,
@ -144,7 +144,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
def generate_static(self, static_type: StaticType) -> None: def generate_static(self, static_type: StaticType) -> None:
self.m.static_group( self.m.static_group(
country=self.country, country=self.country,
name=self.ground_object.string_identifier, name=self.ground_object.group_name,
_type=static_type, _type=static_type,
position=self.ground_object.position, position=self.ground_object.position,
heading=self.ground_object.heading, heading=self.ground_object.heading,

View File

@ -1,19 +1,15 @@
import random from abc import ABC
from dcs.vehicles import AirDefence from game import Game
from game import db
from gen.sam.group_generator import GroupGenerator from gen.sam.group_generator import GroupGenerator
from theater.theatergroundobject import SamGroundObject
class GenericSamGroupGenerator(GroupGenerator): class GenericSamGroupGenerator(GroupGenerator, ABC):
""" """
This is the base for all SAM group generators This is the base for all SAM group generators
""" """
@property def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
def groupNamePrefix(self) -> str: ground_object.skynet_capable = True
# prefix the SAM site for use with the Skynet IADS plugin super().__init__(game, ground_object)
if self.faction == self.game.player_name: # this is the player faction
return "BLUE SAM "
else:
return "RED SAM "

View File

@ -22,31 +22,26 @@ if TYPE_CHECKING:
# types rather than pydcs groups. # types rather than pydcs groups.
class GroupGenerator: class GroupGenerator:
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Optional[Faction] = None): # faction is not mandatory because some subclasses do not use it def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
self.game = game self.game = game
self.go = ground_object self.go = ground_object
self.position = ground_object.position self.position = ground_object.position
self.heading = random.randint(0, 359) self.heading = random.randint(0, 359)
self.faction = faction self.vg = unitgroup.VehicleGroup(self.game.next_group_id(),
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.groupNamePrefix + self.go.group_identifier) self.go.group_name)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0) wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True wp.ETA_locked = True
@property
def groupNamePrefix(self) -> str:
return ""
def generate(self): def generate(self):
raise NotImplementedError raise NotImplementedError
def get_generated_group(self) -> unitgroup.VehicleGroup: def get_generated_group(self) -> unitgroup.VehicleGroup:
return self.vg return self.vg
def add_unit(self, unit_type: VehicleType, name: str, pos_x: float, pos_y: float, heading: int): def add_unit(self, unit_type: VehicleType, name: str, pos_x: float,
nn = "cgroup|" + str(self.go.cp_id) + '|' + str(self.go.group_id) + '|' + str(self.go.group_identifier) + "|" + name pos_y: float, heading: int) -> Vehicle:
unit = Vehicle(self.game.next_unit_id(), unit = Vehicle(self.game.next_unit_id(),
nn, unit_type.id) f"{self.go.group_name}|{name}", unit_type.id)
unit.position.x = pos_x unit.position.x = pos_x
unit.position.y = pos_y unit.position.y = pos_y
unit.heading = heading unit.heading = heading
@ -88,6 +83,7 @@ class GroupGenerator:
current_offset += outer_offset current_offset += outer_offset
return positions return positions
class ShipGroupGenerator(GroupGenerator): class ShipGroupGenerator(GroupGenerator):
"""Abstract class for other ship generator classes""" """Abstract class for other ship generator classes"""
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction): def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
@ -96,15 +92,14 @@ class ShipGroupGenerator(GroupGenerator):
self.position = ground_object.position self.position = ground_object.position
self.heading = random.randint(0, 359) self.heading = random.randint(0, 359)
self.faction = faction self.faction = faction
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.groupNamePrefix + self.go.group_identifier) self.vg = unitgroup.ShipGroup(self.game.next_group_id(),
self.go.group_name)
wp = self.vg.add_waypoint(self.position, 0) wp = self.vg.add_waypoint(self.position, 0)
wp.ETA_locked = True wp.ETA_locked = True
def add_unit(self, unit_type, name, pos_x, pos_y, heading): def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
nn = "cgroup|" + str(self.go.cp_id) + '|' + str(self.go.group_id) + '|' + str(self.go.group_identifier) + "|" + name
unit = Ship(self.game.next_unit_id(), unit = Ship(self.game.next_unit_id(),
nn, unit_type) f"{self.go.group_name}|{name}", unit_type)
unit.position.x = pos_x unit.position.x = pos_x
unit.position.y = pos_y unit.position.y = pos_y
unit.heading = heading unit.heading = heading

View File

@ -38,6 +38,7 @@ from gen.sam.sam_zu23_ural import ZU23UralGenerator
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
from gen.sam.freya_ewr import FreyaGenerator from gen.sam.freya_ewr import FreyaGenerator
from theater import TheaterGroundObject from theater import TheaterGroundObject
from theater.theatergroundobject import SamGroundObject
SAM_MAP = { SAM_MAP = {
"HawkGenerator": HawkGenerator, "HawkGenerator": HawkGenerator,
@ -129,13 +130,13 @@ def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject,
possible_sams_generators = get_faction_possible_sams_generator(faction) possible_sams_generators = get_faction_possible_sams_generator(faction)
if len(possible_sams_generators) > 0: if len(possible_sams_generators) > 0:
sam_generator_class = random.choice(possible_sams_generators) sam_generator_class = random.choice(possible_sams_generators)
generator = sam_generator_class(game, ground_object, db.FACTIONS[faction]) generator = sam_generator_class(game, ground_object)
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
return None return None
def generate_shorad_group(game: Game, ground_object: TheaterGroundObject, def generate_shorad_group(game: Game, ground_object: SamGroundObject,
faction_name: str) -> Optional[VehicleGroup]: faction_name: str) -> Optional[VehicleGroup]:
faction = db.FACTIONS[faction_name] faction = db.FACTIONS[faction_name]

View File

@ -95,7 +95,7 @@ class QFlightTypeComboBox(QComboBox):
yield from self.ENEMY_AIRBASE_MISSIONS yield from self.ENEMY_AIRBASE_MISSIONS
elif isinstance(self.target, TheaterGroundObject): elif isinstance(self.target, TheaterGroundObject):
# TODO: Filter more based on the category. # TODO: Filter more based on the category.
friendly = self.target.parent_control_point(self.theater).captured friendly = self.target.control_point.captured
if friendly: if friendly:
yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS
else: else:

View File

@ -1,7 +1,7 @@
from PySide2.QtGui import QStandardItem, QStandardItemModel from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game from game import Game
from gen import Conflict, FlightWaypointType from gen import BuildingGroundObject, Conflict, FlightWaypointType
from gen.flights.flight import FlightWaypoint from gen.flights.flight import FlightWaypoint
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
from theater import ControlPointType from theater import ControlPointType
@ -71,7 +71,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured): if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
if not ground_object.is_dead and not ground_object.dcs_identifier == "AA": if not ground_object.is_dead and not isinstance(ground_object, BuildingGroundObject):
wpt = FlightWaypoint( wpt = FlightWaypoint(
FlightWaypointType.CUSTOM, FlightWaypointType.CUSTOM,
ground_object.position.x, ground_object.position.x,

View File

@ -121,8 +121,8 @@ class QLiberationMap(QGraphicsView):
def setGame(self, game: Optional[Game]): def setGame(self, game: Optional[Game]):
self.game = game self.game = game
logging.debug("Reloading Map Canvas")
if self.game is not None: if self.game is not None:
logging.debug("Reloading Map Canvas")
self.reload_scene() self.reload_scene()
""" """

View File

@ -1,4 +1,5 @@
import logging import logging
import traceback
import webbrowser import webbrowser
from typing import Optional from typing import Optional
@ -16,7 +17,7 @@ from PySide2.QtWidgets import (
) )
import qt_ui.uiconstants as CONST import qt_ui.uiconstants as CONST
from game import Game, persistency, VERSION from game import Game, VERSION, persistency
from qt_ui.dialogs import Dialog from qt_ui.dialogs import Dialog
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
from qt_ui.models import GameModel from qt_ui.models import GameModel
@ -40,10 +41,9 @@ class QLiberationWindow(QMainWindow):
self.game: Optional[Game] = None self.game: Optional[Game] = None
self.game_model = GameModel() self.game_model = GameModel()
Dialog.set_game(self.game_model) Dialog.set_game(self.game_model)
self.ato_panel = None self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.info_panel = None self.info_panel = QInfoPanel(self.game)
self.liberation_map = None self.liberation_map = QLiberationMap(self.game_model)
self.setGame(persistency.restore_game())
self.setGeometry(300, 100, 270, 100) self.setGeometry(300, 100, 270, 100)
self.setWindowTitle(f"DCS Liberation - v{VERSION}") self.setWindowTitle(f"DCS Liberation - v{VERSION}")
@ -55,17 +55,14 @@ class QLiberationWindow(QMainWindow):
self.initMenuBar() self.initMenuBar()
self.initToolbar() self.initToolbar()
self.connectSignals() self.connectSignals()
self.onGameGenerated(self.game)
screen = QDesktopWidget().screenGeometry() screen = QDesktopWidget().screenGeometry()
self.setGeometry(0, 0, screen.width(), screen.height()) self.setGeometry(0, 0, screen.width(), screen.height())
self.setWindowState(Qt.WindowMaximized) self.setWindowState(Qt.WindowMaximized)
def initUi(self): self.onGameGenerated(persistency.restore_game())
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.liberation_map = QLiberationMap(self.game_model)
self.info_panel = QInfoPanel(self.game)
def initUi(self):
hbox = QSplitter(Qt.Horizontal) hbox = QSplitter(Qt.Horizontal)
vbox = QSplitter(Qt.Vertical) vbox = QSplitter(Qt.Vertical)
hbox.addWidget(self.ato_panel) hbox.addWidget(self.ato_panel)
@ -193,8 +190,7 @@ class QLiberationWindow(QMainWindow):
filter="*.liberation") filter="*.liberation")
if file is not None: if file is not None:
game = persistency.load_game(file[0]) game = persistency.load_game(file[0])
self.setGame(game) GameUpdateSignal.get_instance().updateGame(game)
GameUpdateSignal.get_instance().updateGame(self.game)
def saveGame(self): def saveGame(self):
logging.info("Saving game") logging.info("Saving game")
@ -217,14 +213,27 @@ class QLiberationWindow(QMainWindow):
GameUpdateSignal.get_instance().updateGame(self.game) GameUpdateSignal.get_instance().updateGame(self.game)
def setGame(self, game: Optional[Game]): def setGame(self, game: Optional[Game]):
if game is not None: try:
game.on_load() if game is not None:
self.game = game game.on_load()
if self.info_panel is not None: self.game = game
self.info_panel.setGame(game) if self.info_panel is not None:
self.game_model.set(self.game) self.info_panel.setGame(game)
if self.liberation_map is not None: self.game_model.set(self.game)
self.liberation_map.setGame(game) if self.liberation_map is not None:
self.liberation_map.setGame(game)
except AttributeError:
logging.exception("Incompatible save game")
QMessageBox.critical(
self,
"Could not load save game",
"The save game you have loaded is incompatible with this "
"version of DCS Liberation.\n"
"\n"
f"{traceback.format_exc()}",
QMessageBox.Ok
)
GameUpdateSignal.get_instance().updateGame(None)
def showAboutDialog(self): def showAboutDialog(self):
text = "<h3>DCS Liberation " + VERSION + "</h3>" + \ text = "<h3>DCS Liberation " + VERSION + "</h3>" + \

View File

@ -72,10 +72,10 @@ if dcsLiberation and SkynetIADS then
end end
--add EW units to the IADS: --add EW units to the IADS:
iads:addEarlyWarningRadarsByPrefix(coalitionPrefix .. " EW") iads:addEarlyWarningRadarsByPrefix(coalitionPrefix .. "|EWR|")
--add SAM groups to the IADS: --add SAM groups to the IADS:
iads:addSAMSitesByPrefix(coalitionPrefix .. " SAM") iads:addSAMSitesByPrefix(coalitionPrefix .. "|SAM|")
-- specific configurations, for each SAM type -- specific configurations, for each SAM type
if actAsEwr then if actAsEwr then

View File

@ -19,6 +19,14 @@ grey text -------------------- #B7C0C6
*/ */
/*
* Makes all message box text selectable.
* https://stackoverflow.com/a/32595502/632035
*/
QMessageBox {
messagebox-text-interaction-flags: 5;
}
/*QMenuBar*/ /*QMenuBar*/
QMenuBar { QMenuBar {
spacing: 2px; /* spacing between menu bar items */ spacing: 2px; /* spacing between menu bar items */

View File

@ -1,3 +1,11 @@
/* /*
windows basis styles windows basis styles
*/ */
/*
* Makes all message box text selectable.
* https://stackoverflow.com/a/32595502/632035
*/
QMessageBox {
messagebox-text-interaction-flags: 5;
}

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import itertools
import re import re
from typing import Dict, List, TYPE_CHECKING from typing import Dict, List, TYPE_CHECKING
from enum import Enum from enum import Enum
@ -17,7 +18,7 @@ from game import db
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from .base import Base from .base import Base
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import TheaterGroundObject from .theatergroundobject import SamGroundObject, TheaterGroundObject
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@ -50,7 +51,8 @@ class ControlPoint(MissionTarget):
self.id = id self.id = id
self.full_name = name self.full_name = name
self.at = at self.at = at
self.ground_objects: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[SamGroundObject] = []
self.size = size self.size = size
self.importance = importance self.importance = importance
@ -64,6 +66,11 @@ class ControlPoint(MissionTarget):
self.stances: Dict[int, CombatStance] = {} self.stances: Dict[int, CombatStance] = {}
self.airport = None self.airport = None
@property
def ground_objects(self) -> List[TheaterGroundObject]:
return list(
itertools.chain(self.connected_objectives, self.base_defenses))
@classmethod @classmethod
def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True): def from_airport(cls, airport: Airport, radials: List[int], size: int, importance: float, has_frontline=True):
assert airport assert airport
@ -225,9 +232,6 @@ class ControlPoint(MissionTarget):
self.base.armor = {} self.base.armor = {}
# Handle cyclic dependency. # Handle cyclic dependency.
from .start_generator import generate_airbase_defense_group from .start_generator import BaseDefenseGenerator
for idx, ground_object in enumerate(self.ground_objects): self.base_defenses = []
ground_object.groups = [] BaseDefenseGenerator(game, self, faction_name).generate()
if ground_object.airbase_group and faction_name != "":
generate_airbase_defense_group(idx, ground_object,
faction_name, game)

View File

@ -160,11 +160,9 @@ class GameGenerator:
class ControlPointGroundObjectGenerator: class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, control_point: ControlPoint, def __init__(self, game: Game, control_point: ControlPoint) -> None:
templates: GroundObjectTemplates) -> None:
self.game = game self.game = game
self.control_point = control_point self.control_point = control_point
self.templates = templates
@property @property
def faction_name(self) -> str: def faction_name(self) -> str:
@ -178,8 +176,7 @@ class ControlPointGroundObjectGenerator:
return db.FACTIONS[self.faction_name] return db.FACTIONS[self.faction_name]
def generate(self) -> bool: def generate(self) -> bool:
self.control_point.ground_objects = [] self.control_point.connected_objectives = []
self.generate_ground_points()
if self.faction.navy_generators: if self.faction.navy_generators:
# Even airbases can generate navies if they are close enough to the # Even airbases can generate navies if they are close enough to the
# water. This is not controlled by the control point definition, but # water. This is not controlled by the control point definition, but
@ -187,85 +184,8 @@ class ControlPointGroundObjectGenerator:
# for the ship. # for the ship.
self.generate_navy() self.generate_navy()
if self.faction.missiles:
# TODO: Presumably only for airbases?
self.generate_missile_sites()
return True return True
def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point."""
if self.control_point.is_global:
return
# TODO: Should probably perform this check later.
# Just because we don't have factories for the faction doesn't mean we
# shouldn't generate AA.
available_categories = self.faction.building_set
if not available_categories:
return
# Always generate at least one AA point.
self.generate_aa_site()
# And between 2 and 7 other objectives.
amount = random.randrange(2, 7)
for i in range(amount):
# 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0:
self.generate_aa_site()
else:
category = random.choice(available_categories)
self.generate_ground_point(category)
def generate_ground_point(self, category: str) -> None:
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
point = find_location(category != "oil",
self.control_point.position,
self.game.theater, 10000, 40000,
self.control_point.ground_objects)
if point is None:
logging.error(
f"Could not find point for {obj_name} at {self.control_point}")
return
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,
unit["heading"], self.control_point, unit["type"])
self.control_point.ground_objects.append(g)
def generate_aa_site(self) -> None:
obj_name = namegen.random_objective_name()
position = find_location(True, self.control_point.position,
self.game.theater, 10000, 40000,
self.control_point.ground_objects)
if position is None:
logging.error(
f"Could not find point for {obj_name} at {self.control_point}")
return
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=False)
group = generate_anti_air_group(self.game, g, self.faction_name)
if group is not None:
g.groups = [group]
self.control_point.ground_objects.append(g)
def generate_navy(self) -> None: def generate_navy(self) -> None:
skip_player_navy = self.game.settings.do_not_generate_player_navy skip_player_navy = self.game.settings.do_not_generate_player_navy
if self.control_point.captured and skip_player_navy: if self.control_point.captured and skip_player_navy:
@ -295,30 +215,7 @@ class ControlPointGroundObjectGenerator:
g.groups = [] g.groups = []
if group is not None: if group is not None:
g.groups.append(group) g.groups.append(group)
self.control_point.ground_objects.append(g) self.control_point.connected_objectives.append(g)
def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count):
self.generate_missile_site()
def generate_missile_site(self) -> None:
point = find_location(True, self.control_point.position,
self.game.theater, 2500, 40000, [], False)
if point is None:
logging.info(
f"Could not find point for {self.control_point} missile site")
return
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id,
point, 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.ground_objects.append(g)
return
class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator): class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
@ -341,7 +238,7 @@ class CarrierGroundObjectGenerator(ControlPointGroundObjectGenerator):
g.groups = [] g.groups = []
if group is not None: if group is not None:
g.groups.append(group) g.groups.append(group)
self.control_point.ground_objects.append(g) self.control_point.connected_objectives.append(g)
self.control_point.name = random.choice(carrier_names) self.control_point.name = random.choice(carrier_names)
return True return True
@ -366,21 +263,23 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
g.groups = [] g.groups = []
if group is not None: if group is not None:
g.groups.append(group) g.groups.append(group)
self.control_point.ground_objects.append(g) self.control_point.connected_objectives.append(g)
self.control_point.name = random.choice(lha_names) self.control_point.name = random.choice(lha_names)
return True return True
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator): class BaseDefenseGenerator:
def generate(self) -> bool: def __init__(self, game: Game, control_point: ControlPoint,
if not super().generate(): faction_name: str) -> None:
return False self.game = game
self.control_point = control_point
self.faction_name = faction_name
def generate(self) -> None:
for i in range(random.randint(3, 6)): for i in range(random.randint(3, 6)):
self.generate_sam(i) self.generate_base_defense(i)
return True
def generate_sam(self, index: int) -> None: def generate_base_defense(self, index: int) -> None:
position = find_location(True, self.control_point.position, position = find_location(True, self.control_point.position,
self.game.theater, 400, 3200, [], True) self.game.theater, 400, 3200, [], True)
@ -403,8 +302,122 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
g = SamGroundObject(namegen.random_objective_name(), group_id, g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=True) position, self.control_point, for_airbase=True)
generate_airbase_defense_group(index, g, self.faction_name, self.game) generate_airbase_defense_group(index, g, self.faction_name,
self.control_point.ground_objects.append(g) self.game)
self.control_point.base_defenses.append(g)
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(self, game: Game, control_point: ControlPoint,
templates: GroundObjectTemplates) -> None:
super().__init__(game, control_point)
self.templates = templates
def generate(self) -> bool:
if not super().generate():
return False
BaseDefenseGenerator(self.game, self.control_point,
self.faction_name).generate()
self.generate_ground_points()
if self.faction.missiles:
self.generate_missile_sites()
return True
def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point."""
if self.control_point.is_global:
return
# Always generate at least one AA point.
self.generate_aa_site()
# And between 2 and 7 other objectives.
amount = random.randrange(2, 7)
for i in range(amount):
# 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0:
self.generate_aa_site()
else:
self.generate_ground_point()
def generate_ground_point(self) -> None:
try:
category = random.choice(self.faction.building_set)
except IndexError:
logging.exception("Faction has no buildings defined")
return
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
point = find_location(category != "oil",
self.control_point.position,
self.game.theater, 10000, 40000,
self.control_point.ground_objects)
if point is None:
logging.error(
f"Could not find point for {obj_name} at {self.control_point}")
return
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,
unit["heading"], self.control_point, unit["type"])
self.control_point.connected_objectives.append(g)
def generate_aa_site(self) -> None:
obj_name = namegen.random_objective_name()
position = find_location(True, self.control_point.position,
self.game.theater, 10000, 40000,
self.control_point.ground_objects)
if position is None:
logging.error(
f"Could not find point for {obj_name} at {self.control_point}")
return
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=False)
group = generate_anti_air_group(self.game, g, self.faction_name)
if group is not None:
g.groups = [group]
self.control_point.connected_objectives.append(g)
def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count):
self.generate_missile_site()
def generate_missile_site(self) -> None:
point = find_location(True, self.control_point.position,
self.game.theater, 2500, 40000, [], False)
if point is None:
logging.info(
f"Could not find point for {self.control_point} missile site")
return
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(namegen.random_objective_name(), group_id,
point, 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
class GroundObjectGenerator: class GroundObjectGenerator:
@ -424,11 +437,9 @@ class GroundObjectGenerator:
def generate_for_control_point(self, control_point: ControlPoint) -> bool: def generate_for_control_point(self, control_point: ControlPoint) -> bool:
generator: ControlPointGroundObjectGenerator generator: ControlPointGroundObjectGenerator
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP: if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
generator = CarrierGroundObjectGenerator(self.game, control_point, generator = CarrierGroundObjectGenerator(self.game, control_point)
self.templates)
elif control_point.cptype == ControlPointType.LHA_GROUP: elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(self.game, control_point, generator = LhaGroundObjectGenerator(self.game, control_point)
self.templates)
else: else:
generator = AirbaseGroundObjectGenerator(self.game, control_point, generator = AirbaseGroundObjectGenerator(self.game, control_point,
self.templates) self.templates)
@ -436,8 +447,8 @@ class GroundObjectGenerator:
def generate_airbase_defense_group(airbase_defense_group_id: int, def generate_airbase_defense_group(airbase_defense_group_id: int,
ground_obj: TheaterGroundObject, ground_obj: SamGroundObject, faction: str,
faction: str, game: Game) -> None: game: Game) -> None:
if airbase_defense_group_id == 0: if airbase_defense_group_id == 0:
group = generate_armor_group(faction, game, ground_obj) group = generate_armor_group(faction, game, ground_obj)
elif airbase_defense_group_id == 1 and random.randint(0, 1) == 0: elif airbase_defense_group_id == 1 and random.randint(0, 1) == 0:

View File

@ -8,7 +8,6 @@ from dcs.unit import Unit
from dcs.unitgroup import Group from dcs.unitgroup import Group
if TYPE_CHECKING: if TYPE_CHECKING:
from .conflicttheater import ConflictTheater
from .controlpoint import ControlPoint from .controlpoint import ControlPoint
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
@ -72,29 +71,21 @@ CATEGORY_MAP = {
class TheaterGroundObject(MissionTarget): class TheaterGroundObject(MissionTarget):
def __init__(self, name: str, category: str, group_id: int, object_id: int, def __init__(self, name: str, category: str, group_id: int, position: Point,
position: Point, heading: int, cp_id: int, dcs_identifier: str, heading: int, control_point: ControlPoint, dcs_identifier: str,
airbase_group: bool, sea_object: bool) -> None: airbase_group: bool, sea_object: bool) -> None:
super().__init__(name, position) super().__init__(name, position)
self.category = category self.category = category
self.group_id = group_id self.group_id = group_id
self.object_id = object_id
self.heading = heading self.heading = heading
self.cp_id = cp_id self.control_point = control_point
self.dcs_identifier = dcs_identifier self.dcs_identifier = dcs_identifier
self.airbase_group = airbase_group self.airbase_group = airbase_group
self.sea_object = sea_object self.sea_object = sea_object
self.is_dead = False self.is_dead = False
# TODO: There is never more than one group.
self.groups: List[Group] = [] self.groups: List[Group] = []
@property
def string_identifier(self):
return "{}|{}|{}|{}".format(self.category, self.cp_id, self.group_id, self.object_id)
@property
def group_identifier(self) -> str:
return "{}|{}".format(self.category, self.group_id)
@property @property
def units(self) -> List[Unit]: def units(self) -> List[Unit]:
""" """
@ -103,26 +94,20 @@ class TheaterGroundObject(MissionTarget):
return list(itertools.chain.from_iterable([g.units for g in self.groups])) return list(itertools.chain.from_iterable([g.units for g in self.groups]))
@property @property
def name_abbrev(self) -> str: def group_name(self) -> str:
return ABBREV_NAME[self.category] """The name of the unit group."""
return f"{self.category}|{self.group_id}"
def __str__(self) -> str: def __str__(self) -> str:
return NAME_BY_CATEGORY[self.category] return NAME_BY_CATEGORY[self.category]
def matches_string_identifier(self, identifier): def is_same_group(self, identifier: str) -> bool:
return self.string_identifier == identifier return self.group_id == identifier
@property @property
def obj_name(self) -> str: def obj_name(self) -> str:
return self.name return self.name
def parent_control_point(self, theater: ConflictTheater) -> ControlPoint:
"""Searches the theater for the parent control point."""
for cp in theater.controlpoints:
if cp.id == self.cp_id:
return cp
raise RuntimeError("Could not find matching control point in theater")
class BuildingGroundObject(TheaterGroundObject): class BuildingGroundObject(TheaterGroundObject):
def __init__(self, name: str, category: str, group_id: int, object_id: int, def __init__(self, name: str, category: str, group_id: int, object_id: int,
@ -132,14 +117,19 @@ class BuildingGroundObject(TheaterGroundObject):
name=name, name=name,
category=category, category=category,
group_id=group_id, group_id=group_id,
object_id=object_id,
position=position, position=position,
heading=heading, heading=heading,
cp_id=control_point.id, control_point=control_point,
dcs_identifier=dcs_identifier, dcs_identifier=dcs_identifier,
airbase_group=False, airbase_group=False,
sea_object=False sea_object=False
) )
self.object_id = object_id
@property
def group_name(self) -> str:
"""The name of the unit group."""
return f"{self.category}|{self.group_id}|{self.object_id}"
class GenericCarrierGroundObject(TheaterGroundObject): class GenericCarrierGroundObject(TheaterGroundObject):
@ -154,10 +144,9 @@ class CarrierGroundObject(GenericCarrierGroundObject):
name=name, name=name,
category="CARRIER", category="CARRIER",
group_id=group_id, group_id=group_id,
object_id=0,
position=control_point.position, position=control_point.position,
heading=0, heading=0,
cp_id=control_point.id, control_point=control_point,
dcs_identifier="CARRIER", dcs_identifier="CARRIER",
airbase_group=True, airbase_group=True,
sea_object=True sea_object=True
@ -172,10 +161,9 @@ class LhaGroundObject(GenericCarrierGroundObject):
name=name, name=name,
category="LHA", category="LHA",
group_id=group_id, group_id=group_id,
object_id=0,
position=control_point.position, position=control_point.position,
heading=0, heading=0,
cp_id=control_point.id, control_point=control_point,
dcs_identifier="LHA", dcs_identifier="LHA",
airbase_group=True, airbase_group=True,
sea_object=True sea_object=True
@ -189,16 +177,18 @@ class MissileSiteGroundObject(TheaterGroundObject):
name=name, name=name,
category="aa", category="aa",
group_id=group_id, group_id=group_id,
object_id=0,
position=position, position=position,
heading=0, heading=0,
cp_id=control_point.id, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False, airbase_group=False,
sea_object=False sea_object=False
) )
# TODO: Differentiate types.
# This type gets used both for AA sites (SAM, AAA, or SHORAD) but also for the
# armor garrisons at airbases. These should each be split into their own types.
class SamGroundObject(TheaterGroundObject): class SamGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point, def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None: control_point: ControlPoint, for_airbase: bool) -> None:
@ -206,14 +196,26 @@ class SamGroundObject(TheaterGroundObject):
name=name, name=name,
category="aa", category="aa",
group_id=group_id, group_id=group_id,
object_id=0,
position=position, position=position,
heading=0, heading=0,
cp_id=control_point.id, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=for_airbase, airbase_group=for_airbase,
sea_object=False sea_object=False
) )
# Set by the SAM unit generator if the generated group is compatible
# with Skynet.
self.skynet_capable = False
@property
def group_name(self) -> str:
if self.skynet_capable:
# Prefix the group names of SAM sites with the side color so Skynet
# can find them.
color = "BLUE" if self.control_point.captured else "RED"
return f"{color}|SAM|{self.group_id}"
else:
return super().group_name
class ShipGroundObject(TheaterGroundObject): class ShipGroundObject(TheaterGroundObject):
@ -223,10 +225,9 @@ class ShipGroundObject(TheaterGroundObject):
name=name, name=name,
category="aa", category="aa",
group_id=group_id, group_id=group_id,
object_id=0,
position=position, position=position,
heading=0, heading=0,
cp_id=control_point.id, control_point=control_point,
dcs_identifier="AA", dcs_identifier="AA",
airbase_group=False, airbase_group=False,
sea_object=True sea_object=True