From 6c9a9de3f39283ece6d5a33a1746788446d0cc2a Mon Sep 17 00:00:00 2001 From: Walter Date: Thu, 29 Oct 2020 14:43:15 -0500 Subject: [PATCH] parent f03121af5ae214e7a58f37e4e1d4ad9102a2f35e first pass briefing refactor briefing fixes briefing fixes Stop briefing generate being called twice Stop frontline advantage string being appended when there are no units. jinja template always return enum instance in Strategy Selector For some reason on DEFENSE, enum is appended to control point stance, but on all other the enum.value is added instead. I don't see any case where the value is used, but there are many cases that the enum instance is evaluated against. type issue junja's not a thing swap mapping with dict jinja template always return enum instance in Strategy Selector For some reason on DEFENSE, enum is appended to control point stance, but on all other the enum.value is added instead. I don't see any case where the value is used, but there are many cases that the enum instance is evaluated against. type issue Update build.yml junja's not a thing swap mapping with dict Restore build job --- game/operation/frontlineattack.py | 5 +- game/operation/operation.py | 6 +- gen/aircraft.py | 4 + gen/briefinggen.py | 222 +++++++----------- .../QGroundForcesStrategySelector.py | 4 +- requirements.txt | 3 +- .../briefing/templates/briefingtemplate_EN.j2 | 48 ++++ 7 files changed, 148 insertions(+), 144 deletions(-) create mode 100644 resources/briefing/templates/briefingtemplate_EN.j2 diff --git a/game/operation/frontlineattack.py b/game/operation/frontlineattack.py index 19255247..11ae3158 100644 --- a/game/operation/frontlineattack.py +++ b/game/operation/frontlineattack.py @@ -35,6 +35,7 @@ class FrontlineAttackOperation(Operation): conflict=conflict) def generate(self): - self.briefinggen.title = "Frontline CAS" - self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." + ## TODO: What are these for? + # self.briefinggen.title = "Frontline CAS" + # self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu." super(FrontlineAttackOperation, self).generate() diff --git a/game/operation/operation.py b/game/operation/operation.py index 69c152ba..b8f46f67 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -357,9 +357,9 @@ class Operation: "position": { "x": flightTarget.position.x, "y": flightTarget.position.y} } - - self.briefinggen.generate() - kneeboard_generator.generate() + ## These are being called twice in this method? + # self.briefinggen.generate() + # kneeboard_generator.generate() # set a LUA table with data from Liberation that we want to set diff --git a/gen/aircraft.py b/gen/aircraft.py index 2c1f3d71..e4c3443f 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -277,6 +277,10 @@ class FlightData: def aircraft_type(self) -> FlyingType: """Returns the type of aircraft in this flight.""" return self.units[0].unit_type + + @property + def departure_delay_delta(self) -> timedelta: + return timedelta(seconds=self.departure_delay) def num_radio_channels(self, radio_id: int) -> int: """Returns the number of preset channels for the given radio.""" diff --git a/gen/briefinggen.py b/gen/briefinggen.py index 08618d8f..d7bf7ecb 100644 --- a/gen/briefinggen.py +++ b/gen/briefinggen.py @@ -1,13 +1,11 @@ -import datetime import os import random -from collections import defaultdict from dataclasses import dataclass -from typing import List +from typing import List, Dict, TYPE_CHECKING +from jinja2 import Environment, FileSystemLoader, select_autoescape from dcs.mission import Mission -from game import db from .aircraft import FlightData from .airsupportgen import AwacsInfo, TankerInfo from .armor import JtacInfo @@ -16,6 +14,9 @@ from .ground_forces.combat_stance import CombatStance from .radios import RadioFrequency from .runways import RunwayData +if TYPE_CHECKING: + from game import Game + @dataclass class CommInfo: @@ -24,6 +25,17 @@ class CommInfo: freq: RadioFrequency +@dataclass +class BriefingInfo: + description: str + + +@dataclass +class FrontLineInfo(BriefingInfo): + enemy_base: str + player_base: str + + class MissionInfoGenerator: """Base type for generators of mission information for the player. @@ -37,6 +49,7 @@ class MissionInfoGenerator: self.flights: List[FlightData] = [] self.jtacs: List[JtacInfo] = [] self.tankers: List[TankerInfo] = [] + self.frontlines: List[FrontLineInfo] = [] def add_awacs(self, awacs: AwacsInfo) -> None: """Adds an AWACS/GCI to the mission. @@ -79,6 +92,9 @@ class MissionInfoGenerator: """ self.tankers.append(tanker) + def add_frontline(self, frontline: FrontLineInfo) -> None: + self.frontlines.append(frontline) + def generate(self) -> None: """Generates the mission information.""" raise NotImplementedError @@ -86,13 +102,23 @@ class MissionInfoGenerator: class BriefingGenerator(MissionInfoGenerator): - def __init__(self, mission: Mission, conflict: Conflict, game): + def __init__(self, mission: Mission, conflict: Conflict, game: 'Game'): super().__init__(mission) self.conflict = conflict self.game = game self.title = "" self.description = "" self.dynamic_runways: List[RunwayData] = [] + self.allied_flights_by_departure: Dict[str, List[FlightData]] = {} + env = Environment( + loader=FileSystemLoader('resources/briefing/templates'), + autoescape=select_autoescape( + disabled_extensions=('txt'), + default_for_string=True, + default=True, + ) + ) + self.template = env.get_template('briefingtemplate_EN.j2') def add_dynamic_runway(self, runway: RunwayData) -> None: """Adds a dynamically generated runway to the briefing. @@ -102,142 +128,16 @@ class BriefingGenerator(MissionInfoGenerator): """ self.dynamic_runways.append(runway) - def add_flight_description(self, flight: FlightData): - assert flight.client_units - - aircraft = flight.aircraft_type - flight_unit_name = db.unit_type_name(aircraft) - self.description += "-" * 50 + "\n" - self.description += f"{flight_unit_name} x {flight.size}\n\n" - - for i, wpt in enumerate(flight.waypoints): - self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n" - self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n" - - def add_ally_flight_description(self, flight: FlightData): - assert not flight.client_units - aircraft = flight.aircraft_type - flight_unit_name = db.unit_type_name(aircraft) - delay = datetime.timedelta(seconds=flight.departure_delay) - self.description += ( - f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, " - f"departing in {delay}\n" - ) - def generate(self): - self.description = "" - - self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n" - self.description += "=" * 15 + "\n\n" - - self.description += ( - "Most briefing information, including communications and flight " - "plan information, can be found on your kneeboard.\n\n" - ) - self.generate_ongoing_war_text() - - self.description += "\n"*2 - self.description += "Your flights:" + "\n" - self.description += "=" * 15 + "\n\n" - - for flight in self.flights: - if flight.client_units: - self.add_flight_description(flight) - - self.description += "\n"*2 - self.description += "Planned ally flights:" + "\n" - self.description += "=" * 15 + "\n" - allied_flights_by_departure = defaultdict(list) - for flight in self.flights: - if not flight.client_units and flight.friendly: - name = flight.departure.airfield_name - allied_flights_by_departure[name].append(flight) - for departure, flights in allied_flights_by_departure.items(): - self.description += f"\nFrom {departure}\n" - self.description += "-" * 50 + "\n\n" - for flight in flights: - self.add_ally_flight_description(flight) - - if self.comms: - self.description += "\n\nComms Frequencies:\n" - self.description += "=" * 15 + "\n" - for comm_info in self.comms: - self.description += f"{comm_info.name}: {comm_info.freq}\n" - self.description += ("-" * 50) + "\n" - - for runway in self.dynamic_runways: - self.description += f"{runway.airfield_name}\n" - self.description += f"RADIO : {runway.atc}\n" - if runway.tacan is not None: - self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n" - if runway.icls is not None: - self.description += f"ICLS Channel : {runway.icls}\n" - self.description += "-" * 50 + "\n" - - - self.description += "JTACS [F-10 Menu] : \n" - self.description += "===================\n\n" - for jtac in self.jtacs: - self.description += f"{jtac.region} -- Code : {jtac.code}\n" - - self.mission.set_description_text(self.description) - + self.generate_allied_flights_by_departure() + self.mission.set_description_text(self.template.render(vars(self))) + ## TODO: Remove debug code + with open(r'C:\Users\walte\Documents\briefing.txt', 'w') as file: + file.write(self.template.render(vars(self))) self.mission.add_picture_blue(os.path.abspath( "./resources/ui/splash_screen.png")) - - def generate_ongoing_war_text(self): - - self.description += "Current situation:\n" - self.description += "=" * 15 + "\n\n" - - conflict_number = 0 - - for front_line in self.game.theater.conflicts(from_player=True): - conflict_number = conflict_number + 1 - player_base = front_line.control_point_a - enemy_base = front_line.control_point_b - - has_numerical_superiority = player_base.base.total_armor > enemy_base.base.total_armor - self.description += self.__random_frontline_sentence(player_base.name, enemy_base.name) - - if enemy_base.id in player_base.stances.keys(): - stance = player_base.stances[enemy_base.id] - - if player_base.base.total_armor == 0: - self.description += "We do not have a single vehicle available to hold our position, the situation is critical, and we will lose ground inevitably.\n" - elif enemy_base.base.total_armor == 0: - self.description += "The enemy forces have been crushed, we will be able to make significant progress toward " + enemy_base.name + ". \n" - if stance == CombatStance.AGGRESSIVE: - if has_numerical_superiority: - self.description += "On this location, our ground forces will try to make progress against the enemy" - self.description += ". As the enemy is outnumbered, our forces should have no issue making progress.\n" - else: - self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n" - elif stance == CombatStance.ELIMINATION: - if has_numerical_superiority: - self.description += "On this location, our ground forces will focus on the destruction of enemy assets, before attempting to make progress toward " + enemy_base.name + ". " - self.description += "The enemy is already outnumbered, and this maneuver might draw a final blow to their forces.\n" - else: - self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n" - elif stance == CombatStance.BREAKTHROUGH: - if has_numerical_superiority: - self.description += "On this location, our ground forces will focus on progression toward " + enemy_base.name + ".\n" - else: - self.description += "On this location, our ground forces have been ordered to rush toward " + enemy_base.name + ". Wish them luck... We are also expecting a counter attack.\n" - elif stance in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]: - if has_numerical_superiority: - self.description += "On this location, our ground forces will hold position. We are not expecting an enemy assault.\n" - else: - self.description += "On this location, our ground forces have been ordered to hold still, and defend against enemy attacks. An enemy assault might be iminent.\n" - - if conflict_number == 0: - self.description += "There are currently no fights on the ground.\n" - - self.description += "\n\n" - - def __random_frontline_sentence(self, player_base_name, enemy_base_name): templates = [ "There are combats between {} and {}. ", @@ -248,4 +148,54 @@ class BriefingGenerator(MissionInfoGenerator): ] return random.choice(templates).format(player_base_name, enemy_base_name) + # TODO: refactor this, perhaps move to FrontLineInfo factory object? + def generate_ongoing_war_text(self): + for front_line in self.game.theater.conflicts(from_player=True): + player_base = front_line.control_point_a + enemy_base = front_line.control_point_b + has_numerical_superiority = player_base.base.total_armor > enemy_base.base.total_armor + description = self.__random_frontline_sentence(player_base.name, enemy_base.name) + stance = player_base.stances[enemy_base.id] ## Sometimes this contains enum value, sometimes it contains int. + if player_base.base.total_armor == 0: + player_zero = True + description += "We do not have a single vehicle available to hold our position, the situation is critical, and we will lose ground inevitably.\n" + elif enemy_base.base.total_armor == 0: + player_zero = False + description += "The enemy forces have been crushed, we will be able to make significant progress toward " + enemy_base.name + ". \n" + else: player_zero = False + if not player_zero: + if stance == CombatStance.AGGRESSIVE: + if has_numerical_superiority: + description += "On this location, our ground forces will try to make progress against the enemy" + description += ". As the enemy is outnumbered, our forces should have no issue making progress.\n" + else: + description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n" + elif stance == CombatStance.ELIMINATION: + if has_numerical_superiority: + description += "On this location, our ground forces will focus on the destruction of enemy assets, before attempting to make progress toward " + enemy_base.name + ". " + description += "The enemy is already outnumbered, and this maneuver might draw a final blow to their forces.\n" + else: + description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n" + elif stance == CombatStance.BREAKTHROUGH: + if has_numerical_superiority: + description += "On this location, our ground forces will focus on progression toward " + enemy_base.name + ".\n" + else: + description += "On this location, our ground forces have been ordered to rush toward " + enemy_base.name + ". Wish them luck... We are also expecting a counter attack.\n" + elif stance in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]: + if has_numerical_superiority: + description += "On this location, our ground forces will hold position. We are not expecting an enemy assault.\n" + else: + description += "On this location, our ground forces have been ordered to hold still, and defend against enemy attacks. An enemy assault might be iminent.\n" + self.add_frontline(FrontLineInfo(description, enemy_base, player_base)) + + # TODO: This should determine if runway is friendly through a method more robust than the existing string match + def generate_allied_flights_by_departure(self) -> None: + for flight in self.flights: + if not flight.client_units and flight.friendly: ## where else can we get this? + name = flight.departure.airfield_name + if name in self.allied_flights_by_departure.keys(): + self.allied_flights_by_departure[name].append(flight) + else: + self.allied_flights_by_departure[name] = [flight] + diff --git a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py index 2ccdf40b..09c3fa5b 100644 --- a/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py +++ b/qt_ui/windows/basemenu/ground_forces/QGroundForcesStrategySelector.py @@ -14,8 +14,8 @@ class QGroundForcesStrategySelector(QComboBox): self.cp.stances[enemy_cp.id] = CombatStance.DEFENSIVE for i, stance in enumerate(CombatStance): - self.addItem(stance.name, userData=stance.value) - if self.cp.stances[enemy_cp.id] == stance.value: + self.addItem(stance.name, userData=stance) + if self.cp.stances[enemy_cp.id] == stance: self.setCurrentIndex(i) self.currentTextChanged.connect(self.on_change) diff --git a/requirements.txt b/requirements.txt index 3ce6e8d3..41233a08 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ Pillow~=7.2.0 tabulate~=0.8.7 mypy==0.782 -mypy-extensions==0.4.3 \ No newline at end of file +mypy-extensions==0.4.3 +jinja2>=2.11.2 diff --git a/resources/briefing/templates/briefingtemplate_EN.j2 b/resources/briefing/templates/briefingtemplate_EN.j2 new file mode 100644 index 00000000..fb7cf30d --- /dev/null +++ b/resources/briefing/templates/briefingtemplate_EN.j2 @@ -0,0 +1,48 @@ +DCS Liberation Turn {{ game.turn }} +==================== + +Most briefing information, including communications and flight plan information, can be found on your kneeboard. + +Current situation: +==================== +{% if not frontlines %}There are currently no fights on the ground.{% endif %}{% if frontlines %}{% for frontline in frontlines %} +{{ frontline.description }}{% endfor %}{% endif %} + +Your flights: +==================== +{% for flight in flights if flight.client_units %} +-------------------------------------------------- +{{ flight.flight_type.name }} {{ flight.units[0].type }} x {{ flight.size }}, {{ flight.package.target.name}} +{% for waypoint in flight.waypoints %}{{ loop.index }} -- {{waypoint.name}} : {{ waypoint.description}} +{% endfor %} +--------------------------------------------------{% endfor %} + + +Planned ally flights: +==================== +{% for dep in allied_flights_by_departure %} +{{ dep }} +--------------------------------------------------- +{% for flight in allied_flights_by_departure[dep] %} +{{ flight.flight_type.name }} {{ flight.units[0].type }} x {{flight.size}}, departing in {{ flight.departure_delay_delta }}, {{ flight.package.target.name}}{% endfor %} +{% endfor %} + +Carriers and FARPs: +===================={% for runway in dynamic_runways %} +-------------------------------------------------- +{{ runway.airfield_name}} +RADIO : {{ runway.atc }} +TACAN : {{ runway.tacan }} {{ runway.tacan_callsign }} +{% if runway.icls %}ICLS Channel: {{ runway.icls }}{% endif %} +{% endfor %} + +AWACS: +==================== +{% for i in awacs %}{{ i.callsign }} -- Freq : {{i.freq.mhz}} +{% endfor %} + +JTACS [F-10 Menu] : +==================== +{% for jtac in jtacs %}Frontline {{ jtac.region }} -- Code : {{ jtac.code }} +{% endfor %} +