From da17d1e5d17f62bd3e2d01fc81655d85ef0f8616 Mon Sep 17 00:00:00 2001 From: walterroach Date: Mon, 23 Nov 2020 16:46:05 -0600 Subject: [PATCH] Change Operation to a static class Removed always True "event successful" Add `AirWarEvent` as the primary game `Event` applied to every miz Cleanup of `FrontLineAttackEvent` Change `Operation.is_awacs_enabled` to two bools for each side red/blue Currently controlled by whether an AWACs is available for the faction (and only ever true for Blue) --- game/event/airwar.py | 14 +++ game/event/event.py | 35 +----- game/event/frontlineattack.py | 41 +------ game/game.py | 5 +- game/operation/operation.py | 183 ++++++++++++++++---------------- game/theater/conflicttheater.py | 34 ++++++ gen/airsupportgen.py | 46 ++++---- qt_ui/widgets/QTopPanel.py | 30 ++---- 8 files changed, 182 insertions(+), 206 deletions(-) create mode 100644 game/event/airwar.py diff --git a/game/event/airwar.py b/game/event/airwar.py new file mode 100644 index 00000000..0f804375 --- /dev/null +++ b/game/event/airwar.py @@ -0,0 +1,14 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +from .event import Event + +if TYPE_CHECKING: + from game.theater import ConflictTheater + + +class AirWarEvent(Event): + """An Event centered on the overall Air War""" + + def __str__(self): + return "Frontline attack" diff --git a/game/event/event.py b/game/event/event.py index 9cca66aa..3942e153 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -15,10 +15,11 @@ from game.theater import ControlPoint from gen import AirTaskingOrder from gen.ground_forces.combat_stance import CombatStance from ..unitmap import UnitMap +from game.operation.operation import Operation if TYPE_CHECKING: from ..game import Game - from game.operation.operation import Operation + DIFFICULTY_LOG_BASE = 1.1 EVENT_DEPARTURE_MAX_DISTANCE = 340000 @@ -32,21 +33,18 @@ STRONG_DEFEAT_INFLUENCE = 0.5 class Event: silent = False informational = False - is_awacs_enabled = False - ca_slots = 0 game = None # type: Game location = None # type: Point from_cp = None # type: ControlPoint to_cp = None # type: ControlPoint - operation = None # type: Operation + operation = Operation difficulty = 1 # type: int BONUS_BASE = 5 def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): self.game = game - self.departure_cp: Optional[ControlPoint] = None self.from_cp = from_cp self.to_cp = target_cp self.location = location @@ -57,41 +55,14 @@ class Event: def is_player_attacking(self) -> bool: return self.attacker_name == self.game.player_name - @property - def enemy_cp(self) -> Optional[ControlPoint]: - if self.attacker_name == self.game.player_name: - return self.to_cp - else: - return self.departure_cp - @property def tasks(self) -> List[Type[Task]]: return [] - @property - def global_cp_available(self) -> bool: - return False - - def is_departure_available_from(self, cp: ControlPoint) -> bool: - if not cp.captured: - return False - - if self.location.distance_to_point(cp.position) > EVENT_DEPARTURE_MAX_DISTANCE: - return False - - if cp.is_global and not self.global_cp_available: - return False - - return True - def bonus(self) -> int: return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE) - def is_successful(self, debriefing: Debriefing) -> bool: - return self.operation.is_successful(debriefing) - def generate(self) -> UnitMap: - self.operation.is_awacs_enabled = self.is_awacs_enabled self.operation.ca_slots = self.ca_slots self.operation.prepare(self.game) diff --git a/game/event/frontlineattack.py b/game/event/frontlineattack.py index 4fa6d201..6dab825d 100644 --- a/game/event/frontlineattack.py +++ b/game/event/frontlineattack.py @@ -1,42 +1,11 @@ -from typing import List, Type - -from dcs.task import CAP, CAS, Task -from game.operation.operation import Operation - -from ..debriefing import Debriefing from .event import Event class FrontlineAttackEvent(Event): - - @property - def tasks(self) -> List[Type[Task]]: - if self.is_player_attacking: - return [CAS, CAP] - else: - return [CAP] - - @property - def global_cp_available(self) -> bool: - return True - + """ + An event centered on a FrontLine Conflict. + Currently the same as its parent, but here for legacy compatibility as well as to allow for + future unique Event handling + """ def __str__(self): return "Frontline attack" - - def is_successful(self, debriefing: Debriefing): - attackers_success = True - if self.from_cp.captured: - return attackers_success - else: - return not attackers_success - - def commit(self, debriefing: Debriefing): - super(FrontlineAttackEvent, self).commit(debriefing) - - def skip(self): - if self.to_cp.captured: - self.to_cp.base.affect_strength(-0.1) - - def player_attacking(self): - assert self.departure_cp is not None - self.operation = Operation(departure_cp=self.departure_cp,) diff --git a/game/game.py b/game/game.py index 28e19756..242d8b0b 100644 --- a/game/game.py +++ b/game/game.py @@ -182,8 +182,7 @@ class Game: def finish_event(self, event: Event, debriefing: Debriefing): logging.info("Finishing event {}".format(event)) event.commit(debriefing) - if event.is_successful(debriefing): - self.budget += event.bonus() + self.budget += event.bonus() if event in self.events: self.events.remove(event) @@ -194,7 +193,7 @@ class Game: if isinstance(event, Event): return event and event.attacker_name and event.attacker_name == self.player_name else: - return event and event.name and event.name == self.player_name + raise RuntimeError(f"{event} was passed when an expected") def on_load(self) -> None: LuaPluginManager.load_settings(self.settings) diff --git a/game/operation/operation.py b/game/operation/operation.py index 671d060b..0e3abbb2 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -41,9 +41,7 @@ if TYPE_CHECKING: class Operation: - attackers_starting_position = None # type: db.StartingPosition - defenders_starting_position = None # type: db.StartingPosition - + """Static class for managing the final Mission generation""" current_mission = None # type: Mission airgen = None # type: AircraftConflictGenerator triggersgen = None # type: TriggersGenerator @@ -58,16 +56,14 @@ class Operation: environment_settings = None trigger_radius = TRIGGER_RADIUS_MEDIUM is_quick = None + player_awacs_enabled = True + # TODO: #436 Generate Air Support for red + enemy_awacs_enabled = True is_awacs_enabled = False ca_slots = 0 unit_map: UnitMap - - def __init__(self, - departure_cp: ControlPoint, - ): - self.departure_cp = departure_cp - self.plugin_scripts: List[str] = [] - self.jtacs: List[JtacInfo] = [] + jtacs: List[JtacInfo] = [] + plugin_scripts: List[str] = [] @classmethod def prepare(cls, game: Game): @@ -93,12 +89,6 @@ class Operation: frontline.position ) - def units_of(self, country_name: str) -> List[UnitType]: - return [] - - def is_successful(self, debriefing: Debriefing) -> bool: - return True - @classmethod def _set_mission(cls, mission: Mission) -> None: cls.current_mission = mission @@ -115,22 +105,25 @@ class Operation: 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: + @classmethod + def inject_lua_trigger(cls, contents: str, comment: str) -> None: trigger = TriggerStart(comment=comment) trigger.add_action(DoScript(String(contents))) - self.current_mission.triggerrules.triggers.append(trigger) + cls.current_mission.triggerrules.triggers.append(trigger) - def bypass_plugin_script(self, mnemonic: str) -> None: - self.plugin_scripts.append(mnemonic) + @classmethod + def bypass_plugin_script(cls, mnemonic: str) -> None: + cls.plugin_scripts.append(mnemonic) - def inject_plugin_script(self, plugin_mnemonic: str, script: str, + @classmethod + def inject_plugin_script(cls, plugin_mnemonic: str, script: str, script_mnemonic: str) -> None: - if script_mnemonic in self.plugin_scripts: + if script_mnemonic in cls.plugin_scripts: logging.debug( f"Skipping already loaded {script} for {plugin_mnemonic}" ) else: - self.plugin_scripts.append(script_mnemonic) + cls.plugin_scripts.append(script_mnemonic) plugin_path = Path("./resources/plugins", plugin_mnemonic) @@ -143,13 +136,14 @@ class Operation: trigger = TriggerStart(comment=f"Load {script_mnemonic}") filename = script_path.resolve() - fileref = self.current_mission.map_resource.add_resource_file( + fileref = cls.current_mission.map_resource.add_resource_file( filename) trigger.add_action(DoScriptFile(fileref)) - self.current_mission.triggerrules.triggers.append(trigger) + cls.current_mission.triggerrules.triggers.append(trigger) + @classmethod def notify_info_generators( - self, + cls, groundobjectgen: GroundObjectsGenerator, airsupportgen: AirSupportConflictGenerator, jtacs: List[JtacInfo], @@ -158,8 +152,8 @@ class Operation: """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings) """ gens: List[MissionInfoGenerator] = [ - KneeboardGenerator(self.current_mission, self.game), - BriefingGenerator(self.current_mission, self.game) + KneeboardGenerator(cls.current_mission, cls.game), + BriefingGenerator(cls.current_mission, cls.game) ] for gen in gens: for dynamic_runway in groundobjectgen.runways.values(): @@ -168,7 +162,7 @@ class Operation: for tanker in airsupportgen.air_support.tankers: gen.add_tanker(tanker) - if self.is_awacs_enabled: + if cls.player_awacs_enabled: for awacs in airsupportgen.air_support.awacs: gen.add_awacs(awacs) @@ -189,15 +183,17 @@ class Operation: cls._create_tacan_registry(unique_map_frequencies) cls._create_radio_registry(unique_map_frequencies) - def assign_channels_to_flights(self, flights: List[FlightData], + @classmethod + def assign_channels_to_flights(cls, 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) + cls.assign_channels_to_flight(flight, air_support) - def assign_channels_to_flight(self, flight: FlightData, + @staticmethod + def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None: """Assigns preset radio channels for a client flight.""" airframe = flight.aircraft_type @@ -235,12 +231,11 @@ class Operation: 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: - if data.atc: - 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) + if data.theater == cls.game.theater.terrain.name and data.atc: + 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. @@ -254,19 +249,20 @@ class Operation: ) cls.groundobjectgen.generate() - def _generate_destroyed_units(self) -> None: + @classmethod + def _generate_destroyed_units(cls) -> None: """Add destroyed units to the Mission""" - for d in self.game.get_destroyed_units(): + for d in cls.game.get_destroyed_units(): try: utype = db.unit_type_from_name(d["type"]) except KeyError: continue 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), + if utype is not None and not cls.game.position_culled(pos) and cls.game.settings.perf_destroyed_units: + cls.current_mission.static_group( + country=cls.current_mission.country( + cls.game.player_country), name="", _type=utype, hidden=True, @@ -274,63 +270,64 @@ class Operation: heading=d["orientation"], dead=True, ) - - def generate(self) -> UnitMap: + + @classmethod + def generate(cls) -> UnitMap: """Build the final Mission to be exported""" - self.create_unit_map() - self.create_radio_registries() + cls.create_unit_map() + cls.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() + EnvironmentGenerator(cls.current_mission, + cls.game.conditions).generate() + cls._generate_ground_units() + cls._generate_destroyed_units() + cls._generate_air_units() + cls.assign_channels_to_flights(cls.airgen.flights, + cls.airsupportgen.air_support) + cls._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] + default_conflict = [i for i in cls.conflicts()][0] # Triggers - triggersgen = TriggersGenerator(self.current_mission, default_conflict, - self.game) + triggersgen = TriggersGenerator(cls.current_mission, default_conflict, + cls.game) triggersgen.generate() # Setup combined arms parameters - self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0 - if self.game.player_country in [country.name for country in self.current_mission.coalition["blue"].countries.values()]: - self.current_mission.groundControl.blue_tactical_commander = self.ca_slots + cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0 + if cls.game.player_country in [country.name for country in cls.current_mission.coalition["blue"].countries.values()]: + cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots else: - self.current_mission.groundControl.red_tactical_commander = self.ca_slots + cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots # Options forcedoptionsgen = ForcedOptionsGenerator( - self.current_mission, self.game) + cls.current_mission, cls.game) forcedoptionsgen.generate() # Generate Visuals Smoke Effects - visualgen = VisualGenerator(self.current_mission, self.game) - if self.game.settings.perf_smoke_gen: + visualgen = VisualGenerator(cls.current_mission, cls.game) + if cls.game.settings.perf_smoke_gen: visualgen.generate() - self.generate_lua(self.airgen, self.airsupportgen, self.jtacs) + cls.generate_lua(cls.airgen, cls.airsupportgen, cls.jtacs) # Inject Plugins Lua Scripts and data for plugin in LuaPluginManager.plugins(): if plugin.enabled: - plugin.inject_scripts(self) - plugin.inject_configuration(self) + plugin.inject_scripts(cls) + plugin.inject_configuration(cls) - self.assign_channels_to_flights(self.airgen.flights, - self.airsupportgen.air_support) - self.notify_info_generators( - self.groundobjectgen, - self.airsupportgen, - self.jtacs, - self.airgen + cls.assign_channels_to_flights(cls.airgen.flights, + cls.airsupportgen.air_support) + cls.notify_info_generators( + cls.groundobjectgen, + cls.airsupportgen, + cls.jtacs, + cls.airgen ) - return self.unit_map + return cls.unit_map @classmethod def _generate_air_units(cls) -> None: @@ -343,7 +340,7 @@ class Operation: cls.airsupportgen = AirSupportConflictGenerator( cls.current_mission, default_conflict, cls.game, cls.radio_registry, cls.tacan_registry) - cls.airsupportgen.generate(cls.is_awacs_enabled) + cls.airsupportgen.generate() # Generate Aircraft Activity on the map cls.airgen = AircraftConflictGenerator( @@ -364,33 +361,35 @@ class Operation: cls.current_mission.country(cls.game.player_country), cls.current_mission.country(cls.game.enemy_country)) - def _generate_ground_conflicts(self) -> None: + @classmethod + def _generate_ground_conflicts(cls) -> None: """For each frontline in the Operation, generate the ground conflicts and JTACs""" - for front_line in self.game.theater.conflicts(True): + for front_line in cls.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), + cls.game.player_name, + cls.game.enemy_name, + cls.current_mission.country(cls.game.player_country), + cls.current_mission.country(cls.game.enemy_country), player_cp, enemy_cp, - self.game.theater + cls.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] + player_gp = cls.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id] + enemy_gp = cls.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id] ground_conflict_gen = GroundConflictGenerator( - self.current_mission, - conflict, self.game, + cls.current_mission, + conflict, cls.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id] ) ground_conflict_gen.generate() - self.jtacs.extend(ground_conflict_gen.jtacs) + cls.jtacs.extend(ground_conflict_gen.jtacs) - def generate_lua(self, airgen: AircraftConflictGenerator, + @classmethod + def generate_lua(cls, airgen: AircraftConflictGenerator, airsupportgen: AirSupportConflictGenerator, jtacs: List[JtacInfo]) -> None: # TODO: Refactor this @@ -411,7 +410,7 @@ class Operation: "tacan": str(tanker.tacan.number) + tanker.tacan.band.name } - if self.is_awacs_enabled: + if airsupportgen.air_support.awacs: for awacs in airsupportgen.air_support.awacs: luaData["AWACs"][awacs.callsign] = { "dcsGroupName": awacs.dcsGroupName, diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index aacaa5a2..97befd7b 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -470,6 +470,40 @@ class ConflictTheater: closest = control_point closest_distance = distance return closest + + def closest_opposing_control_points(self) -> Tuple[ControlPoint]: + """ + Returns a tuple of the two nearest opposing ControlPoints in theater. + (player_cp, enemy_cp) + """ + all_cp_min_distances = {} + for idx, control_point in enumerate(self.controlpoints): + distances = {} + closest_distance = None + for i, cp in enumerate(self.controlpoints): + if i != idx and cp.captured is not control_point.captured: + dist = cp.position.distance_to_point(control_point.position) + if not closest_distance: + closest_distance = dist + if dist < closest_distance: + distances[cp.id] = dist + closest_cp = min(distances, key=distances.get) + all_cp_min_distances[(control_point.id, closest_cp)] = distances[closest_cp] + closest_opposing_cps = [ + self.find_control_point_by_id(i) + for i + in min(all_cp_min_distances, key=all_cp_min_distances.get) + ] # type: List[ControlPoint] + if closest_opposing_cps[0].captured: + return tuple(closest_opposing_cps) + else: + return tuple(reversed(closest_opposing_cps)) + + def find_control_point_by_id(self, id: int) -> ControlPoint: + for i in self.controlpoints: + if i.id == id: + return i + raise RuntimeError(f"Cannot find ControlPoint with ID {id}") def add_json_cp(self, theater, p: dict) -> ControlPoint: cp: ControlPoint diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 97aeea1f..22344450 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -1,3 +1,4 @@ +import logging from dataclasses import dataclass, field from typing import List, Type @@ -13,6 +14,7 @@ from dcs.task import ( ) from game import db +from game.operation.operation import Operation from .naming import namegen from .callsigns import callsign_for_support_unit from .conflictgen import Conflict @@ -67,7 +69,7 @@ class AirSupportConflictGenerator: def support_tasks(cls) -> List[Type[MainTask]]: return [Refueling, AWACS] - def generate(self, is_awacs_enabled): + def generate(self): player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp fallback_tanker_number = 0 @@ -120,26 +122,26 @@ class AirSupportConflictGenerator: self.air_support.tankers.append(TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)) - if is_awacs_enabled: - try: - freq = self.radio_registry.alloc_uhf() - awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0] - awacs_flight = self.mission.awacs_flight( - country=self.mission.country(self.game.player_country), - name=namegen.next_awacs_name(self.mission.country(self.game.player_country)), - plane_type=awacs_unit, - altitude=AWACS_ALT, - airport=None, - position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE), - frequency=freq.mhz, - start_type=StartType.Warm, - ) - awacs_flight.set_frequency(freq.mhz) + try: + freq = self.radio_registry.alloc_uhf() + awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0] + awacs_flight = self.mission.awacs_flight( + country=self.mission.country(self.game.player_country), + name=namegen.next_awacs_name(self.mission.country(self.game.player_country)), + plane_type=awacs_unit, + altitude=AWACS_ALT, + airport=None, + position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE), + frequency=freq.mhz, + start_type=StartType.Warm, + ) + awacs_flight.set_frequency(freq.mhz) - awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) - awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) + awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) + awacs_flight.points[0].tasks.append(SetImmortalCommand(True)) - self.air_support.awacs.append(AwacsInfo( - str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq)) - except: - print("No AWACS for faction") \ No newline at end of file + self.air_support.awacs.append(AwacsInfo( + str(awacs_flight.name), callsign_for_support_unit(awacs_flight), freq)) + except: + Operation.player_awacs_enabled = False + logging.warning("No AWACS for faction") \ No newline at end of file diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index 38fdc3ab..d20abcf0 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -10,7 +10,7 @@ from PySide2.QtWidgets import ( import qt_ui.uiconstants as CONST from game import Game -from game.event import CAP, CAS, FrontlineAttackEvent +from game.event.airwar import AirWarEvent from gen.ato import Package from gen.flights.traveltime import TotEstimator from qt_ui.models import GameModel @@ -214,26 +214,14 @@ class QTopPanel(QFrame): if negative_starts: if not self.confirm_negative_start_time(negative_starts): return - - # TODO: Refactor this nonsense. - game_event = None - for event in self.game.events: - if isinstance(event, - FrontlineAttackEvent) and event.is_player_attacking: - game_event = event - # TODO: Why does this exist? - if game_event is None: - game_event = FrontlineAttackEvent( - self.game, - self.game.theater.controlpoints[0], - self.game.theater.controlpoints[1], - self.game.theater.controlpoints[1].position, - self.game.player_name, - self.game.enemy_name) - game_event.is_awacs_enabled = True - game_event.ca_slots = 1 - game_event.departure_cp = self.game.theater.controlpoints[0] - game_event.player_attacking() + closest_cps = self.game.theater.closest_opposing_control_points() + game_event = AirWarEvent( + self.game, + closest_cps[0], + closest_cps[1], + self.game.theater.controlpoints[0].position, + self.game.player_name, + self.game.enemy_name) unit_map = self.game.initiate_event(game_event) waiting = QWaitingForMissionResultWindow(game_event, self.game,