Merge pull request #309 from DanAlbert/theater-refactor

Refactor game and ground object generation.
This commit is contained in:
C. Perreau 2020-11-06 20:33:19 +01:00 committed by GitHub
commit 040db055fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 963 additions and 625 deletions

View File

@ -1,2 +1,3 @@
from .game import Game
from . import db
from .version import VERSION

View File

@ -1,11 +1,11 @@
import inspect
import dcs
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa']
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
WW2_FREE = ['fuel', 'factory', 'ware', 'aa']
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'aa']
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'aa']
WW2_FREE = ['fuel', 'factory', 'ware']
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp']
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp']
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',

View File

@ -14,7 +14,6 @@ from game.infos.information import Information
from game.operation.operation import Operation
from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
from theater.start_generator import generate_airbase_defense_group
if TYPE_CHECKING:
from ..game import Game

8
game/version.py Normal file
View File

@ -0,0 +1,8 @@
from pathlib import Path
#: Current version of Liberation.
VERSION = "2.2.0-preview"
if Path("buildnumber").exists():
with open("buildnumber", "r") as file:
VERSION += f"-{file.readline()}"

View File

@ -1,8 +1,18 @@
"""Generators for creating the groups for ground objectives.
The classes in this file are responsible for creating the vehicle groups, ship
groups, statics, missile sites, and AA sites for the mission. Each of these
objectives is defined in the Theater by a TheaterGroundObject. These classes
create the pydcs groups and statics for those areas and add them to the mission.
"""
from __future__ import annotations
import logging
import random
from typing import Dict, Iterator
from typing import Dict, Iterator, Optional, TYPE_CHECKING
from dcs import Mission
from dcs.country import Country
from dcs.statics import fortification_map, warehouse_map
from dcs.task import (
ActivateBeaconCommand,
@ -10,22 +20,339 @@ from dcs.task import (
EPLRS,
OptAlarmState,
)
from dcs.unit import Ship, Vehicle
from dcs.unitgroup import StaticGroup
from dcs.unit import Ship, Vehicle, Unit
from dcs.unitgroup import Group, ShipGroup, StaticGroup
from dcs.unittype import StaticType, UnitType
from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name
from theater import ControlPoint, TheaterGroundObject
from theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject,
GenericCarrierGroundObject,
LhaGroundObject, ShipGroundObject,
)
from .conflictgen import Conflict
from .radios import RadioRegistry
from .radios import RadioFrequency, RadioRegistry
from .runways import RunwayData
from .tacan import TacanBand, TacanRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry
if TYPE_CHECKING:
from game import Game
FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
class GenericGroundObjectGenerator:
"""An unspecialized ground object generator.
Currently used only for SAM and missile (V1/V2) sites.
"""
def __init__(self, ground_object: TheaterGroundObject, country: Country,
game: Game, mission: Mission) -> None:
self.ground_object = ground_object
self.country = country
self.game = game
self.m = mission
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
for group in self.ground_object.groups:
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized unit type: {group.units[0].type}")
vg = self.m.vehicle_group(self.country, group.name, unit_type,
position=group.position,
heading=group.units[0].heading)
vg.units[0].name = self.m.string(group.units[0].name)
vg.units[0].player_can_drive = True
for i, u in enumerate(group.units):
if i > 0:
vehicle = Vehicle(self.m.next_unit_id(),
self.m.string(u.name), u.type)
vehicle.position.x = u.position.x
vehicle.position.y = u.position.y
vehicle.heading = u.heading
vehicle.player_can_drive = True
vg.add_unit(vehicle)
self.enable_eplrs(vg, unit_type)
self.set_alarm_state(vg)
@staticmethod
def enable_eplrs(group: Group, unit_type: UnitType) -> None:
if hasattr(unit_type, 'eplrs'):
if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def set_alarm_state(self, group: Group) -> None:
if self.game.settings.perf_red_alert_state:
group.points[0].tasks.append(OptAlarmState(2))
else:
group.points[0].tasks.append(OptAlarmState(1))
class BuildingSiteGenerator(GenericGroundObjectGenerator):
"""Generator for building sites.
Building sites are the primary type of non-airbase objective locations that
appear on the map. They come in a handful of variants each with different
types of buildings and ground units.
"""
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
if self.ground_object.dcs_identifier in warehouse_map:
static_type = warehouse_map[self.ground_object.dcs_identifier]
self.generate_static(static_type)
elif self.ground_object.dcs_identifier in fortification_map:
static_type = fortification_map[self.ground_object.dcs_identifier]
self.generate_static(static_type)
elif self.ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
for f in FORTIFICATION_UNITS:
if f.id == self.ground_object.dcs_identifier:
unit_type = f
self.generate_vehicle_group(unit_type)
break
else:
logging.error(
f"{self.ground_object.dcs_identifier} not found in static maps")
def generate_vehicle_group(self, unit_type: UnitType) -> None:
if not self.ground_object.is_dead:
self.m.vehicle_group(
country=self.country,
name=self.ground_object.string_identifier,
_type=unit_type,
position=self.ground_object.position,
heading=self.ground_object.heading,
)
def generate_static(self, static_type: StaticType) -> None:
self.m.static_group(
country=self.country,
name=self.ground_object.string_identifier,
_type=static_type,
position=self.ground_object.position,
heading=self.ground_object.heading,
dead=self.ground_object.is_dead,
)
class GenericCarrierGenerator(GenericGroundObjectGenerator):
"""Base type for carrier group generation.
Used by both CV(N) groups and LHA groups.
"""
def __init__(self, ground_object: GenericCarrierGroundObject,
control_point: ControlPoint, country: Country, game: Game,
mission: Mission, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
runways: Dict[str, RunwayData]) -> None:
super().__init__(ground_object, country, game, mission)
self.ground_object = ground_object
self.control_point = control_point
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.icls_alloc = icls_alloc
self.runways = runways
def generate(self) -> None:
# TODO: Require single group?
for group in self.ground_object.groups:
if not group.units:
logging.warning(
f"Found empty carrier group in {self.control_point}")
continue
atc = self.radio_registry.alloc_uhf()
ship_group = self.configure_carrier(group, atc)
for unit in group.units[1:]:
ship_group.add_unit(self.create_ship(unit, atc))
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc)
brc = self.steam_into_wind(ship_group)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
def get_carrier_type(self, group: Group) -> UnitType:
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized carrier name: {group.units[0].type}")
return unit_type
def configure_carrier(self, group: Group,
atc_channel: RadioFrequency) -> ShipGroup:
unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group(self.country, group.name, unit_type,
position=group.position,
heading=group.units[0].heading)
ship_group.set_frequency(atc_channel.hertz)
ship_group.units[0].name = self.m.string(group.units[0].name)
return ship_group
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
ship = Ship(self.m.next_unit_id(),
self.m.string(unit.name),
unit_type_from_name(unit.type))
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.heading
# TODO: Verify.
ship.set_frequency(atc_channel.hertz)
return ship
def steam_into_wind(self, group: ShipGroup) -> Optional[int]:
brc = self.m.weather.wind_at_ground.direction + 180
for attempt in range(5):
point = group.points[0].position.point_from_heading(
brc, 100000 - attempt * 20000)
if self.game.theater.is_in_sea(point):
group.add_waypoint(point)
return brc
return None
def tacan_callsign(self) -> str:
raise NotImplementedError
@staticmethod
def activate_beacons(group: ShipGroup, tacan: TacanChannel,
callsign: str, icls: int) -> None:
group.points[0].tasks.append(ActivateBeaconCommand(
channel=tacan.number,
modechannel=tacan.band.value,
callsign=callsign,
unit_id=group.units[0].id,
aa=False
))
group.points[0].tasks.append(ActivateICLSCommand(
icls, unit_id=group.units[0].id
))
def add_runway_data(self, brc: int, atc: RadioFrequency,
tacan: TacanChannel, callsign: str, icls: int) -> None:
# TODO: Make unit name usable.
# This relies on one control point mapping exactly
# to one LHA, carrier, or other usable "runway".
# This isn't wholly true, since the DD escorts of
# the carrier group are valid for helicopters, but
# they aren't exposed as such to the game. Should
# clean this up so that's possible. We can't use the
# unit name since it's an arbitrary ID.
self.runways[self.control_point.name] = RunwayData(
self.control_point.name,
brc,
"N/A",
atc=atc,
tacan=tacan,
tacan_callsign=callsign,
icls=icls,
)
class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups."""
def get_carrier_type(self, group: Group) -> UnitType:
unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier:
unit_type = db.upgrade_to_supercarrier(unit_type,
self.control_point.name)
return unit_type
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice([
"STE",
"CVN",
"CVH",
"CCV",
"ACC",
"ARC",
"GER",
"ABR",
"LIN",
"TRU",
])
class LhaGenerator(GenericCarrierGenerator):
"""Generator for LHA groups."""
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice([
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
])
class ShipObjectGenerator(GenericGroundObjectGenerator):
"""Generator for non-carrier naval groups."""
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
for group in self.ground_object.groups:
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized unit type: {group.units[0].type}")
self.generate_group(group, unit_type)
def generate_group(self, group_def: Group, unit_type: UnitType):
group = self.m.ship_group(self.country, group_def.name, unit_type,
position=group_def.position,
heading=group_def.units[0].heading)
group.units[0].name = self.m.string(group_def.units[0].name)
# TODO: Skipping the first unit looks like copy pasta from the carrier.
for unit in group_def.units[1:]:
unit_type = unit_type_from_name(unit.type)
ship = Ship(self.m.next_unit_id(),
self.m.string(unit.name), unit_type)
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.heading
group.add_unit(ship)
self.set_alarm_state(group)
class GroundObjectsGenerator:
"""Creates DCS groups and statics for the theater during mission generation.
Most of the work of group/static generation is delegated to the other
generator classes. This class is responsible for finding each of the
locations for spawning ground objects, determining their types, and creating
the appropriate generators.
"""
FARP_CAPACITY = 4
def __init__(self, mission: Mission, conflict: Conflict, game,
@ -61,188 +388,34 @@ class GroundObjectsGenerator:
)
def generate(self):
for cp in self.game.theater.controlpoints:
if cp.captured:
country = self.game.player_country
country_name = self.game.player_country
else:
country = self.game.enemy_country
side = self.m.country(country)
country_name = self.game.enemy_country
country = self.m.country(country_name)
for ground_object in cp.ground_objects:
if ground_object.dcs_identifier == "AA":
if self.game.position_culled(ground_object.position):
continue
for g in ground_object.groups:
if len(g.units) > 0:
utype = unit_type_from_name(g.units[0].type)
if not ground_object.sea_object:
vg = self.m.vehicle_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
vg.units[0].name = self.m.string(g.units[0].name)
vg.units[0].player_can_drive = True
for i, u in enumerate(g.units):
if i > 0:
vehicle = Vehicle(self.m.next_unit_id(), self.m.string(u.name), u.type)
vehicle.position.x = u.position.x
vehicle.position.y = u.position.y
vehicle.heading = u.heading
vehicle.player_can_drive = True
vg.add_unit(vehicle)
if hasattr(utype, 'eplrs'):
if utype.eplrs:
vg.points[0].tasks.append(EPLRS(vg.id))
else:
vg = self.m.ship_group(side, g.name, utype, position=g.position,
heading=g.units[0].heading)
vg.units[0].name = self.m.string(g.units[0].name)
for i, u in enumerate(g.units):
utype = unit_type_from_name(u.type)
if i > 0:
ship = Ship(self.m.next_unit_id(), self.m.string(u.name), utype)
ship.position.x = u.position.x
ship.position.y = u.position.y
ship.heading = u.heading
vg.add_unit(ship)
if self.game.settings.perf_red_alert_state:
vg.points[0].tasks.append(OptAlarmState(2))
else:
vg.points[0].tasks.append(OptAlarmState(1))
elif ground_object.dcs_identifier in ["CARRIER", "LHA"]:
for g in ground_object.groups:
if len(g.units) > 0:
utype = unit_type_from_name(g.units[0].type)
if ground_object.dcs_identifier == "CARRIER" and self.game.settings.supercarrier == True:
utype = db.upgrade_to_supercarrier(utype, cp.name)
sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
atc_channel = self.radio_registry.alloc_uhf()
sg.set_frequency(atc_channel.hertz)
sg.units[0].name = self.m.string(g.units[0].name)
for i, u in enumerate(g.units):
if i > 0:
ship = Ship(self.m.next_unit_id(), self.m.string(u.name), unit_type_from_name(u.type))
ship.position.x = u.position.x
ship.position.y = u.position.y
ship.heading = u.heading
# TODO: Verify.
ship.set_frequency(atc_channel.hertz)
sg.add_unit(ship)
# Find carrier direction (In the wind)
found_carrier_destination = False
attempt = 0
brc = self.m.weather.wind_at_ground.direction + 180
while not found_carrier_destination and attempt < 5:
point = sg.points[0].position.point_from_heading(brc, 100000-attempt*20000)
if self.game.theater.is_in_sea(point):
found_carrier_destination = True
sg.add_waypoint(point)
else:
attempt = attempt + 1
# Set UP TACAN and ICLS
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
icls_channel = next(self.icls_alloc)
# TODO: Assign these properly.
if ground_object.dcs_identifier == "CARRIER":
tacan_callsign = random.choice([
"STE",
"CVN",
"CVH",
"CCV",
"ACC",
"ARC",
"GER",
"ABR",
"LIN",
"TRU",
])
else:
tacan_callsign = random.choice([
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
])
sg.points[0].tasks.append(ActivateBeaconCommand(
channel=tacan.number,
modechannel=tacan.band.value,
callsign=tacan_callsign,
unit_id=sg.units[0].id,
aa=False
))
sg.points[0].tasks.append(ActivateICLSCommand(
icls_channel,
unit_id=sg.units[0].id
))
# TODO: Make unit name usable.
# This relies on one control point mapping exactly
# to one LHA, carrier, or other usable "runway".
# This isn't wholly true, since the DD escorts of
# the carrier group are valid for helicopters, but
# they aren't exposed as such to the game. Should
# clean this up so that's possible. We can't use the
# unit name since it's an arbitrary ID.
self.runways[cp.name] = RunwayData(
cp.name,
brc,
"N/A",
atc=atc_channel,
tacan=tacan,
tacan_callsign=tacan_callsign,
icls=icls_channel,
)
if isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator(ground_object, country,
self.game, self.m)
elif isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator(ground_object, cp, country,
self.game, self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc, self.runways)
elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator(ground_object, cp, country,
self.game, self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc, self.runways)
elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator(ground_object, country,
self.game, self.m)
else:
if self.game.position_culled(ground_object.position):
continue
static_type = None
if ground_object.dcs_identifier in warehouse_map:
static_type = warehouse_map[ground_object.dcs_identifier]
elif ground_object.dcs_identifier in fortification_map:
static_type = fortification_map[ground_object.dcs_identifier]
elif ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
for f in FORTIFICATION_UNITS:
if f.id == ground_object.dcs_identifier:
unit_type = f
break
else:
print("Didn't find {} in static _map(s)!".format(ground_object.dcs_identifier))
continue
if static_type is None:
if not ground_object.is_dead:
group = self.m.vehicle_group(
country=side,
name=ground_object.string_identifier,
_type=unit_type,
position=ground_object.position,
heading=ground_object.heading,
)
logging.info("generated {}object identifier {} with mission id {}".format(
"dead " if ground_object.is_dead else "", group.name, group.id))
else:
group = self.m.static_group(
country=side,
name=ground_object.string_identifier,
_type=static_type,
position=ground_object.position,
heading=ground_object.heading,
dead=ground_object.is_dead,
)
logging.info("generated {}object identifier {} with mission id {}".format("dead " if ground_object.is_dead else "", group.name, group.id))
generator = GenericGroundObjectGenerator(ground_object,
country, self.game,
self.m)
generator.generate()

View File

@ -15,7 +15,12 @@ if TYPE_CHECKING:
from game.game import Game
class GroupGenerator():
# TODO: Generate a group description rather than a pydcs group.
# It appears that all of this work gets redone at miz generation time (see
# groundobjectsgen for an example). We can do less work and include the data we
# care about in the format we want if we just generate our own group description
# types rather than pydcs groups.
class GroupGenerator:
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Optional[Faction] = None): # faction is not mandatory because some subclasses do not use it
self.game = game
@ -34,7 +39,7 @@ class GroupGenerator():
def generate(self):
raise NotImplementedError
def get_generated_group(self):
def get_generated_group(self) -> unitgroup.VehicleGroup:
return self.vg
def add_unit(self, unit_type: VehicleType, name: str, pos_x: float, pos_y: float, heading: int):

View File

@ -1,9 +1,10 @@
import random
from typing import List, Type
from typing import List, Optional, Type
from dcs.vehicles import AirDefence
from dcs.unitgroup import VehicleGroup
from game import db
from game import Game, db
from gen.sam.aaa_bofors import BoforsGenerator
from gen.sam.aaa_flak import FlakGenerator
from gen.sam.aaa_flak18 import Flak18Generator
@ -34,6 +35,7 @@ from gen.sam.sam_zsu23 import ZSU23Generator
from gen.sam.sam_zu23 import ZU23Generator
from gen.sam.sam_zu23_ural import ZU23UralGenerator
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
from theater import TheaterGroundObject
SAM_MAP = {
"HawkGenerator": HawkGenerator,
@ -111,13 +113,14 @@ def get_faction_possible_sams_generator(faction: str) -> List[Type[GroupGenerato
return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP.keys()]
def generate_anti_air_group(game, parent_cp, ground_object, faction: str):
def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject,
faction: str) -> Optional[VehicleGroup]:
"""
This generate a SAM group
:param parentCp: The parent control point
:param ground_object: The ground object which will own the sam group
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
:param game: The Game.
:param ground_object: The ground object which will own the sam group.
:param faction: Owner faction.
:return: The generated group, or None if one could not be generated.
"""
possible_sams_generators = get_faction_possible_sams_generator(faction)
if len(possible_sams_generators) > 0:
@ -128,7 +131,8 @@ def generate_anti_air_group(game, parent_cp, ground_object, faction: str):
return None
def generate_shorad_group(game, parent_cp, ground_object, faction_name: str):
def generate_shorad_group(game: Game, ground_object: TheaterGroundObject,
faction_name: str) -> Optional[VehicleGroup]:
faction = db.FACTIONS[faction_name]
if len(faction.shorads) > 0:
@ -137,4 +141,4 @@ def generate_shorad_group(game, parent_cp, ground_object, faction_name: str):
generator.generate()
return generator.get_generated_group()
else:
return generate_anti_air_group(game, parent_cp, ground_object, faction_name)
return generate_anti_air_group(game, ground_object, faction_name)

View File

@ -7,7 +7,7 @@ from PySide2 import QtWidgets
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen
from game import db, persistency
from game import db, persistency, VERSION
from qt_ui import (
liberation_install,
liberation_theme,
@ -20,7 +20,7 @@ from qt_ui.windows.preferences.QLiberationFirstStartWindow import \
QLiberationFirstStartWindow
# Logging setup
logging_config.init_logging(uiconstants.VERSION_STRING)
logging_config.init_logging(VERSION)
if __name__ == "__main__":
# Load eagerly to catch errors early.

View File

@ -7,10 +7,6 @@ from PySide2.QtGui import QColor, QFont, QPixmap
from theater.theatergroundobject import CATEGORY_MAP
from .liberation_theme import get_theme_icons
VERSION_STRING = "2.2.0-preview"
if Path("buildnumber").exists():
with open("buildnumber", "r") as file:
VERSION_STRING += f"-{file.readline()}"
URLS : Dict[str, str] = {
"Manual": "https://github.com/khopa/dcs_liberation/wiki",

View File

@ -1,7 +1,6 @@
import logging
import sys
import webbrowser
from typing import Optional, Union
from typing import Optional
from PySide2.QtCore import Qt
from PySide2.QtGui import QCloseEvent, QIcon
@ -10,14 +9,14 @@ from PySide2.QtWidgets import (
QActionGroup, QDesktopWidget,
QFileDialog,
QMainWindow,
QMenu, QMessageBox,
QMessageBox,
QSplitter,
QVBoxLayout,
QWidget,
)
import qt_ui.uiconstants as CONST
from game import Game, persistency
from game import Game, persistency, VERSION
from qt_ui.dialogs import Dialog
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
from qt_ui.models import GameModel
@ -47,7 +46,7 @@ class QLiberationWindow(QMainWindow):
self.setGame(persistency.restore_game())
self.setGeometry(300, 100, 270, 100)
self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING)
self.setWindowTitle(f"DCS Liberation - v{VERSION}")
self.setWindowIcon(QIcon("./resources/icon.png"))
self.statusBar().showMessage('Ready')
@ -228,7 +227,7 @@ class QLiberationWindow(QMainWindow):
self.liberation_map.setGame(game)
def showAboutDialog(self):
text = "<h3>DCS Liberation " + CONST.VERSION_STRING + "</h3>" + \
text = "<h3>DCS Liberation " + VERSION + "</h3>" + \
"<b>Source code :</b> https://github.com/khopa/dcs_liberation" + \
"<h4>Authors</h4>" + \
"<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>." \

View File

@ -1,6 +1,5 @@
from __future__ import unicode_literals
import datetime
import logging
from typing import List, Optional
@ -10,15 +9,14 @@ from PySide2.QtWidgets import QVBoxLayout
from dcs.task import CAP, CAS
import qt_ui.uiconstants as CONST
from game import Game, db
from game import db
from game.settings import Settings
from gen import namegen
from qt_ui.windows.newgame.QCampaignList import (
Campaign,
QCampaignList,
load_campaigns,
)
from theater import ConflictTheater, start_generator
from theater.start_generator import GameGenerator
class NewGameWizard(QtWidgets.QWizard):
@ -76,39 +74,13 @@ class NewGameWizard(QtWidgets.QWizard):
settings.do_not_generate_player_navy = no_player_navy
settings.do_not_generate_enemy_navy = no_enemy_navy
self.generatedGame = self.start_new_game(player_name, enemy_name, conflictTheater, midGame, multiplier,
timePeriod, settings, starting_money)
generator = GameGenerator(player_name, enemy_name, conflictTheater,
settings, timePeriod, starting_money,
multiplier, midGame)
self.generatedGame = generator.generate()
super(NewGameWizard, self).accept()
def start_new_game(self, player_name: str, enemy_name: str, conflictTheater: ConflictTheater,
midgame: bool, multiplier: float, period: datetime, settings:Settings, starting_money: int):
# Reset name generator
namegen.reset()
start_generator.prepare_theater(conflictTheater, settings, midgame)
print("-- Starting New Game Generator")
print("Enemy name : " + enemy_name)
print("Player name : " + player_name)
print("Midgame : " + str(midgame))
start_generator.generate_initial_units(conflictTheater, enemy_name, True, multiplier)
print("-- Initial units generated")
game = Game(player_name=player_name,
enemy_name=enemy_name,
theater=conflictTheater,
start_date=period,
settings=settings)
print("-- Game Object generated")
start_generator.generate_groundobjects(conflictTheater, game)
game.budget = starting_money
game.settings.multiplier = multiplier
game.settings.sams = True
game.settings.version = CONST.VERSION_STRING
return game
class IntroPage(QtWidgets.QWizardPage):
def __init__(self, parent=None):

View File

@ -35,7 +35,6 @@ class ControlPoint(MissionTarget):
position = None # type: Point
name = None # type: str
allow_sea_units = True
captured = False
has_frontline = True
@ -47,10 +46,9 @@ class ControlPoint(MissionTarget):
at: db.StartingPosition, radials: List[int], size: int,
importance: float, has_frontline=True,
cptype=ControlPointType.AIRBASE):
super().__init__(" ".join(re.split(r" |-", name)[:2]), position)
self.id = id
self.name = " ".join(re.split(r" |-", name)[:2])
self.full_name = name
self.position: Point = position
self.at = at
self.ground_objects: List[TheaterGroundObject] = []
@ -228,10 +226,8 @@ class ControlPoint(MissionTarget):
# Handle cyclic dependency.
from .start_generator import generate_airbase_defense_group
airbase_def_id = 0
for ground_object in self.ground_objects:
for idx, ground_object in enumerate(self.ground_objects):
ground_object.groups = []
if ground_object.airbase_group and faction_name != "":
generate_airbase_defense_group(airbase_def_id, ground_object,
faction_name, game, self)
airbase_def_id = airbase_def_id + 1
generate_airbase_defense_group(idx, ground_object,
faction_name, game)

View File

@ -4,11 +4,27 @@ from typing import Tuple
from dcs.mapping import Point
from . import ControlPoint, MissionTarget
# TODO: Dedup by moving everything to using this class.
FRONTLINE_MIN_CP_DISTANCE = 5000
def compute_position(control_point_a: ControlPoint,
control_point_b: ControlPoint) -> Point:
a = control_point_a.position
b = control_point_b.position
attack_heading = a.heading_between_point(b)
attack_distance = a.distance_to_point(b)
middle_point = a.point_from_heading(attack_heading, attack_distance / 2)
strength_delta = float(control_point_a.base.strength -
control_point_b.base.strength)
position = middle_point.point_from_heading(attack_heading,
strength_delta *
attack_distance / 2 -
FRONTLINE_MIN_CP_DISTANCE)
return position
class FrontLine(MissionTarget):
"""Defines a front line location between two control points.
@ -17,6 +33,8 @@ class FrontLine(MissionTarget):
def __init__(self, control_point_a: ControlPoint,
control_point_b: ControlPoint) -> None:
super().__init__(f"Front line {control_point_a}/{control_point_b}",
compute_position(control_point_a, control_point_b))
self.control_point_a = control_point_a
self.control_point_b = control_point_b
@ -24,22 +42,3 @@ class FrontLine(MissionTarget):
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b
@property
def name(self) -> str:
a = self.control_point_a.name
b = self.control_point_b.name
return f"Front line {a}/{b}"
@property
def position(self) -> Point:
a = self.control_point_a.position
b = self.control_point_b.position
attack_heading = a.heading_between_point(b)
attack_distance = a.distance_to_point(b)
middle_point = a.point_from_heading(attack_heading, attack_distance / 2)
strength_delta = (self.control_point_a.base.strength - self.control_point_b.base.strength) / 1.0
position = middle_point.point_from_heading(attack_heading,
strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE)
return position

View File

@ -1,22 +1,18 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dcs.mapping import Point
class MissionTarget(ABC):
# TODO: These should just be required objects to the constructor
# The TheatherGroundObject class is difficult to modify because it's
# generated data that's pickled ahead of time.
@property
@abstractmethod
def name(self) -> str:
"""The name of the mission target."""
class MissionTarget:
def __init__(self, name: str, position: Point) -> None:
"""Initializes a mission target.
@property
@abstractmethod
def position(self) -> Point:
"""The location of the mission target."""
Args:
name: The name of the mission target.
position: The location of the mission target.
"""
self.name = name
self.position = position
def distance_to(self, other: MissionTarget) -> int:
"""Computes the distance to the given mission target."""

View File

@ -1,16 +1,19 @@
from __future__ import annotations
import logging
import math
import pickle
import random
import typing
from typing import Any, Dict, List, Optional
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from game import db
from game.data.building_data import DEFAULT_AVAILABLE_BUILDINGS
from game import Game, db
from game.factions.faction import Faction
from game.settings import Settings
from game.version import VERSION
from gen import namegen
from gen.defenses.armor_group_generator import generate_armor_group
from gen.fleet.ship_group_generator import (
@ -30,6 +33,13 @@ from theater import (
TheaterGroundObject,
)
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from theater.theatergroundobject import (
SamGroundObject, BuildingGroundObject, CarrierGroundObject,
LhaGroundObject,
MissileSiteGroundObject, ShipGroundObject,
)
GroundObjectTemplates = Dict[str, Dict[str, Any]]
UNIT_VARIETY = 6
UNIT_AMOUNT_FACTOR = 16
@ -43,215 +53,387 @@ COUNT_BY_TASK = {
}
def generate_initial_units(theater: ConflictTheater, enemy_country: str, sams: bool, multiplier: float):
for cp in theater.enemy_points():
if cp.captured:
continue
class GameGenerator:
def __init__(self, player: str, enemy: str, theater: ConflictTheater,
settings: Settings, start_date, starting_budget: int,
multiplier: float, midgame: bool) -> None:
self.player = player
self.enemy = enemy
self.theater = theater
self.settings = settings
self.start_date = start_date
self.starting_budget = starting_budget
self.multiplier = multiplier
self.midgame = midgame
def generate(self) -> Game:
# Reset name generator
namegen.reset()
self.prepare_theater()
self.populate_red_airbases()
game = Game(player_name=self.player,
enemy_name=self.enemy,
theater=self.theater,
start_date=self.start_date,
settings=self.settings)
GroundObjectGenerator(game).generate()
game.budget = self.starting_budget
game.settings.multiplier = self.multiplier
game.settings.sams = True
game.settings.version = VERSION
return game
def prepare_theater(self) -> None:
to_remove = []
# Auto-capture half the bases if midgame.
if self.midgame:
control_points = self.theater.controlpoints
for control_point in control_points[:len(control_points) // 2]:
control_point.captured = True
# Remove carrier and lha, invert situation if needed
for cp in self.theater.controlpoints:
no_carrier = self.settings.do_not_generate_carrier
no_lha = self.settings.do_not_generate_lha
if cp.cptype is ControlPointType.AIRCRAFT_CARRIER_GROUP and \
no_carrier:
to_remove.append(cp)
elif cp.cptype is ControlPointType.LHA_GROUP and no_lha:
to_remove.append(cp)
if self.settings.inverted:
cp.captured = cp.captured_invert
# do remove
for cp in to_remove:
self.theater.controlpoints.remove(cp)
# TODO: Fix this. This captures all bases for blue.
# reapply midgame inverted if needed
if self.midgame and self.settings.inverted:
for i, cp in enumerate(reversed(self.theater.controlpoints)):
if i > len(self.theater.controlpoints):
break
else:
cp.captured = True
def populate_red_airbases(self) -> None:
for control_point in self.theater.enemy_points():
if control_point.captured:
continue
self.populate_red_airbase(control_point)
def populate_red_airbase(self, control_point: ControlPoint) -> None:
# Force reset cp on generation
cp.base.aircraft = {}
cp.base.armor = {}
cp.base.aa = {}
cp.base.commision_points = {}
cp.base.strength = 1
control_point.base.aircraft = {}
control_point.base.armor = {}
control_point.base.aa = {}
control_point.base.commision_points = {}
control_point.base.strength = 1
for task in [PinpointStrike, CAP, CAS, AirDefence]:
assert cp.importance <= IMPORTANCE_HIGH, "invalid importance {}".format(cp.importance)
assert cp.importance >= IMPORTANCE_LOW, "invalid importance {}".format(cp.importance)
if IMPORTANCE_HIGH <= control_point.importance <= IMPORTANCE_LOW:
raise ValueError(
f"CP importance must be between {IMPORTANCE_LOW} and "
f"{IMPORTANCE_HIGH}, is {control_point.importance}")
importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
variety = int(UNIT_VARIETY)
unittypes = db.choose_units(task, importance_factor, variety, enemy_country)
if not sams and task == AirDefence:
unittypes = [x for x in db.find_unittype(AirDefence, enemy_country) if x not in db.SAM_BAN]
count_log = math.log(cp.importance + 0.01, UNIT_COUNT_IMPORTANCE_LOG)
count = max(COUNT_BY_TASK[task] * multiplier * (1+count_log), 1)
if len(unittypes) > 0:
count_per_type = max(int(float(count) / len(unittypes)), 1)
for unit_type in unittypes:
logging.info("{} - {} {}".format(cp.name, db.unit_type_name(unit_type), count_per_type))
cp.base.commision_units({unit_type: count_per_type})
def generate_groundobjects(theater: ConflictTheater, game):
with open("resources/groundobject_templates.p", "rb") as f:
tpls = pickle.load(f)
group_id = 0
cp_to_remove = []
for cp in theater.controlpoints:
group_id = generate_cp_ground_points(cp, theater, game, group_id, tpls)
# CP
if cp.captured:
faction_name = game.player_name
else:
faction_name = game.enemy_name
faction = db.FACTIONS[faction_name]
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
# Create ground object group
group_id = game.next_group_id()
g = TheaterGroundObject("CARRIER")
g.group_id = group_id
g.object_id = 0
g.cp_id = cp.id
g.airbase_group = True
g.dcs_identifier = "CARRIER"
g.sea_object = True
g.obj_name = namegen.random_objective_name()
g.heading = 0
g.position = Point(cp.position.x, cp.position.y)
group = generate_carrier_group(faction_name, game, g)
g.groups = []
if group is not None:
g.groups.append(group)
cp.ground_objects.append(g)
# Set new name :
if len(faction.carrier_names) > 0:
cp.name = random.choice(faction.carrier_names)
else:
cp_to_remove.append(cp)
elif cp.cptype == ControlPointType.LHA_GROUP:
# Create ground object group
group_id = game.next_group_id()
g = TheaterGroundObject("LHA")
g.group_id = group_id
g.object_id = 0
g.cp_id = cp.id
g.airbase_group = True
g.dcs_identifier = "LHA"
g.sea_object = True
g.obj_name = namegen.random_objective_name()
g.heading = 0
g.position = Point(cp.position.x, cp.position.y)
group = generate_lha_group(faction_name, game, g)
g.groups = []
if group is not None:
g.groups.append(group)
cp.ground_objects.append(g)
# Set new name :
if len(faction.helicopter_carrier_names) > 0:
cp.name = random.choice(faction.helicopter_carrier_names)
else:
cp_to_remove.append(cp)
else:
for i in range(random.randint(3, 6)):
logging.info("GENERATE BASE DEFENSE")
point = find_location(True, cp.position, theater, 800, 3200, [], True)
logging.info(point)
if point is None:
logging.info("Couldn't find point for {} base defense".format(cp))
continue
group_id = game.next_group_id()
g = TheaterGroundObject("aa")
g.group_id = group_id
g.object_id = 0
g.cp_id = cp.id
g.airbase_group = True
g.dcs_identifier = "AA"
g.sea_object = False
g.obj_name = namegen.random_objective_name()
g.heading = 0
g.position = Point(point.x, point.y)
generate_airbase_defense_group(i, g, faction_name, game, cp)
cp.ground_objects.append(g)
logging.info("---------------------------")
logging.info("CP Generation : " + cp.name)
for ground_object in cp.ground_objects:
logging.info(ground_object.groups)
# Generate navy groups
if len(faction.navy_generators) > 0 and cp.allow_sea_units:
if cp.captured and game.settings.do_not_generate_player_navy:
continue
if not cp.captured and game.settings.do_not_generate_enemy_navy:
importance_factor = ((control_point.importance - IMPORTANCE_LOW) /
(IMPORTANCE_HIGH - IMPORTANCE_LOW))
# noinspection PyTypeChecker
unit_types = db.choose_units(task, importance_factor, UNIT_VARIETY,
self.enemy)
if not unit_types:
continue
for i in range(faction.navy_group_count):
count_log = math.log(control_point.importance + 0.01,
UNIT_COUNT_IMPORTANCE_LOG)
count = max(
COUNT_BY_TASK[task] * self.multiplier * (1 + count_log), 1
)
point = find_location(False, cp.position, theater, 5000, 40000, [], False)
if point is None:
logging.info("Couldn't find point for {} ships".format(cp))
continue
group_id = game.next_group_id()
g = TheaterGroundObject("aa")
g.group_id = group_id
g.object_id = 0
g.cp_id = cp.id
g.airbase_group = False
g.dcs_identifier = "AA"
g.sea_object = True
g.obj_name = namegen.random_objective_name()
g.heading = 0
g.position = Point(point.x, point.y)
group = generate_ship_group(game, g, faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
cp.ground_objects.append(g)
if len(faction.missiles) > 0:
for i in range(faction.missiles_group_count):
point = find_location(True, cp.position, theater, 2500, 40000, [], False)
if point is None:
logging.info("Couldn't find point for {} missiles".format(cp))
continue
group_id = game.next_group_id()
g = TheaterGroundObject("aa")
g.group_id = group_id
g.object_id = 0
g.cp_id = cp.id
g.airbase_group = False
g.dcs_identifier = "AA"
g.sea_object = False
g.obj_name = namegen.random_objective_name()
g.heading = 0
g.position = Point(point.x, point.y)
group = generate_missile_group(game, g, faction_name)
g.groups = []
if group is not None:
g.groups.append(group)
cp.ground_objects.append(g)
for cp in cp_to_remove:
theater.controlpoints.remove(cp)
count_per_type = max(int(float(count) / len(unit_types)), 1)
for unit_type in unit_types:
control_point.base.commision_units({unit_type: count_per_type})
class ControlPointGroundObjectGenerator:
def __init__(self, game: Game, control_point: ControlPoint,
templates: GroundObjectTemplates) -> None:
self.game = game
self.control_point = control_point
self.templates = templates
def generate_airbase_defense_group(airbase_defense_group_id, ground_obj:TheaterGroundObject, faction, game, cp):
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_name
else:
return self.game.enemy_name
logging.info("GENERATE AIR DEFENSE GROUP")
logging.info(faction)
logging.info(airbase_defense_group_id)
@property
def faction(self) -> Faction:
return db.FACTIONS[self.faction_name]
def generate(self) -> bool:
self.control_point.ground_objects = []
self.generate_ground_points()
if self.faction.navy_generators:
# Even airbases can generate navies if they are close enough to the
# water. This is not controlled by the control point definition, but
# rather by whether or not the generator can find a valid position
# for the ship.
self.generate_navy()
if self.faction.missiles:
# TODO: Presumably only for airbases?
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
# 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:
skip_player_navy = self.game.settings.do_not_generate_player_navy
if self.control_point.captured and skip_player_navy:
return
skip_enemy_navy = self.game.settings.do_not_generate_enemy_navy
if not self.control_point.captured and skip_enemy_navy:
return
for _ in range(self.faction.navy_group_count):
self.generate_ship()
def generate_ship(self) -> None:
point = find_location(False, self.control_point.position,
self.game.theater, 5000, 40000, [], False)
if point is None:
logging.error(
f"Could not find point for {self.control_point}'s navy")
return
group_id = self.game.next_group_id()
g = ShipGroundObject(namegen.random_objective_name(), group_id, point,
self.control_point)
group = generate_ship_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)
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):
def generate(self) -> bool:
if not super().generate():
return False
carrier_names = self.faction.carrier_names
if not carrier_names:
logging.info(
f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no carriers")
return False
# Create ground object group
group_id = self.game.next_group_id()
g = CarrierGroundObject(namegen.random_objective_name(), group_id,
self.control_point)
group = generate_carrier_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.ground_objects.append(g)
self.control_point.name = random.choice(carrier_names)
return True
class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate(self) -> bool:
if not super().generate():
return False
lha_names = self.faction.helicopter_carrier_names
if not lha_names:
logging.info(
f"Skipping generation of {self.control_point.name} because "
f"{self.faction_name} has no LHAs")
return False
# Create ground object group
group_id = self.game.next_group_id()
g = LhaGroundObject(namegen.random_objective_name(), group_id,
self.control_point)
group = generate_lha_group(self.faction_name, self.game, g)
g.groups = []
if group is not None:
g.groups.append(group)
self.control_point.ground_objects.append(g)
self.control_point.name = random.choice(lha_names)
return True
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def generate(self) -> bool:
if not super().generate():
return False
for i in range(random.randint(3, 6)):
self.generate_sam(i)
return True
def generate_sam(self, index: int) -> None:
position = find_location(True, self.control_point.position,
self.game.theater, 800, 3200, [], True)
if position is None:
logging.error("Could not find position for "
f"{self.control_point} base defense")
return
group_id = self.game.next_group_id()
g = SamGroundObject(namegen.random_objective_name(), group_id,
position, self.control_point, for_airbase=True)
generate_airbase_defense_group(index, g, self.faction_name, self.game)
self.control_point.ground_objects.append(g)
class GroundObjectGenerator:
def __init__(self, game: Game) -> None:
self.game = game
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
# the iterator.
control_points = list(self.game.theater.controlpoints)
for control_point in control_points:
if not self.generate_for_control_point(control_point):
self.game.theater.controlpoints.remove(control_point)
def generate_for_control_point(self, control_point: ControlPoint) -> bool:
generator: ControlPointGroundObjectGenerator
if control_point.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
generator = CarrierGroundObjectGenerator(self.game, control_point,
self.templates)
elif control_point.cptype == ControlPointType.LHA_GROUP:
generator = LhaGroundObjectGenerator(self.game, control_point,
self.templates)
else:
generator = AirbaseGroundObjectGenerator(self.game, control_point,
self.templates)
return generator.generate()
def generate_airbase_defense_group(airbase_defense_group_id: int,
ground_obj: TheaterGroundObject,
faction: str, game: Game) -> None:
if airbase_defense_group_id == 0:
group = generate_armor_group(faction, game, ground_obj)
elif airbase_defense_group_id == 1 and random.randint(0, 1) == 0:
group = generate_anti_air_group(game, cp, ground_obj, faction)
group = generate_anti_air_group(game, ground_obj, faction)
elif random.randint(0, 2) == 1:
group = generate_shorad_group(game, cp, ground_obj, faction)
group = generate_shorad_group(game, ground_obj, faction)
else:
group = generate_armor_group(faction, game, ground_obj)
@ -260,22 +442,31 @@ def generate_airbase_defense_group(airbase_defense_group_id, ground_obj:TheaterG
ground_obj.groups.append(group)
def find_location(on_ground, near, theater, min, max, others, is_base_defense=False) -> typing.Optional[Point]:
# TODO: https://stackoverflow.com/a/19482012/632035
# A lot of the time spent on mission generation is spent in this function since
# just randomly guess up to 1800 times and often fail. This is particularly
# problematic while trying to find placement for navies in Nevada.
def find_location(on_ground: bool, near: Point, theater: ConflictTheater,
min_range: int, max_range: int,
others: List[TheaterGroundObject],
is_base_defense: bool = False) -> Optional[Point]:
"""
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on ground)
:param on_ground: Whether it should be on ground or on sea (True = on
ground)
:param near: Point
:param theater: Theater object
:param min: Minimal range from point
:param max: Max range from point
:param min_range: Minimal range from point
:param max_range: Max range from point
:param others: Other already existing ground objects
:param is_base_defense: True if the location is for base defense.
:return:
"""
point = None
for _ in range(300):
# Check if on land or sea
p = near.random_point_within(max, min)
p = near.random_point_within(max_range, min_range)
if on_ground and theater.is_on_land(p):
point = p
elif not on_ground and theater.is_in_sea(p):
@ -297,15 +488,16 @@ def find_location(on_ground, near, theater, min, max, others, is_base_defense=Fa
break
if point:
for other in theater.controlpoints:
if is_base_defense: break
if other.position != near:
for control_point in theater.controlpoints:
if is_base_defense:
break
if control_point.position != near:
if point is None:
break
if other.position.distance_to_point(point) < 30000:
if control_point.position.distance_to_point(point) < 30000:
point = None
break
for ground_obj in other.ground_objects:
for ground_obj in control_point.ground_objects:
if ground_obj.position.distance_to_point(point) < 10000:
point = None
break
@ -313,114 +505,3 @@ def find_location(on_ground, near, theater, min, max, others, is_base_defense=Fa
if point:
return point
return None
def generate_cp_ground_points(cp: ControlPoint, theater, game, group_id, templates):
"""
Generate inital ground objects and AA site for given control point
:param cp: Control point to initialize
:param theater: Theater
:param game: Game object
:param group_id: Group id
:param templates: Ground object templates
:return: True if something was generated
"""
# Reset cp ground objects
cp.ground_objects = []
if cp.is_global:
return False
if cp.captured:
faction = game.player_name
else:
faction = game.enemy_name
faction_data = db.FACTIONS[faction]
available_categories = faction_data.building_set
if len(available_categories) == 0:
return False
amount = random.randrange(3, 8)
for i in range(0, amount):
obj_name = namegen.random_objective_name()
if i >= amount - 1:
tpl_category = "aa"
else:
if random.randint(0, 3) == 0:
tpl_category = "aa"
else:
tpl_category = random.choice(available_categories)
tpl = random.choice(list(templates[tpl_category].values()))
point = find_location(tpl_category != "oil", cp.position, theater, 10000, 40000, cp.ground_objects)
if point is None:
logging.info("Couldn't find point for {}".format(cp))
continue
object_id = 0
group_id = game.next_group_id()
logging.info("generated {} for {}".format(tpl_category, cp))
for object in tpl:
object_id += 1
g = TheaterGroundObject(tpl_category)
g.group_id = group_id
g.object_id = object_id
g.cp_id = cp.id
g.airbase_group = False
g.obj_name = obj_name
g.dcs_identifier = object["type"]
g.heading = object["heading"]
g.sea_object = False
g.position = Point(point.x + object["offset"].x, point.y + object["offset"].y)
if g.dcs_identifier == "AA":
g.groups = []
group = generate_anti_air_group(game, cp, g, faction)
if group is not None:
g.groups.append(group)
cp.ground_objects.append(g)
return group_id
def prepare_theater(theater: ConflictTheater, settings:Settings, midgame):
to_remove = []
# autocapture half the base if midgame
if midgame:
for i in range(0, int(len(theater.controlpoints) / 2)):
theater.controlpoints[i].captured = True
# Remove carrier and lha, invert situation if needed
for cp in theater.controlpoints:
if cp.cptype is ControlPointType.AIRCRAFT_CARRIER_GROUP and settings.do_not_generate_carrier:
to_remove.append(cp)
elif cp.cptype is ControlPointType.LHA_GROUP and settings.do_not_generate_lha:
to_remove.append(cp)
if settings.inverted:
cp.captured = cp.captured_invert
# do remove
for cp in to_remove:
theater.controlpoints.remove(cp)
# reapply midgame inverted if needed
if midgame and settings.inverted:
for i, cp in enumerate(reversed(theater.controlpoints)):
if i > len(theater.controlpoints):
break
else:
cp.captured = True

View File

@ -1,4 +1,5 @@
import uuid
from __future__ import annotations
from typing import List, TYPE_CHECKING
from dcs.mapping import Point
@ -68,21 +69,21 @@ CATEGORY_MAP = {
class TheaterGroundObject(MissionTarget):
cp_id = 0
group_id = 0
object_id = 0
dcs_identifier = None # type: str
is_dead = False
airbase_group = False
heading = 0
position = None # type: Point
groups: List[Group] = []
obj_name = ""
sea_object = False
uuid = uuid.uuid1()
def __init__(self, category: str):
def __init__(self, name: str, category: str, group_id: int, object_id: int,
position: Point, heading: int, cp_id: int, dcs_identifier: str,
airbase_group: bool, sea_object: bool) -> None:
super().__init__(name, position)
self.category = category
self.group_id = group_id
self.object_id = object_id
self.heading = heading
self.cp_id = cp_id
self.dcs_identifier = dcs_identifier
self.airbase_group = airbase_group
self.sea_object = sea_object
self.is_dead = False
self.groups: List[Group] = []
@property
def string_identifier(self):
@ -96,20 +97,128 @@ class TheaterGroundObject(MissionTarget):
def name_abbrev(self) -> str:
return ABBREV_NAME[self.category]
def __str__(self):
def __str__(self) -> str:
return NAME_BY_CATEGORY[self.category]
def matches_string_identifier(self, id):
return self.string_identifier == id
def matches_string_identifier(self, identifier):
return self.string_identifier == identifier
@property
def name(self) -> str:
return self.obj_name
def obj_name(self) -> str:
return self.name
def parent_control_point(
self, theater: "ConflictTheater") -> "ControlPoint":
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):
def __init__(self, name: str, category: str, group_id: int, object_id: int,
position: Point, heading: int, control_point: ControlPoint,
dcs_identifier: str) -> None:
super().__init__(
name=name,
category=category,
group_id=group_id,
object_id=object_id,
position=position,
heading=heading,
cp_id=control_point.id,
dcs_identifier=dcs_identifier,
airbase_group=False,
sea_object=False
)
class GenericCarrierGroundObject(TheaterGroundObject):
pass
# TODO: Why is this both a CP and a TGO?
class CarrierGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int,
control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="CARRIER",
group_id=group_id,
object_id=0,
position=control_point.position,
heading=0,
cp_id=control_point.id,
dcs_identifier="CARRIER",
airbase_group=True,
sea_object=True
)
# TODO: Why is this both a CP and a TGO?
class LhaGroundObject(GenericCarrierGroundObject):
def __init__(self, name: str, group_id: int,
control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="LHA",
group_id=group_id,
object_id=0,
position=control_point.position,
heading=0,
cp_id=control_point.id,
dcs_identifier="LHA",
airbase_group=True,
sea_object=True
)
class MissileSiteGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="aa",
group_id=group_id,
object_id=0,
position=position,
heading=0,
cp_id=control_point.id,
dcs_identifier="AA",
airbase_group=False,
sea_object=False
)
class SamGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint, for_airbase: bool) -> None:
super().__init__(
name=name,
category="aa",
group_id=group_id,
object_id=0,
position=position,
heading=0,
cp_id=control_point.id,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False
)
class ShipGroundObject(TheaterGroundObject):
def __init__(self, name: str, group_id: int, position: Point,
control_point: ControlPoint) -> None:
super().__init__(
name=name,
category="aa",
group_id=group_id,
object_id=0,
position=position,
heading=0,
cp_id=control_point.id,
dcs_identifier="AA",
airbase_group=False,
sea_object=True
)