From 49b6951ac35d45bfd334cf146b02b7c455166b36 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 16 Oct 2020 18:25:25 -0700 Subject: [PATCH] Generate weather conditions at turn start. Weather and exact time of day information is helpful during mission planning, so generate it at the start of the turn rather than at takeoff time. Another advantage aside from planning is that we can now use the wind information to set carrier headings and takeoff runways appropriately. --- game/event/event.py | 2 - game/game.py | 24 +++- game/operation/operation.py | 13 +- game/weather.py | 183 ++++++++++++++++++++++++++++ gen/environmentgen.py | 163 ++++--------------------- qt_ui/widgets/QTopPanel.py | 2 +- qt_ui/widgets/QTurnCounter.py | 45 ++++--- qt_ui/widgets/map/QLiberationMap.py | 5 +- 8 files changed, 266 insertions(+), 171 deletions(-) create mode 100644 game/weather.py diff --git a/game/event/event.py b/game/event/event.py index 0af3852c..8f7ac1b8 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -12,7 +12,6 @@ from game import db, persistency from game.debriefing import Debriefing from game.infos.information import Information from game.operation.operation import Operation -from gen.environmentgen import EnvironmentSettings from gen.ground_forces.combat_stance import CombatStance from theater import ControlPoint from theater.start_generator import generate_airbase_defense_group @@ -42,7 +41,6 @@ class Event: operation = None # type: Operation difficulty = 1 # type: int - environment_settings = None # type: EnvironmentSettings BONUS_BASE = 5 def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str): diff --git a/game/game.py b/game/game.py index dcac8220..e8e38662 100644 --- a/game/game.py +++ b/game/game.py @@ -2,7 +2,7 @@ import logging import math import random import sys -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from typing import Any, Dict, List from dcs.action import Coalition @@ -28,6 +28,7 @@ from .event.event import Event, UnitsDeliveryEvent from .event.frontlineattack import FrontlineAttackEvent from .infos.information import Information from .settings import Settings +from .weather import Conditions, TimeOfDay COMMISION_UNIT_VARIETY = 4 COMMISION_LIMITS_SCALE = 1.5 @@ -78,7 +79,7 @@ class Game: self.enemy_name = enemy_name self.enemy_country = db.FACTIONS[enemy_name]["country"] self.turn = 0 - self.date = datetime(start_date.year, start_date.month, start_date.day) + self.date = date(start_date.year, start_date.month, start_date.day) self.game_stats = GameStats() self.game_stats.update(self) self.ground_planners: Dict[int, GroundPlanner] = {} @@ -91,6 +92,8 @@ class Game: self.current_unit_id = 0 self.current_group_id = 0 + self.conditions = self.generate_conditions() + self.blue_ato = AirTaskingOrder() self.red_ato = AirTaskingOrder() @@ -101,6 +104,9 @@ class Game: self.sanitize_sides() self.on_load() + def generate_conditions(self) -> Conditions: + return Conditions.generate(self.theater, self.date, + self.current_turn_time_of_day, self.settings) def sanitize_sides(self): """ @@ -218,6 +224,12 @@ class Game: def on_load(self) -> None: ObjectiveDistanceCache.set_theater(self.theater) + # Save game compatibility. + + # TODO: Remove in 2.3. + if not hasattr(self, "conditions"): + self.conditions = self.generate_conditions() + def pass_turn(self, no_action=False): logging.info("Pass turn") self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0)) @@ -252,6 +264,8 @@ class Game: for cp in self.theater.controlpoints: self.aircraft_inventory.set_from_control_point(cp) + self.conditions = self.generate_conditions() + # Plan flights & combat for next turn self.__culling_points = self.compute_conflicts_position() self.ground_planners = {} @@ -340,11 +354,11 @@ class Game: self.informations.append(info) @property - def current_turn_daytime(self): - return ["dawn", "day", "dusk", "night"][self.turn % 4] + def current_turn_time_of_day(self) -> TimeOfDay: + return list(TimeOfDay)[self.turn % 4] @property - def current_day(self): + def current_day(self) -> date: return self.date + timedelta(days=self.turn // 4) def next_unit_id(self): diff --git a/game/operation/operation.py b/game/operation/operation.py index ecc82e51..1f12d623 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -21,7 +21,7 @@ from gen.airsupportgen import AirSupport, AirSupportConflictGenerator from gen.armor import GroundConflictGenerator, JtacInfo from gen.beacons import load_beacons_for_terrain from gen.briefinggen import BriefingGenerator -from gen.environmentgen import EnviromentGenerator +from gen.environmentgen import EnvironmentGenerator from gen.forcedoptionsgen import ForcedOptionsGenerator from gen.groundobjectsgen import GroundObjectsGenerator from gen.kneeboard import KneeboardGenerator @@ -45,7 +45,6 @@ class Operation: triggersgen = None # type: TriggersGenerator airsupportgen = None # type: AirSupportConflictGenerator visualgen = None # type: VisualGenerator - envgen = None # type: EnviromentGenerator groundobjectgen = None # type: GroundObjectsGenerator briefinggen = None # type: BriefingGenerator forcedoptionsgen = None # type: ForcedOptionsGenerator @@ -162,13 +161,9 @@ class Operation: for frequency in unique_map_frequencies: radio_registry.reserve(frequency) - # Generate meteo - envgen = EnviromentGenerator(self.current_mission, self.conflict, - self.game) - if self.environment_settings is None: - self.environment_settings = envgen.generate() - else: - envgen.load(self.environment_settings) + # Set mission time and weather conditions. + EnvironmentGenerator(self.current_mission, + self.game.conditions).generate() # Generate ground object first diff --git a/game/weather.py b/game/weather.py new file mode 100644 index 00000000..a9ac5141 --- /dev/null +++ b/game/weather.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import datetime +import logging +import random +from dataclasses import dataclass +from enum import Enum +from typing import Optional + +from dcs.weather import Weather as PydcsWeather, Wind + +from game.settings import Settings +from theater import ConflictTheater + + +class TimeOfDay(Enum): + Dawn = "dawn" + Day = "day" + Dusk = "dusk" + Night = "night" + + +@dataclass(frozen=True) +class WindConditions: + at_0m: Wind + at_2000m: Wind + at_8000m: Wind + + +@dataclass(frozen=True) +class Clouds: + base: int + density: int + thickness: int + precipitation: PydcsWeather.Preceptions + + +@dataclass(frozen=True) +class Fog: + visibility: int + thickness: int + + +class Weather: + def __init__(self) -> None: + self.clouds = self.generate_clouds() + self.fog = self.generate_fog() + self.wind = self.generate_wind() + + def generate_clouds(self) -> Optional[Clouds]: + raise NotImplementedError + + def generate_fog(self) -> Optional[Fog]: + if random.randrange(5) != 0: + return None + return Fog( + visibility=random.randint(2500, 5000), + thickness=random.randint(100, 500) + ) + + def generate_wind(self) -> WindConditions: + raise NotImplementedError + + @staticmethod + def random_wind(minimum: int, maximum) -> WindConditions: + wind_direction = random.randint(0, 360) + at_0m_factor = 1 + at_2000m_factor = 2 + at_8000m_factor = 3 + base_wind = random.randint(minimum, maximum) + + return WindConditions( + # Always some wind to make the smoke move a bit. + at_0m=Wind(wind_direction, min(1, base_wind * at_0m_factor)), + at_2000m=Wind(wind_direction, base_wind * at_2000m_factor), + at_8000m=Wind(wind_direction, base_wind * at_8000m_factor) + ) + + @staticmethod + def random_cloud_base() -> int: + return random.randint(2000, 3000) + + @staticmethod + def random_cloud_thickness() -> int: + return random.randint(100, 400) + + +class ClearSkies(Weather): + def generate_clouds(self) -> Optional[Clouds]: + return None + + def generate_fog(self) -> Optional[Fog]: + return None + + def generate_wind(self) -> WindConditions: + return self.random_wind(0, 0) + + +class Cloudy(Weather): + def generate_clouds(self) -> Optional[Clouds]: + return Clouds( + base=self.random_cloud_base(), + density=random.randint(1, 8), + thickness=self.random_cloud_thickness(), + precipitation=PydcsWeather.Preceptions.None_ + ) + + def generate_wind(self) -> WindConditions: + return self.random_wind(0, 4) + + +class Raining(Weather): + def generate_clouds(self) -> Optional[Clouds]: + return Clouds( + base=self.random_cloud_base(), + density=random.randint(5, 8), + thickness=self.random_cloud_thickness(), + precipitation=PydcsWeather.Preceptions.Rain + ) + + def generate_wind(self) -> WindConditions: + return self.random_wind(0, 6) + + +class Thunderstorm(Weather): + def generate_clouds(self) -> Optional[Clouds]: + return Clouds( + base=self.random_cloud_base(), + density=random.randint(9, 10), + thickness=self.random_cloud_thickness(), + precipitation=PydcsWeather.Preceptions.Thunderstorm + ) + + def generate_wind(self) -> WindConditions: + return self.random_wind(0, 8) + + +@dataclass +class Conditions: + time_of_day: TimeOfDay + start_time: datetime.datetime + weather: Weather + + @classmethod + def generate(cls, theater: ConflictTheater, day: datetime.date, + time_of_day: TimeOfDay, settings: Settings) -> Conditions: + return cls( + time_of_day=time_of_day, + start_time=cls.generate_start_time( + theater, day, time_of_day, settings.night_disabled + ), + weather=cls.generate_weather() + ) + + @classmethod + def generate_start_time(cls, theater: ConflictTheater, day: datetime.date, + time_of_day: TimeOfDay, + night_disabled: bool) -> datetime.datetime: + if night_disabled: + logging.info("Skip Night mission due to user settings") + time_range = { + TimeOfDay.Dawn: (8, 9), + TimeOfDay.Day: (10, 12), + TimeOfDay.Dusk: (12, 14), + TimeOfDay.Night: (14, 17), + }[time_of_day] + else: + time_range = theater.daytime_map[time_of_day.value] + + time = datetime.time(hour=random.randint(*time_range)) + return datetime.datetime.combine(day, time) + + @classmethod + def generate_weather(cls) -> Weather: + chances = { + Thunderstorm: 1, + Raining: 20, + Cloudy: 60, + ClearSkies: 20, + } + weather_type = random.choices(list(chances.keys()), + weights=list(chances.values()))[0] + return weather_type() diff --git a/gen/environmentgen.py b/gen/environmentgen.py index 57d70452..7712cea5 100644 --- a/gen/environmentgen.py +++ b/gen/environmentgen.py @@ -1,147 +1,36 @@ -import logging -import random -from datetime import timedelta +from typing import Optional from dcs.mission import Mission -from dcs.weather import Weather, Wind -from .conflictgen import Conflict - -WEATHER_CLOUD_BASE = 2000, 3000 -WEATHER_CLOUD_DENSITY = 1, 8 -WEATHER_CLOUD_THICKNESS = 100, 400 -WEATHER_CLOUD_BASE_MIN = 1600 - -WEATHER_FOG_CHANCE = 20 -WEATHER_FOG_VISIBILITY = 2500, 5000 -WEATHER_FOG_THICKNESS = 100, 500 - -RANDOM_TIME = { - "night": 7, - "dusk": 40, - "dawn": 40, - "day": 100, -} - -RANDOM_WEATHER = { - 1: 0, # thunderstorm - 2: 20, # rain - 3: 80, # clouds - 4: 100, # clear -} +from game.weather import Clouds, Fog, Conditions, WindConditions -class EnvironmentSettings: - weather_dict = None - start_time = None - - -class EnviromentGenerator: - def __init__(self, mission: Mission, conflict: Conflict, game): +class EnvironmentGenerator: + def __init__(self, mission: Mission, conditions: Conditions) -> None: self.mission = mission - self.conflict = conflict - self.game = game + self.conditions = conditions - def _gen_time(self): + def set_clouds(self, clouds: Optional[Clouds]) -> None: + if clouds is None: + return + self.mission.weather.clouds_base = clouds.base + self.mission.weather.clouds_thickness = clouds.thickness + self.mission.weather.clouds_density = clouds.density + self.mission.weather.clouds_iprecptns = clouds.precipitation - start_time = self.game.current_day + def set_fog(self, fog: Optional[Fog]) -> None: + if fog is None: + return + self.mission.weather.fog_visibility = fog.visibility + self.mission.weather.fog_thickness = fog.thickness - daytime = self.game.current_turn_daytime - logging.info("Mission time will be {}".format(daytime)) - if self.game.settings.night_disabled: - logging.info("Skip Night mission due to user settings") - if daytime == "dawn": - time_range = (8, 9) - elif daytime == "day": - time_range = (10, 12) - elif daytime == "dusk": - time_range = (12, 14) - elif daytime == "night": - time_range = (14, 17) - else: - time_range = (10, 12) - else: - time_range = self.game.theater.daytime_map[daytime] - - start_time += timedelta(hours=random.randint(*time_range)) - - logging.info("time - {}, slot - {}, night skipped - {}".format( - str(start_time), - str(time_range), - self.game.settings.night_disabled)) - - self.mission.start_time = start_time - - def _generate_wind(self, wind_speed, wind_direction=None): - # wind - if not wind_direction: - wind_direction = random.randint(0, 360) - - self.mission.weather.wind_at_ground = Wind(wind_direction, wind_speed) - self.mission.weather.wind_at_2000 = Wind(wind_direction, wind_speed * 2) - self.mission.weather.wind_at_8000 = Wind(wind_direction, wind_speed * 3) - - def _generate_base_weather(self): - # clouds - self.mission.weather.clouds_base = random.randint(*WEATHER_CLOUD_BASE) - self.mission.weather.clouds_density = random.randint(*WEATHER_CLOUD_DENSITY) - self.mission.weather.clouds_thickness = random.randint(*WEATHER_CLOUD_THICKNESS) - - # wind - self._generate_wind(random.randint(0, 4)) - - # fog - if random.randint(0, 100) < WEATHER_FOG_CHANCE: - self.mission.weather.fog_visibility = random.randint(*WEATHER_FOG_VISIBILITY) - self.mission.weather.fog_thickness = random.randint(*WEATHER_FOG_THICKNESS) - - def _gen_random_weather(self): - weather_type = None - for k, v in RANDOM_WEATHER.items(): - if random.randint(0, 100) <= v: - weather_type = k - break - - logging.info("generated weather {}".format(weather_type)) - if weather_type == 1: - # thunderstorm - self._generate_base_weather() - self._generate_wind(random.randint(0, 8)) - - self.mission.weather.clouds_density = random.randint(9, 10) - self.mission.weather.clouds_iprecptns = Weather.Preceptions.Thunderstorm - elif weather_type == 2: - # rain - self._generate_base_weather() - self.mission.weather.clouds_density = random.randint(5, 8) - self.mission.weather.clouds_iprecptns = Weather.Preceptions.Rain - - self._generate_wind(random.randint(0, 6)) - elif weather_type == 3: - # clouds - self._generate_base_weather() - elif weather_type == 4: - # clear - pass - - if self.mission.weather.clouds_density > 0: - # sometimes clouds are randomized way too low and need to be fixed - self.mission.weather.clouds_base = max(self.mission.weather.clouds_base, WEATHER_CLOUD_BASE_MIN) - - if self.mission.weather.wind_at_ground.speed == 0: - # frontline smokes look silly w/o any wind - self._generate_wind(1) - - def generate(self) -> EnvironmentSettings: - self._gen_time() - self._gen_random_weather() - - settings = EnvironmentSettings() - settings.start_time = self.mission.start_time - settings.weather_dict = self.mission.weather.dict() - return settings - - def load(self, settings: EnvironmentSettings): - self.mission.start_time = settings.start_time - self.mission.weather.load_from_dict(settings.weather_dict) + def set_wind(self, wind: WindConditions) -> None: + self.mission.weather.wind_at_ground = wind.at_0m + self.mission.weather.wind_at_2000 = wind.at_2000m + self.mission.weather.wind_at_8000 = wind.at_8000m + def generate(self): + self.mission.start_time = self.conditions.start_time + self.set_clouds(self.conditions.weather.clouds) + self.set_fog(self.conditions.weather.fog) + self.set_wind(self.conditions.weather.wind) diff --git a/qt_ui/widgets/QTopPanel.py b/qt_ui/widgets/QTopPanel.py index d115c98b..dadbee0d 100644 --- a/qt_ui/widgets/QTopPanel.py +++ b/qt_ui/widgets/QTopPanel.py @@ -97,7 +97,7 @@ class QTopPanel(QFrame): if game is None: return - self.turnCounter.setCurrentTurn(game.turn, game.current_day) + self.turnCounter.setCurrentTurn(game.turn, game.conditions) self.budgetBox.setGame(game) self.factionsInfos.setGame(game) diff --git a/qt_ui/widgets/QTurnCounter.py b/qt_ui/widgets/QTurnCounter.py index f7e6fd88..a26112e1 100644 --- a/qt_ui/widgets/QTurnCounter.py +++ b/qt_ui/widgets/QTurnCounter.py @@ -1,7 +1,8 @@ import datetime -from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox +from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QVBoxLayout +from game.weather import Conditions, TimeOfDay import qt_ui.uiconstants as CONST @@ -13,23 +14,37 @@ class QTurnCounter(QGroupBox): def __init__(self): super(QTurnCounter, self).__init__("Turn") - self.icons = [CONST.ICONS["Dawn"], CONST.ICONS["Day"], CONST.ICONS["Dusk"], CONST.ICONS["Night"]] - - self.daytime_icon = QLabel() - self.daytime_icon.setPixmap(self.icons[0]) - self.turn_info = QLabel() + self.icons = { + TimeOfDay.Dawn: CONST.ICONS["Dawn"], + TimeOfDay.Day: CONST.ICONS["Day"], + TimeOfDay.Dusk: CONST.ICONS["Dusk"], + TimeOfDay.Night: CONST.ICONS["Night"], + } self.layout = QHBoxLayout() - self.layout.addWidget(self.daytime_icon) - self.layout.addWidget(self.turn_info) self.setLayout(self.layout) - def setCurrentTurn(self, turn: int, current_day: datetime): + self.daytime_icon = QLabel() + self.daytime_icon.setPixmap(self.icons[TimeOfDay.Dawn]) + self.layout.addWidget(self.daytime_icon) + + self.time_column = QVBoxLayout() + self.layout.addLayout(self.time_column) + + self.date_display = QLabel() + self.time_column.addWidget(self.date_display) + + self.time_display = QLabel() + self.time_column.addWidget(self.time_display) + + def setCurrentTurn(self, turn: int, conditions: Conditions) -> None: + """Sets the turn information display. + + :arg turn Current turn number. + :arg conditions Current time and weather conditions. """ - Set the money amount to display - :arg turn Current turn number - :arg current_day Current day - """ - self.daytime_icon.setPixmap(self.icons[turn % 4]) - self.turn_info.setText(current_day.strftime("%d %b %Y")) + self.daytime_icon.setPixmap(self.icons[conditions.time_of_day]) + self.date_display.setText(conditions.start_time.strftime("%d %b %Y")) + self.time_display.setText( + conditions.start_time.strftime("%H:%M:%S Local")) self.setTitle("Turn " + str(turn + 1)) diff --git a/qt_ui/widgets/map/QLiberationMap.py b/qt_ui/widgets/map/QLiberationMap.py index 2c83996c..7344275f 100644 --- a/qt_ui/widgets/map/QLiberationMap.py +++ b/qt_ui/widgets/map/QLiberationMap.py @@ -21,6 +21,7 @@ from game import Game, db from game.data.aaa_db import AAA_UNITS from game.data.radar_db import UNITS_WITH_RADAR from game.utils import meter_to_feet +from game.weather import TimeOfDay from gen import Conflict, PackageWaypointTiming from gen.ato import Package from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType @@ -509,9 +510,9 @@ class QLiberationMap(QGraphicsView): scene.addPixmap(bg) # Apply graphical effects to simulate current daytime - if self.game.current_turn_daytime == "day": + if self.game.current_turn_time_of_day == TimeOfDay.Day: pass - elif self.game.current_turn_daytime == "night": + elif self.game.current_turn_time_of_day == TimeOfDay.Night: ov = QPixmap(bg.width(), bg.height()) ov.fill(CONST.COLORS["night_overlay"]) overlay = scene.addPixmap(ov)