diff --git a/game/event/event.py b/game/event/event.py index db8e5ee6..acc929fe 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -92,7 +92,7 @@ class Event: 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 0046526d..fa3d3416 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 @@ class FrontlineAttackEvent(Event): 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/frontlineattack.py b/game/operation/frontlineattack.py deleted file mode 100644 index 4dc18dae..00000000 --- a/game/operation/frontlineattack.py +++ /dev/null @@ -1,38 +0,0 @@ -from dcs.terrain.terrain import Terrain - -from gen.conflictgen import Conflict -from .operation import Operation -from .. import db - -MAX_DISTANCE_BETWEEN_GROUPS = 12000 - - -class FrontlineAttackOperation(Operation): - interceptors = None # type: db.AssignedUnitsDict - escort = None # type: db.AssignedUnitsDict - strikegroup = None # type: db.AssignedUnitsDict - - attackers = None # type: db.ArmorDict - defenders = None # type: db.ArmorDict - - def prepare(self, terrain: Terrain, is_quick: bool): - super(FrontlineAttackOperation, self).prepare(terrain, is_quick) - if self.defender_name == self.game.player_name: - self.attackers_starting_position = None - self.defenders_starting_position = None - - conflict = Conflict.frontline_cas_conflict( - attacker_name=self.attacker_name, - defender_name=self.defender_name, - attacker=self.current_mission.country(self.attacker_country), - defender=self.current_mission.country(self.defender_country), - from_cp=self.from_cp, - to_cp=self.to_cp, - theater=self.game.theater - ) - - self.initialize(mission=self.current_mission, - conflict=conflict) - - def generate(self): - super(FrontlineAttackOperation, self).generate() diff --git a/game/operation/operation.py b/game/operation/operation.py index 57a70630..ec1ee774 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -1,7 +1,10 @@ +from __future__ import annotations +from game.theater.theatergroundobject import TheaterGroundObject + 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 +12,9 @@ from dcs.coalition import Coalition 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 +31,19 @@ from gen.kneeboard import KneeboardGenerator 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 +53,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 +61,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 [] @@ -83,55 +97,21 @@ class Operation: def is_successfull(self, debriefing: Debriefing) -> bool: return True - @property - def is_player_attack(self) -> bool: - return self.from_cp.captured + @classmethod + def _set_mission(cls, mission: Mission) -> None: + cls.current_mission = mission - 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? + @classmethod + def _setup_mission_coalitions(cls): + cls.current_mission.coalition["blue"] = Coalition("blue") + cls.current_mission.coalition["red"] = Coalition("red") - 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") - - 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)]()) - - 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 +141,8 @@ class Operation: 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 +152,13 @@ class Operation: 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) @@ -196,141 +177,11 @@ class Operation: gen.add_flight(flight) gen.generate() - def generate(self): - radio_registry = RadioRegistry() - tacan_registry = TacanRegistry() - - # 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: - if beacon.channel is None: - logging.error( - f"TACAN beacon has no channel: {beacon.callsign}") - else: - tacan_registry.reserve(beacon.tacan_channel) - - for airfield, data in AIRFIELD_DATA.items(): - if data.theater == self.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. - - 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 - ) - groundobjectgen.generate() - - # Generate destroyed units - for d in self.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), - name="", - _type=utype, - hidden=True, - position=pos, - 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) - - # 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 - 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.generate() - - # Generate Visuals Smoke Effects - visualgen = VisualGenerator(self.current_mission, self.conflict, - self.game) - if self.game.settings.perf_smoke_gen: - visualgen.generate() - - self.generate_lua(airgen, airsupportgen, jtacs) - - # Inject Plugins Lua Scripts and data - for plugin in LuaPluginManager.plugins(): - if plugin.enabled: - 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) + @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: @@ -356,18 +207,188 @@ class Operation: 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) + + for beacon in beacons: + unique_map_frequencies.add(beacon.frequency) + if beacon.is_tacan: + if beacon.channel is None: + logging.error( + f"TACAN beacon has no channel: {beacon.callsign}") + else: + cls.tacan_registry.reserve(beacon.tacan_channel) + + @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: + 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) + # No need to reserve ILS or TACAN because those are in the + # beacon list. + + @classmethod + def _generate_ground_units(cls): + cls.groundobjectgen = GroundObjectsGenerator( + cls.current_mission, + cls.game, + cls.radio_registry, + cls.tacan_registry + ) + cls.groundobjectgen.generate() + + 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"]) + 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), + name="", + _type=utype, + hidden=True, + position=pos, + heading=d["orientation"], + dead=True, + ) + + def generate(self): + """Build the final Mission to be exported""" + 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 + 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 + else: + self.current_mission.groundControl.red_tactical_commander = self.ca_slots + + # Options + forcedoptionsgen = ForcedOptionsGenerator( + self.current_mission, self.game) + forcedoptionsgen.generate() + + # Generate Visuals Smoke Effects + visualgen = VisualGenerator(self.current_mission, self.game) + if self.game.settings.perf_smoke_gen: + visualgen.generate() + + self.generate_lua(self.airgen, self.airsupportgen, self.jtacs) + + # Inject Plugins Lua Scripts and data + for plugin in LuaPluginManager.plugins(): + if plugin.enabled: + plugin.inject_scripts(self) + plugin.inject_configuration(self) + + self.assign_channels_to_flights(self.airgen.flights, + self.airsupportgen.air_support) + self.notify_info_generators( + self.groundobjectgen, + self.airsupportgen, + self.jtacs, + self.airgen + ) + + @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] + + # Air Support (Tanker & Awacs) + assert cls.radio_registry and cls.tacan_registry + 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 + ) + + def _generate_ground_conflicts(self) -> None: + """For each frontline in the Operation, generate the ground conflicts and JTACs""" + 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) + def generate_lua(self, airgen: AircraftConflictGenerator, airsupportgen: AirSupportConflictGenerator, jtacs: List[JtacInfo]) -> None: - luaData = {} - luaData["AircraftCarriers"] = {} - luaData["Tankers"] = {} - luaData["AWACs"] = {} - luaData["JTACs"] = {} - luaData["TargetPoints"] = {} - - self.assign_channels_to_flights(airgen.flights, - airsupportgen.air_support) + # TODO: Refactor this + luaData = { + "AircraftCarriers": {}, + "Tankers": {}, + "AWACs": {}, + "JTACs": {}, + "TargetPoints": {}, + } # type: ignore for tanker in airsupportgen.air_support.tankers: luaData["Tankers"][tanker.callsign] = { @@ -405,9 +426,10 @@ class Operation: if flightTarget: flightTargetName = None flightTargetType = None - if hasattr(flightTarget, 'obj_name'): + if isinstance(flightTarget, TheaterGroundObject): 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)" @@ -512,4 +534,4 @@ class Operation: trigger = TriggerStart(comment="Set DCS Liberation data") trigger.add_action(DoScript(String(lua))) - self.current_mission.triggerrules.triggers.append(trigger) \ No newline at end of file + Operation.current_mission.triggerrules.triggers.append(trigger) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 476f831a..a1934b3f 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -160,6 +160,9 @@ class ControlPoint(MissionTarget): 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 758bf0b8..ca970958 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -647,12 +647,11 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"] 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 0e504021..9c949ff7 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -75,10 +75,33 @@ class GroundConflictGenerator: 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]), diff --git a/gen/conflictgen.py b/gen/conflictgen.py index 35be5956..136a0dff 100644 --- a/gen/conflictgen.py +++ b/gen/conflictgen.py @@ -67,10 +67,7 @@ class Conflict: position: Point, heading=None, distance=None, - ground_attackers_location: Point = None, - ground_defenders_location: Point = None, - air_attackers_location: Point = None, - air_defenders_location: Point = None): + ): self.attackers_side = attackers_side self.defenders_side = defenders_side @@ -84,11 +81,6 @@ class Conflict: self.heading = heading self.distance = distance self.size = to_cp.size - self.radials = to_cp.radials - self.ground_attackers_location = ground_attackers_location - self.ground_defenders_location = ground_defenders_location - self.air_attackers_location = air_attackers_location - self.air_defenders_location = air_defenders_location @property def center(self) -> Point: @@ -110,24 +102,6 @@ class Conflict: def to_size(self): return self.to_cp.size * GROUND_DISTANCE_FACTOR - def find_insertion_point(self, other_point: Point) -> Point: - if self.is_vector: - dx = self.position.x - self.tail.x - dy = self.position.y - self.tail.y - dr2 = float(dx ** 2 + dy ** 2) - - lerp = ((other_point.x - self.tail.x) * dx + (other_point.y - self.tail.y) * dy) / dr2 - if lerp < 0: - lerp = 0 - elif lerp > 1: - lerp = 1 - - x = lerp * dx + self.tail.x - y = lerp * dy + self.tail.y - return Point(x, y) - else: - return self.position - def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point: return Conflict._find_ground_position(at, max_distance, heading, self.theater) @@ -183,6 +157,24 @@ class Conflict: 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 @@ -228,273 +220,3 @@ class Conflict: logging.error("Didn't find ground position ({})!".format(initial)) return initial - - @classmethod - def capture_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - position = to_cp.position - attack_raw_heading = to_cp.position.heading_between_point(from_cp.position) - attack_heading = to_cp.find_radial(attack_raw_heading) - defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading) - - distance = GROUND_DISTANCE - attackers_location = position.point_from_heading(attack_heading, distance) - attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, attack_heading, theater) - - defenders_location = position.point_from_heading(defense_heading, 0) - defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, defense_heading, theater) - - return cls( - position=position, - theater=theater, - from_cp=from_cp, - to_cp=to_cp, - attackers_side=attacker_name, - defenders_side=defender_name, - attackers_country=attacker, - defenders_country=defender, - ground_attackers_location=attackers_location, - ground_defenders_location=defenders_location, - air_attackers_location=position.point_from_heading(attack_raw_heading, CAPTURE_AIR_ATTACKERS_DISTANCE), - air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), CAPTURE_AIR_DEFENDERS_DISTANCE) - ) - - @classmethod - def strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - position = to_cp.position - attack_raw_heading = to_cp.position.heading_between_point(from_cp.position) - attack_heading = to_cp.find_radial(attack_raw_heading) - defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading) - - distance = to_cp.size * GROUND_DISTANCE_FACTOR - attackers_location = position.point_from_heading(attack_heading, distance) - attackers_location = Conflict._find_ground_position( - attackers_location, int(distance * 2), - _heading_sum(attack_heading, 180), theater) - - defenders_location = position.point_from_heading(defense_heading, distance) - defenders_location = Conflict._find_ground_position( - defenders_location, int(distance * 2), - _heading_sum(defense_heading, 180), theater) - - return cls( - position=position, - theater=theater, - from_cp=from_cp, - to_cp=to_cp, - attackers_side=attacker_name, - defenders_side=defender_name, - attackers_country=attacker, - defenders_country=defender, - ground_attackers_location=attackers_location, - ground_defenders_location=defenders_location, - air_attackers_location=position.point_from_heading(attack_raw_heading, STRIKE_AIR_ATTACKERS_DISTANCE), - air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), STRIKE_AIR_DEFENDERS_DISTANCE) - ) - - @classmethod - def intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> Point: - raw_distance = from_cp.position.distance_to_point(to_cp.position) * 1.5 - distance = max(min(raw_distance, INTERCEPT_MAX_DISTANCE), INTERCEPT_MIN_DISTANCE) - heading = _heading_sum(from_cp.position.heading_between_point(to_cp.position), random.choice([-1, 1]) * random.randint(60, 100)) - return from_cp.position.point_from_heading(heading, distance) - - @classmethod - def intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - heading = from_cp.position.heading_between_point(position) - return cls( - position=position.point_from_heading(position.heading_between_point(to_cp.position), INTERCEPT_CONFLICT_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, - ground_attackers_location=None, - ground_defenders_location=None, - air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, INTERCEPT_ATTACKERS_DISTANCE), - air_defenders_location=position - ) - - @classmethod - def ground_attack_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - heading = random.choice(to_cp.radials) - initial_location = to_cp.position.random_point_within(*GROUND_ATTACK_DISTANCE) - position = Conflict._find_ground_position(initial_location, GROUND_INTERCEPT_SPREAD, _heading_sum(heading, 180), theater) - if not position: - heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position)) - position = to_cp.position.point_from_heading(heading, to_cp.size * GROUND_DISTANCE_FACTOR) - - return cls( - position=position, - theater=theater, - from_cp=from_cp, - to_cp=to_cp, - attackers_side=attacker_name, - defenders_side=defender_name, - attackers_country=attacker, - defenders_country=defender, - ground_attackers_location=position, - ground_defenders_location=None, - air_attackers_location=None, - air_defenders_location=position.point_from_heading(heading, AIR_DISTANCE), - ) - - @classmethod - def convoy_strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - frontline_position, frontline_heading, frontline_length = Conflict.frontline_vector(from_cp, to_cp, theater) - if not frontline_position: - assert False - - heading = frontline_heading - starting_position = Conflict._find_ground_position(frontline_position.point_from_heading(heading, 7000), - GROUND_INTERCEPT_SPREAD, - _opposite_heading(heading), theater) - if not starting_position: - starting_position = frontline_position - destination_position = frontline_position - else: - destination_position = frontline_position - - return cls( - position=destination_position, - theater=theater, - from_cp=from_cp, - to_cp=to_cp, - attackers_side=attacker_name, - defenders_side=defender_name, - attackers_country=attacker, - defenders_country=defender, - ground_attackers_location=None, - ground_defenders_location=starting_position, - air_attackers_location=starting_position.point_from_heading(_opposite_heading(heading), AIR_DISTANCE), - air_defenders_location=starting_position.point_from_heading(heading, AIR_DISTANCE), - ) - - @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, - ground_attackers_location=None, - ground_defenders_location=None, - air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, AIR_DISTANCE), - air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE), - ) - - @classmethod - def frontline_cap_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) - attack_position = position.point_from_heading(heading, random.randint(0, int(distance))) - attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE) - defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE)) - - 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, - air_attackers_location=attackers_position, - air_defenders_location=defenders_position, - ) - - @classmethod - def ground_base_attack(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - position = to_cp.position - attack_heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position)) - defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading) - - distance = to_cp.size * GROUND_DISTANCE_FACTOR - defenders_location = position.point_from_heading(defense_heading, distance) - defenders_location = Conflict._find_ground_position( - defenders_location, int(distance * 2), - _heading_sum(defense_heading, 180), theater) - - return cls( - position=position, - theater=theater, - from_cp=from_cp, - to_cp=to_cp, - attackers_side=attacker_name, - defenders_side=defender_name, - attackers_country=attacker, - defenders_country=defender, - ground_attackers_location=None, - ground_defenders_location=defenders_location, - air_attackers_location=position.point_from_heading(attack_heading, AIR_DISTANCE), - air_defenders_location=position - ) - - @classmethod - def naval_intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - radial = random.choice(to_cp.sea_radials) - - initial_distance = min(int(from_cp.position.distance_to_point(to_cp.position) * NAVAL_INTERCEPT_DISTANCE_FACTOR), NAVAL_INTERCEPT_DISTANCE_MAX) - initial_position = to_cp.position.point_from_heading(radial, initial_distance) - for offset in range(0, initial_distance, NAVAL_INTERCEPT_STEP): - position = initial_position.point_from_heading(_opposite_heading(radial), offset) - - if not theater.is_on_land(position): - break - return position - - @classmethod - def naval_intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - attacker_heading = from_cp.position.heading_between_point(to_cp.position) - return cls( - position=position, - theater=theater, - from_cp=from_cp, - to_cp=to_cp, - attackers_side=attacker_name, - defenders_side=defender_name, - attackers_country=attacker, - defenders_country=defender, - ground_attackers_location=None, - ground_defenders_location=position, - air_attackers_location=position.point_from_heading(attacker_heading, AIR_DISTANCE), - air_defenders_location=position.point_from_heading(_opposite_heading(attacker_heading), AIR_DISTANCE) - ) - - @classmethod - def transport_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater): - frontline_position, heading = cls.frontline_position(from_cp, to_cp, theater) - initial_dest = frontline_position.point_from_heading(heading, TRANSPORT_FRONTLINE_DIST) - dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater) - if not dest: - radial = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position)) - dest = to_cp.position.point_from_heading(radial, to_cp.size * GROUND_DISTANCE_FACTOR) - - return cls( - position=dest, - theater=theater, - from_cp=from_cp, - to_cp=to_cp, - attackers_side=attacker_name, - defenders_side=defender_name, - attackers_country=attacker, - defenders_country=defender, - ground_attackers_location=from_cp.position, - ground_defenders_location=frontline_position, - air_attackers_location=from_cp.position.point_from_heading(0, 100), - air_defenders_location=frontline_position - ) \ No newline at end of file diff --git a/gen/forcedoptionsgen.py b/gen/forcedoptionsgen.py index 8a6684b2..dff54bc4 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 dcs.forcedoptions import ForcedOptions 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 09385c78..e2c30846 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 ba87bb3e..5344a3b3 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 c2636ea6..97dbaa40 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 32ba24a8..bb010d32 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -233,8 +233,7 @@ class QTopPanel(QFrame): 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)