From f6e0dbbb6a4c5ce56df13b8aaf3ee46631f531b1 Mon Sep 17 00:00:00 2001 From: walterroach <37820425+walterroach@users.noreply.github.com> Date: Sat, 21 Nov 2020 17:00:22 -0600 Subject: [PATCH] operation refactoring --- game/event/event.py | 2 +- game/event/frontlineattack.py | 15 +- game/operation/operation.py | 397 ++++++++++++++++++---------------- game/theater/controlpoint.py | 3 + gen/aircraft.py | 3 +- gen/armor.py | 29 ++- gen/conflictgen.py | 18 ++ gen/forcedoptionsgen.py | 9 +- gen/groundobjectsgen.py | 26 +-- gen/triggergen.py | 2 +- gen/visualgen.py | 3 +- qt_ui/widgets/QTopPanel.py | 3 +- 12 files changed, 272 insertions(+), 238 deletions(-) diff --git a/game/event/event.py b/game/event/event.py index db8e5ee63..acc929fef 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -92,7 +92,7 @@ def generate(self): self.operation.is_awacs_enabled = self.is_awacs_enabled self.operation.ca_slots = self.ca_slots - self.operation.prepare(self.game.theater.terrain, is_quick=False) + self.operation.prepare(self.game) self.operation.generate() self.operation.current_mission.save(persistency.mission_path_for("liberation_nextturn.miz")) self.environment_settings = self.operation.environment_settings diff --git a/game/event/frontlineattack.py b/game/event/frontlineattack.py index e38c45f60..fa3d34160 100644 --- a/game/event/frontlineattack.py +++ b/game/event/frontlineattack.py @@ -1,11 +1,10 @@ from typing import List, Type from dcs.task import CAP, CAS, Task +from game.operation.operation import Operation -from game import db -# from game.operation.frontlineattack import FrontlineAttackOperation -from .event import Event from ..debriefing import Debriefing +from .event import Event class FrontlineAttackEvent(Event): @@ -38,12 +37,6 @@ def skip(self): if self.to_cp.captured: self.to_cp.base.affect_strength(-0.1) - def player_attacking(self, flights: db.TaskForceDict): + def player_attacking(self): assert self.departure_cp is not None - op = FrontlineAttackOperation(game=self.game, - attacker_name=self.attacker_name, - defender_name=self.defender_name, - from_cp=self.from_cp, - departure_cp=self.departure_cp, - to_cp=self.to_cp) - self.operation = op + self.operation = Operation(departure_cp=self.departure_cp,) diff --git a/game/operation/operation.py b/game/operation/operation.py index 1e01065b4..32c131986 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import logging import os from pathlib import Path -from typing import List, Optional, Set +from typing import TYPE_CHECKING, Iterable, List, Optional, Set from dcs import Mission from dcs.action import DoScript, DoScriptFile @@ -9,11 +11,9 @@ from dcs.countries import country_dict from dcs.lua.parse import loads from dcs.mapping import Point -from dcs.terrain.terrain import Terrain from dcs.translation import String from dcs.triggers import TriggerStart from dcs.unittype import UnitType - from game.plugins import LuaPluginManager from game.theater import ControlPoint from gen import Conflict, FlightType, VisualGenerator @@ -30,18 +30,19 @@ from gen.radios import RadioFrequency, RadioRegistry from gen.tacan import TacanRegistry from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator + from .. import db from ..debriefing import Debriefing +if TYPE_CHECKING: + from game import Game + class Operation: attackers_starting_position = None # type: db.StartingPosition defenders_starting_position = None # type: db.StartingPosition current_mission = None # type: Mission - regular_mission = None # type: Mission - quick_mission = None # type: Mission - conflict = None # type: Conflict airgen = None # type: AircraftConflictGenerator triggersgen = None # type: TriggersGenerator airsupportgen = None # type: AirSupportConflictGenerator @@ -51,7 +52,7 @@ class Operation: forcedoptionsgen = None # type: ForcedOptionsGenerator radio_registry: Optional[RadioRegistry] = None tacan_registry: Optional[TacanRegistry] = None - + game = None # type: Game environment_settings = None trigger_radius = TRIGGER_RADIUS_MEDIUM is_quick = None @@ -59,23 +60,35 @@ class Operation: ca_slots = 0 def __init__(self, - game, - attacker_name: str, - defender_name: str, - from_cp: ControlPoint, departure_cp: ControlPoint, - to_cp: ControlPoint): - self.game = game - self.attacker_name = attacker_name - self.attacker_country = db.FACTIONS[attacker_name].country - self.defender_name = defender_name - self.defender_country = db.FACTIONS[defender_name].country - print(self.defender_country, self.attacker_country) - self.from_cp = from_cp + ): self.departure_cp = departure_cp - self.to_cp = to_cp - self.is_quick = False self.plugin_scripts: List[str] = [] + self.jtacs: List[JtacInfo] = [] + + @classmethod + def prepare(cls, game: Game): + with open("resources/default_options.lua", "r") as f: + options_dict = loads(f.read())["options"] + cls._set_mission(Mission(game.theater.terrain)) + cls.game = game + cls._setup_mission_coalitions() + cls.current_mission.options.load_from_dict(options_dict) + + @classmethod + def conflicts(cls) -> Iterable[Conflict]: + assert cls.game + for frontline in cls.game.theater.conflicts(): + yield Conflict( + cls.game.theater, + frontline.control_point_a, + frontline.control_point_b, + cls.game.player_name, + cls.game.enemy_name, + cls.game.player_country, + cls.game.enemy_country, + frontline.position + ) def units_of(self, country_name: str) -> List[UnitType]: return [] @@ -87,51 +100,21 @@ def is_successfull(self, debriefing: Debriefing) -> bool: def is_player_attack(self) -> bool: return self.from_cp.captured - def initialize(self, mission: Mission, conflict: Conflict): - self.current_mission = mission - self.conflict = conflict - # self.briefinggen = BriefingGenerator(self.current_mission, self.game) Is it safe to remove this, or does it also break save compat? - - def prepare(self, terrain: Terrain, is_quick: bool): - with open("resources/default_options.lua", "r") as f: - options_dict = loads(f.read())["options"] - - self.current_mission = Mission(terrain) - - print(self.game.player_country) - print(country_dict[db.country_id_from_name(self.game.player_country)]) - print(country_dict[db.country_id_from_name(self.game.player_country)]()) - - # Setup coalition : - self.current_mission.coalition["blue"] = Coalition("blue") - self.current_mission.coalition["red"] = Coalition("red") + @classmethod + def _set_mission(cls, mission: Mission) -> None: + cls.current_mission = mission - p_country = self.game.player_country - e_country = self.game.enemy_country - self.current_mission.coalition["blue"].add_country(country_dict[db.country_id_from_name(p_country)]()) - self.current_mission.coalition["red"].add_country(country_dict[db.country_id_from_name(e_country)]()) + @classmethod + def _setup_mission_coalitions(cls): + cls.current_mission.coalition["blue"] = Coalition("blue") + cls.current_mission.coalition["red"] = Coalition("red") - print([c for c in self.current_mission.coalition["blue"].countries.keys()]) - print([c for c in self.current_mission.coalition["red"].countries.keys()]) - - if is_quick: - self.quick_mission = self.current_mission - else: - self.regular_mission = self.current_mission - - self.current_mission.options.load_from_dict(options_dict) - self.is_quick = is_quick - - if is_quick: - self.attackers_starting_position = None - self.defenders_starting_position = None - else: - self.attackers_starting_position = self.departure_cp.at - # TODO: Is this possible? - if self.to_cp is not None: - self.defenders_starting_position = self.to_cp.at - else: - self.defenders_starting_position = None + p_country = cls.game.player_country + e_country = cls.game.enemy_country + cls.current_mission.coalition["blue"].add_country( + country_dict[db.country_id_from_name(p_country)]()) + cls.current_mission.coalition["red"].add_country( + country_dict[db.country_id_from_name(e_country)]()) def inject_lua_trigger(self, contents: str, comment: str) -> None: trigger = TriggerStart(comment=comment) @@ -161,7 +144,8 @@ def inject_plugin_script(self, plugin_mnemonic: str, script: str, trigger = TriggerStart(comment=f"Load {script_mnemonic}") filename = script_path.resolve() - fileref = self.current_mission.map_resource.add_resource_file(filename) + fileref = self.current_mission.map_resource.add_resource_file( + filename) trigger.add_action(DoScriptFile(fileref)) self.current_mission.triggerrules.triggers.append(trigger) @@ -171,13 +155,13 @@ def notify_info_generators( airsupportgen: AirSupportConflictGenerator, jtacs: List[JtacInfo], airgen: AircraftConflictGenerator, - ): + ): """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings) """ gens: List[MissionInfoGenerator] = [ KneeboardGenerator(self.current_mission, self.game), BriefingGenerator(self.current_mission, self.game) - ] + ] for gen in gens: for dynamic_runway in groundobjectgen.runways.values(): gen.add_dynamic_runway(dynamic_runway) @@ -195,15 +179,46 @@ def notify_info_generators( for flight in airgen.flights: gen.add_flight(flight) gen.generate() + + @classmethod + def create_radio_registries(cls) -> None: + unique_map_frequencies = set() # type: Set[RadioFrequency] + cls._create_tacan_registry(unique_map_frequencies) + cls._create_radio_registry(unique_map_frequencies) + + def assign_channels_to_flights(self, flights: List[FlightData], + air_support: AirSupport) -> None: + """Assigns preset radio channels for client flights.""" + for flight in flights: + if not flight.client_units: + continue + self.assign_channels_to_flight(flight, air_support) - def generate(self): - radio_registry = RadioRegistry() - tacan_registry = TacanRegistry() + def assign_channels_to_flight(self, flight: FlightData, + air_support: AirSupport) -> None: + """Assigns preset radio channels for a client flight.""" + airframe = flight.aircraft_type + + try: + aircraft_data = AIRCRAFT_DATA[airframe.id] + except KeyError: + logging.warning(f"No aircraft data for {airframe.id}") + return + + if aircraft_data.channel_allocator is not None: + aircraft_data.channel_allocator.assign_channels_for_flight( + flight, air_support + ) + + @classmethod + def _create_tacan_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None: + """ + Dedup beacon/radio frequencies, since some maps have some frequencies + used multiple times. + """ + cls.tacan_registry = TacanRegistry() + beacons = load_beacons_for_terrain(cls.game.theater.terrain.name) - # Dedup beacon/radio frequencies, since some maps have some frequencies - # used multiple times. - beacons = load_beacons_for_terrain(self.game.theater.terrain.name) - unique_map_frequencies: Set[RadioFrequency] = set() for beacon in beacons: unique_map_frequencies.add(beacon.frequency) if beacon.is_tacan: @@ -211,36 +226,35 @@ def generate(self): logging.error( f"TACAN beacon has no channel: {beacon.callsign}") else: - tacan_registry.reserve(beacon.tacan_channel) + cls.tacan_registry.reserve(beacon.tacan_channel) - for airfield, data in AIRFIELD_DATA.items(): - if data.theater == self.game.theater.terrain.name: + @classmethod + def _create_radio_registry(cls, unique_map_frequencies: Set[RadioFrequency]) -> None: + cls.radio_registry = RadioRegistry() + for data in AIRFIELD_DATA.values(): + if data.theater == cls.game.theater.terrain.name: unique_map_frequencies.add(data.atc.hf) unique_map_frequencies.add(data.atc.vhf_fm) unique_map_frequencies.add(data.atc.vhf_am) unique_map_frequencies.add(data.atc.uhf) # No need to reserve ILS or TACAN because those are in the # beacon list. - + unique_map_frequencies: Set[RadioFrequency] = set() for frequency in unique_map_frequencies: - radio_registry.reserve(frequency) - - # Set mission time and weather conditions. - EnvironmentGenerator(self.current_mission, - self.game.conditions).generate() - - # Generate ground object first - - groundobjectgen = GroundObjectsGenerator( - self.current_mission, - self.conflict, - self.game, - radio_registry, - tacan_registry + cls.radio_registry.reserve(frequency) + + @classmethod + def _generate_ground_units(cls): + cls.groundobjectgen = GroundObjectsGenerator( + cls.current_mission, + cls.game, + cls.radio_registry, + cls.tacan_registry ) - groundobjectgen.generate() + cls.groundobjectgen.generate() - # Generate destroyed units + def _generate_destroyed_units(self) -> None: + """Add destroyed units to the Mission""" for d in self.game.get_destroyed_units(): try: utype = db.unit_type_from_name(d["type"]) @@ -250,7 +264,8 @@ def generate(self): pos = Point(d["x"], d["z"]) if utype is not None and not self.game.position_culled(pos) and self.game.settings.perf_destroyed_units: self.current_mission.static_group( - country=self.current_mission.country(self.game.player_country), + country=self.current_mission.country( + self.game.player_country), name="", _type=utype, hidden=True, @@ -258,44 +273,25 @@ def generate(self): heading=d["orientation"], dead=True, ) - - # Air Support (Tanker & Awacs) - airsupportgen = AirSupportConflictGenerator( - self.current_mission, self.conflict, self.game, radio_registry, - tacan_registry) - airsupportgen.generate(self.is_awacs_enabled) - - # Generate Activity on the map - airgen = AircraftConflictGenerator( - self.current_mission, self.conflict, self.game.settings, self.game, - radio_registry) - - airgen.generate_flights( - self.current_mission.country(self.game.player_country), - self.game.blue_ato, - groundobjectgen.runways - ) - airgen.generate_flights( - self.current_mission.country(self.game.enemy_country), - self.game.red_ato, - groundobjectgen.runways - ) - - # Generate ground units on frontline everywhere - jtacs: List[JtacInfo] = [] - for front_line in self.game.theater.conflicts(True): - player_cp = front_line.control_point_a - enemy_cp = front_line.control_point_b - conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name, - self.current_mission.country(self.attacker_country), - self.current_mission.country(self.defender_country), - player_cp, enemy_cp, self.game.theater) - # Generate frontline ops - player_gp = self.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id] - enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] - groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id]) - groundConflictGen.generate() - jtacs.extend(groundConflictGen.jtacs) + + def generate(self): + self.create_radio_registries() + # Set mission time and weather conditions. + EnvironmentGenerator(self.current_mission, + self.game.conditions).generate() + self._generate_ground_units() + self._generate_destroyed_units() + self._generate_air_units() + self.assign_channels_to_flights(self.airgen.flights, + self.airsupportgen.air_support) + self._generate_ground_conflicts() + + # TODO: This is silly, once Bulls position is defined without Conflict this should be removed. + default_conflict = [i for i in self.conflicts()][0] + # Triggers + triggersgen = TriggersGenerator(self.current_mission, default_conflict, + self.game) + triggersgen.generate() # Setup combined arms parameters self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0 @@ -304,34 +300,32 @@ def generate(self): else: self.current_mission.groundControl.red_tactical_commander = self.ca_slots - # Triggers - triggersgen = TriggersGenerator(self.current_mission, self.conflict, - self.game) - triggersgen.generate() - # Options - forcedoptionsgen = ForcedOptionsGenerator(self.current_mission, - self.conflict, self.game) + forcedoptionsgen = ForcedOptionsGenerator(self.current_mission, self.game) forcedoptionsgen.generate() # Generate Visuals Smoke Effects - visualgen = VisualGenerator(self.current_mission, self.conflict, - self.game) + visualgen = VisualGenerator(self.current_mission, self.game) if self.game.settings.perf_smoke_gen: visualgen.generate() - luaData = {} - luaData["AircraftCarriers"] = {} - luaData["Tankers"] = {} - luaData["AWACs"] = {} - luaData["JTACs"] = {} - luaData["TargetPoints"] = {} + self.notify_info_generators( + self.groundobjectgen, + self.airsupportgen, + self.jtacs, + self.airgen + ) - self.assign_channels_to_flights(airgen.flights, - airsupportgen.air_support) + luaData = { + "AircraftCarriers": {}, + "Tankers": {}, + "AWACs": {}, + "JTACs": {}, + "TargetPoints": {}, + } - for tanker in airsupportgen.air_support.tankers: - luaData["Tankers"][tanker.callsign] = { + for tanker in self.airsupportgen.air_support.tankers: + luaData["Tankers"][tanker.callsign] = { "dcsGroupName": tanker.dcsGroupName, "callsign": tanker.callsign, "variant": tanker.variant, @@ -340,15 +334,15 @@ def generate(self): } if self.is_awacs_enabled: - for awacs in airsupportgen.air_support.awacs: - luaData["AWACs"][awacs.callsign] = { + for awacs in self.airsupportgen.air_support.awacs: + luaData["AWACs"][awacs.callsign] = { "dcsGroupName": awacs.dcsGroupName, "callsign": awacs.callsign, "radio": awacs.freq.mhz } - for jtac in jtacs: - luaData["JTACs"][jtac.callsign] = { + for jtac in self.jtacs: + luaData["JTACs"][jtac.callsign] = { "dcsGroupName": jtac.dcsGroupName, "callsign": jtac.callsign, "zone": jtac.region, @@ -356,7 +350,7 @@ def generate(self): "laserCode": jtac.code } - for flight in airgen.flights: + for flight in self.airgen.flights: if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]: flightType = flight.flight_type.name flightTarget = flight.package.target @@ -365,14 +359,15 @@ def generate(self): flightTargetType = None if hasattr(flightTarget, 'obj_name'): flightTargetName = flightTarget.obj_name - flightTargetType = flightType + f" TGT ({flightTarget.category})" + flightTargetType = flightType + \ + f" TGT ({flightTarget.category})" elif hasattr(flightTarget, 'name'): flightTargetName = flightTarget.name flightTargetType = flightType + " TGT (Airbase)" - luaData["TargetPoints"][flightTargetName] = { + luaData["TargetPoints"][flightTargetName] = { "name": flightTargetName, "type": flightTargetType, - "position": { "x": flightTarget.position.x, "y": flightTarget.position.y} + "position": {"x": flightTarget.position.x, "y": flightTarget.position.y} } # set a LUA table with data from Liberation that we want to set @@ -398,7 +393,7 @@ def generate(self): """ for key in luaData["Tankers"]: data = luaData["Tankers"][key] - dcsGroupName= data["dcsGroupName"] + dcsGroupName = data["dcsGroupName"] callsign = data["callsign"] variant = data["variant"] tacan = data["tacan"] @@ -415,7 +410,7 @@ def generate(self): """ for key in luaData["AWACs"]: data = luaData["AWACs"][key] - dcsGroupName= data["dcsGroupName"] + dcsGroupName = data["dcsGroupName"] callsign = data["callsign"] radio = data["radio"] lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n" @@ -430,7 +425,7 @@ def generate(self): """ for key in luaData["JTACs"]: data = luaData["JTACs"][key] - dcsGroupName= data["dcsGroupName"] + dcsGroupName = data["dcsGroupName"] callsign = data["callsign"] zone = data["zone"] laserCode = data["laserCode"] @@ -467,7 +462,6 @@ def generate(self): """ - trigger = TriggerStart(comment="Set DCS Liberation data") trigger.add_action(DoScript(String(lua))) self.current_mission.triggerrules.triggers.append(trigger) @@ -478,30 +472,57 @@ def generate(self): plugin.inject_scripts(self) plugin.inject_configuration(self) - self.assign_channels_to_flights(airgen.flights, - airsupportgen.air_support) - self.notify_info_generators(groundobjectgen, airsupportgen, jtacs, airgen) - - def assign_channels_to_flights(self, flights: List[FlightData], - air_support: AirSupport) -> None: - """Assigns preset radio channels for client flights.""" - for flight in flights: - if not flight.client_units: - continue - self.assign_channels_to_flight(flight, air_support) - - def assign_channels_to_flight(self, flight: FlightData, - air_support: AirSupport) -> None: - """Assigns preset radio channels for a client flight.""" - airframe = flight.aircraft_type + @classmethod + def _generate_air_units(cls) -> None: + """Generate the air units for the Operation""" + # TODO: this is silly, once AirSupportConflictGenerator doesn't require Conflict this can be removed. + default_conflict = [i for i in cls.conflicts()][0] - try: - aircraft_data = AIRCRAFT_DATA[airframe.id] - except KeyError: - logging.warning(f"No aircraft data for {airframe.id}") - return + # Air Support (Tanker & Awacs) + cls.airsupportgen = AirSupportConflictGenerator( + cls.current_mission, default_conflict, cls.game, cls.radio_registry, + cls.tacan_registry) + cls.airsupportgen.generate(cls.is_awacs_enabled) + + # Generate Aircraft Activity on the map + cls.airgen = AircraftConflictGenerator( + cls.current_mission, cls.game.settings, cls.game, + cls.radio_registry) + + cls.airgen.generate_flights( + cls.current_mission.country(cls.game.player_country), + cls.game.blue_ato, + cls.groundobjectgen.runways + ) + cls.airgen.generate_flights( + cls.current_mission.country(cls.game.enemy_country), + cls.game.red_ato, + cls.groundobjectgen.runways + ) - if aircraft_data.channel_allocator is not None: - aircraft_data.channel_allocator.assign_channels_for_flight( - flight, air_support + def _generate_ground_conflicts(self) -> None: + """For each frontline in the Operation, generate the ground conflicts and JTACs""" + self.jtacs: List[JtacInfo] = [] + for front_line in self.game.theater.conflicts(True): + player_cp = front_line.control_point_a + enemy_cp = front_line.control_point_b + conflict = Conflict.frontline_cas_conflict( + self.game.player_name, + self.game.enemy_name, + self.current_mission.country(self.game.player_country), + self.current_mission.country(self.game.enemy_country), + player_cp, + enemy_cp, + self.game.theater + ) + # Generate frontline ops + player_gp = self.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id] + enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] + ground_conflict_gen = GroundConflictGenerator( + self.current_mission, + conflict, self.game, + player_gp, enemy_gp, + player_cp.stances[enemy_cp.id] ) + ground_conflict_gen.generate() + self.jtacs.extend(ground_conflict_gen.jtacs) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 476f831a3..a1934b3ff 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -160,6 +160,9 @@ def __init__(self, id: int, name: str, position: Point, self.stances: Dict[int, CombatStance] = {} self.airport = None self.pending_unit_deliveries: Optional[UnitsDeliveryEvent] = None + + def __repr__(self): + return f"<{__class__}: {self.name}>" @property def ground_objects(self) -> List[TheaterGroundObject]: diff --git a/gen/aircraft.py b/gen/aircraft.py index 758bf0b83..ca9709584 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -647,12 +647,11 @@ class AircraftData: class AircraftConflictGenerator: - def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, + def __init__(self, mission: Mission, settings: Settings, game: Game, radio_registry: RadioRegistry): self.m = mission self.game = game self.settings = settings - self.conflict = conflict self.radio_registry = radio_registry self.flights: List[FlightData] = [] diff --git a/gen/armor.py b/gen/armor.py index 0e5040211..085cfdfcd 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -75,10 +75,33 @@ def __init__(self, mission: Mission, conflict: Conflict, game: Game, player_plan self.enemy_planned_combat_groups = enemy_planned_combat_groups self.player_planned_combat_groups = player_planned_combat_groups self.player_stance = CombatStance(player_stance) - self.enemy_stance = random.choice([CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESSIVE]) + self.enemy_stance = self._enemy_stance() self.game = game self.jtacs: List[JtacInfo] = [] + def _enemy_stance(self): + """Picks the enemy stance according to the number of planned groups on the frontline for each side""" + if len(self.enemy_planned_combat_groups) > len(self.player_planned_combat_groups): + return random.choice( + [ + CombatStance.AGGRESSIVE, + CombatStance.AGGRESSIVE, + CombatStance.AGGRESSIVE, + CombatStance.ELIMINATION, + CombatStance.BREAKTHROUGH + ] + ) + else: + return random.choice( + [ + CombatStance.DEFENSIVE, + CombatStance.DEFENSIVE, + CombatStance.DEFENSIVE, + CombatStance.AMBUSH, + CombatStance.AGGRESSIVE + ] + ) + def _group_point(self, point) -> Point: distance = random.randint( int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]), @@ -266,7 +289,7 @@ def plan_action_for_groups(self, stance, ally_groups, enemy_groups, forward_head hold_2.number = 3 dcs_group.add_trigger_action(hold_2) - retreat_task = GoToWaypoint(toIndex=3) + retreat_task = GoToWaypoint(to_index=3) retreat_task.number = 4 dcs_group.add_trigger_action(retreat_task) @@ -362,7 +385,7 @@ def add_morale_trigger(self, dcs_group, forward_heading): dcs_group.add_waypoint(self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)), PointAction.OffRoad) # Fallback task - fallback = ControlledTask(GoToWaypoint(toIndex=len(dcs_group.points))) + fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points))) fallback.enabled = False dcs_group.add_trigger_action(Hold()) dcs_group.add_trigger_action(fallback) diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 17e11beb7..136a0dffc 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -157,6 +157,24 @@ def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: C return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position)) + @classmethod + def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): + assert cls.has_frontline_between(from_cp, to_cp) + position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater) + + return cls( + position=position, + heading=heading, + distance=distance, + theater=theater, + from_cp=from_cp, + to_cp=to_cp, + attackers_side=attacker_name, + defenders_side=defender_name, + attackers_country=attacker, + defenders_country=defender, + ) + @classmethod def _extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point: pos = initial diff --git a/gen/forcedoptionsgen.py b/gen/forcedoptionsgen.py index 8a6684b2a..dff54bc42 100644 --- a/gen/forcedoptionsgen.py +++ b/gen/forcedoptionsgen.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import logging -import typing +from typing import TYPE_CHECKING from enum import IntEnum from dcs.mission import Mission @@ -7,6 +9,8 @@ from .conflictgen import * +if TYPE_CHECKING: + from game.game import Game class Labels(IntEnum): Off = 0 @@ -16,9 +20,8 @@ class Labels(IntEnum): class ForcedOptionsGenerator: - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, game: Game): self.mission = mission - self.conflict = conflict self.game = game def _set_options_view(self): diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 09385c789..e2c30846b 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -353,40 +353,16 @@ class GroundObjectsGenerator: locations for spawning ground objects, determining their types, and creating the appropriate generators. """ - FARP_CAPACITY = 4 - def __init__(self, mission: Mission, conflict: Conflict, game: Game, + def __init__(self, mission: Mission, game: Game, radio_registry: RadioRegistry, tacan_registry: TacanRegistry): self.m = mission - self.conflict = conflict self.game = game self.radio_registry = radio_registry self.tacan_registry = tacan_registry self.icls_alloc = iter(range(1, 21)) self.runways: Dict[str, RunwayData] = {} - def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]: - if self.conflict.is_vector: - center = self.conflict.center - heading = self.conflict.heading - 90 - else: - center, heading = self.conflict.frontline_position(self.conflict.from_cp, self.conflict.to_cp, self.game.theater) - heading -= 90 - - initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE) - position = self.conflict.find_ground_position(initial_position, heading) - if not position: - position = initial_position - - for i, _ in enumerate(range(0, number_of_units, self.FARP_CAPACITY)): - position = position.point_from_heading(0, i * 275) - - yield self.m.farp( - country=self.m.country(self.game.player_country), - name="FARP", - position=position, - ) - def generate(self): for cp in self.game.theater.controlpoints: if cp.captured: diff --git a/gen/triggergen.py b/gen/triggergen.py index ba87bb3e1..5344a3b3b 100644 --- a/gen/triggergen.py +++ b/gen/triggergen.py @@ -32,7 +32,7 @@ class Silence(Option): class TriggersGenerator: def __init__(self, mission: Mission, conflict: Conflict, game): self.mission = mission - self.conflict = conflict + self.conflict = conflict # TODO: Move conflict out of this class. Only needed for bullseye position self.game = game def _set_allegiances(self, player_coalition: str, enemy_coalition: str): diff --git a/gen/visualgen.py b/gen/visualgen.py index c2636ea61..97dbaa40b 100644 --- a/gen/visualgen.py +++ b/gen/visualgen.py @@ -92,9 +92,8 @@ def turn_heading(heading, fac): class VisualGenerator: - def __init__(self, mission: Mission, conflict: Conflict, game: Game): + def __init__(self, mission: Mission, game: Game): self.mission = mission - self.conflict = conflict self.game = game def _generate_frontline_smokes(self): diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 005962c33..bb010d32c 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -233,8 +233,7 @@ def launch_mission(self): game_event.is_awacs_enabled = True game_event.ca_slots = 1 game_event.departure_cp = self.game.theater.controlpoints[0] - # game_event.player_attacking({CAS: {}, CAP: {}}) - game_event.depart_from = self.game.theater.controlpoints[0] + game_event.player_attacking() self.game.initiate_event(game_event) waiting = QWaitingForMissionResultWindow(game_event, self.game)