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)