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)
This commit is contained in:
walterroach 2020-11-23 16:46:05 -06:00
parent 63bdbebcaa
commit da17d1e5d1
8 changed files with 182 additions and 206 deletions

14
game/event/airwar.py Normal file
View File

@ -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"

View File

@ -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)

View File

@ -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,)

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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")
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")

View File

@ -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,