diff --git a/game/game.py b/game/game.py index 8d2a59e0..9c45763c 100644 --- a/game/game.py +++ b/game/game.py @@ -37,7 +37,7 @@ from .theater.theatergroundobject import ( ) from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder from .timeofday import TimeOfDay -from .weather import Conditions +from .weather.conditions import Conditions if TYPE_CHECKING: from .ato.airtaaskingorder import AirTaskingOrder diff --git a/game/missiongenerator/environmentgenerator.py b/game/missiongenerator/environmentgenerator.py index 24b1dc8b..9fbae5e4 100644 --- a/game/missiongenerator/environmentgenerator.py +++ b/game/missiongenerator/environmentgenerator.py @@ -3,7 +3,11 @@ from typing import Optional from dcs.mission import Mission -from game.weather import Clouds, Fog, Conditions, WindConditions, AtmosphericConditions +from game.weather.atmosphericconditions import AtmosphericConditions +from game.weather.clouds import Clouds +from game.weather.conditions import Conditions +from game.weather.fog import Fog +from game.weather.wind import WindConditions class EnvironmentGenerator: diff --git a/game/missiongenerator/kneeboard.py b/game/missiongenerator/kneeboard.py index 1d41cee4..910d318e 100644 --- a/game/missiongenerator/kneeboard.py +++ b/game/missiongenerator/kneeboard.py @@ -44,7 +44,7 @@ from game.runways import RunwayData from game.theater import TheaterGroundObject, TheaterUnit from game.theater.bullseye import Bullseye from game.utils import Distance, UnitSystem, meters, mps, pounds -from game.weather import Weather +from game.weather.weather import Weather from .aircraft.flightdata import FlightData from .airsupportgenerator import AwacsInfo, TankerInfo from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator diff --git a/game/runways.py b/game/runways.py index 7eee0241..340a8720 100644 --- a/game/runways.py +++ b/game/runways.py @@ -11,7 +11,7 @@ from game.dcs.beacons import BeaconType, Beacons from game.radio.radios import RadioFrequency from game.radio.tacan import TacanChannel from game.utils import Heading -from game.weather import Conditions +from game.weather.conditions import Conditions if TYPE_CHECKING: from game.theater import ConflictTheater diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 243950e2..4cb33472 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -75,7 +75,7 @@ from ..db import Database from ..dcs.aircrafttype import AircraftType from ..dcs.groundunittype import GroundUnitType from ..utils import nautical_miles -from ..weather import Conditions +from ..weather.conditions import Conditions if TYPE_CHECKING: from game import Game diff --git a/game/weather/__init__.py b/game/weather/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/game/weather/atmosphericconditions.py b/game/weather/atmosphericconditions.py new file mode 100644 index 00000000..d075ba06 --- /dev/null +++ b/game/weather/atmosphericconditions.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from game.utils import Pressure + + +@dataclass(frozen=True) +class AtmosphericConditions: + #: Pressure at sea level. + qnh: Pressure + + #: Temperature at sea level in Celcius. + temperature_celsius: float + + #: Turbulence per 10 cm. + turbulence_per_10cm: float diff --git a/game/weather/clouds.py b/game/weather/clouds.py new file mode 100644 index 00000000..ed1e087b --- /dev/null +++ b/game/weather/clouds.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import random +from dataclasses import dataclass, field +from typing import Optional + +from dcs.cloud_presets import Clouds as PydcsClouds +from dcs.weather import Weather as PydcsWeather, CloudPreset + + +@dataclass(frozen=True) +class Clouds: + base: int + density: int + thickness: int + precipitation: PydcsWeather.Preceptions + preset: Optional[CloudPreset] = field(default=None) + + @classmethod + def random_preset(cls, rain: bool) -> Clouds: + clouds = (p.value for p in PydcsClouds) + if rain: + presets = [p for p in clouds if "Rain" in p.name] + else: + presets = [p for p in clouds if "Rain" not in p.name] + preset = random.choice(presets) + return Clouds( + base=random.randint(preset.min_base, preset.max_base), + density=0, + thickness=0, + precipitation=PydcsWeather.Preceptions.None_, + preset=preset, + ) diff --git a/game/weather/conditions.py b/game/weather/conditions.py new file mode 100644 index 00000000..9eeeccc2 --- /dev/null +++ b/game/weather/conditions.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import datetime +import logging +import random +from dataclasses import dataclass + +from game.settings import Settings +from game.theater import ConflictTheater, DaytimeMap, SeasonalConditions +from game.theater.seasonalconditions import determine_season +from game.timeofday import TimeOfDay +from game.weather.weather import Weather, Thunderstorm, Raining, Cloudy, ClearSkies + + +@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, + forced_time: datetime.time | None = None, + ) -> Conditions: + # The time might be forced by the campaign for the first turn. + if forced_time is not None: + _start_time = datetime.datetime.combine(day, forced_time) + else: + _start_time = cls.generate_start_time( + theater, day, time_of_day, settings.night_disabled + ) + + return cls( + time_of_day=time_of_day, + start_time=_start_time, + weather=cls.generate_weather(theater.seasonal_conditions, day, time_of_day), + ) + + @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 = DaytimeMap( + dawn=(datetime.time(hour=8), datetime.time(hour=9)), + day=(datetime.time(hour=10), datetime.time(hour=12)), + dusk=(datetime.time(hour=12), datetime.time(hour=14)), + night=(datetime.time(hour=14), datetime.time(hour=17)), + ).range_of(time_of_day) + else: + time_range = theater.daytime_map.range_of(time_of_day) + + # Starting missions on the hour is a nice gameplay property, so keep the random + # time constrained to that. DaytimeMap enforces that we have only whole hour + # ranges for now, so we don't need to worry about accidentally changing the time + # of day by truncating sub-hours. + time = datetime.time( + hour=random.randint(time_range[0].hour, time_range[1].hour) + ) + return datetime.datetime.combine(day, time) + + @classmethod + def generate_weather( + cls, + seasonal_conditions: SeasonalConditions, + day: datetime.date, + time_of_day: TimeOfDay, + ) -> Weather: + season = determine_season(day) + logging.debug("Weather: Season {}".format(season)) + weather_chances = seasonal_conditions.weather_type_chances[season] + chances = { + Thunderstorm: weather_chances.thunderstorm, + Raining: weather_chances.raining, + Cloudy: weather_chances.cloudy, + ClearSkies: weather_chances.clear_skies, + } + logging.debug("Weather: Chances {}".format(weather_chances)) + weather_type = random.choices( + list(chances.keys()), weights=list(chances.values()) + )[0] + logging.debug("Weather: Type {}".format(weather_type)) + return weather_type(seasonal_conditions, day, time_of_day) diff --git a/game/weather/fog.py b/game/weather/fog.py new file mode 100644 index 00000000..f998429d --- /dev/null +++ b/game/weather/fog.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from game.utils import Distance + + +@dataclass(frozen=True) +class Fog: + visibility: Distance + thickness: int diff --git a/game/weather.py b/game/weather/weather.py similarity index 71% rename from game/weather.py rename to game/weather/weather.py index a023f503..9b147b33 100644 --- a/game/weather.py +++ b/game/weather/weather.py @@ -4,17 +4,13 @@ import datetime import logging import math import random -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Optional, TYPE_CHECKING -from dcs.cloud_presets import Clouds as PydcsClouds -from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind +from dcs.weather import Weather as PydcsWeather, Wind -from game.theater.daytimemap import DaytimeMap -from game.theater.seasonalconditions import determine_season from game.timeofday import TimeOfDay from game.utils import ( - Distance, Heading, Pressure, inches_hg, @@ -23,69 +19,21 @@ from game.utils import ( meters, Speed, ) +from game.weather.atmosphericconditions import AtmosphericConditions +from game.weather.clouds import Clouds +from game.weather.fog import Fog +from game.weather.wind import WindConditions if TYPE_CHECKING: - from game.settings import Settings - from game.theater import ConflictTheater from game.theater.seasonalconditions import SeasonalConditions -@dataclass(frozen=True) -class AtmosphericConditions: - #: Pressure at sea level. - qnh: Pressure - - #: Temperature at sea level in Celcius. - temperature_celsius: float - - #: Turbulence per 10 cm. - turbulence_per_10cm: float - - -@dataclass(frozen=True) -class WindConditions: - at_0m: Wind - at_2000m: Wind - at_8000m: Wind - - @dataclass(frozen=True) class WeibullWindSpeedParameters: shape: float scale: Speed -@dataclass(frozen=True) -class Clouds: - base: int - density: int - thickness: int - precipitation: PydcsWeather.Preceptions - preset: Optional[CloudPreset] = field(default=None) - - @classmethod - def random_preset(cls, rain: bool) -> Clouds: - clouds = (p.value for p in PydcsClouds) - if rain: - presets = [p for p in clouds if "Rain" in p.name] - else: - presets = [p for p in clouds if "Rain" not in p.name] - preset = random.choice(presets) - return Clouds( - base=random.randint(preset.min_base, preset.max_base), - density=0, - thickness=0, - precipitation=PydcsWeather.Preceptions.None_, - preset=preset, - ) - - -@dataclass(frozen=True) -class Fog: - visibility: Distance - thickness: int - - class Weather: def __init__( self, @@ -411,84 +359,3 @@ class Thunderstorm(Weather): WeibullWindSpeedParameters(6.2, knots(20)), WeibullWindSpeedParameters(6.4, knots(20)), ) - - -@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, - forced_time: datetime.time | None = None, - ) -> Conditions: - # The time might be forced by the campaign for the first turn. - if forced_time is not None: - _start_time = datetime.datetime.combine(day, forced_time) - else: - _start_time = cls.generate_start_time( - theater, day, time_of_day, settings.night_disabled - ) - - return cls( - time_of_day=time_of_day, - start_time=_start_time, - weather=cls.generate_weather(theater.seasonal_conditions, day, time_of_day), - ) - - @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 = DaytimeMap( - dawn=(datetime.time(hour=8), datetime.time(hour=9)), - day=(datetime.time(hour=10), datetime.time(hour=12)), - dusk=(datetime.time(hour=12), datetime.time(hour=14)), - night=(datetime.time(hour=14), datetime.time(hour=17)), - ).range_of(time_of_day) - else: - time_range = theater.daytime_map.range_of(time_of_day) - - # Starting missions on the hour is a nice gameplay property, so keep the random - # time constrained to that. DaytimeMap enforces that we have only whole hour - # ranges for now, so we don't need to worry about accidentally changing the time - # of day by truncating sub-hours. - time = datetime.time( - hour=random.randint(time_range[0].hour, time_range[1].hour) - ) - return datetime.datetime.combine(day, time) - - @classmethod - def generate_weather( - cls, - seasonal_conditions: SeasonalConditions, - day: datetime.date, - time_of_day: TimeOfDay, - ) -> Weather: - season = determine_season(day) - logging.debug("Weather: Season {}".format(season)) - weather_chances = seasonal_conditions.weather_type_chances[season] - chances = { - Thunderstorm: weather_chances.thunderstorm, - Raining: weather_chances.raining, - Cloudy: weather_chances.cloudy, - ClearSkies: weather_chances.clear_skies, - } - logging.debug("Weather: Chances {}".format(weather_chances)) - weather_type = random.choices( - list(chances.keys()), weights=list(chances.values()) - )[0] - logging.debug("Weather: Type {}".format(weather_type)) - return weather_type(seasonal_conditions, day, time_of_day) diff --git a/game/weather/wind.py b/game/weather/wind.py new file mode 100644 index 00000000..dd762b26 --- /dev/null +++ b/game/weather/wind.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from dcs.weather import Wind + + +@dataclass(frozen=True) +class WindConditions: + at_0m: Wind + at_2000m: Wind + at_8000m: Wind diff --git a/qt_ui/widgets/QConditionsWidget.py b/qt_ui/widgets/QConditionsWidget.py index ef852c94..37c26d65 100644 --- a/qt_ui/widgets/QConditionsWidget.py +++ b/qt_ui/widgets/QConditionsWidget.py @@ -15,7 +15,7 @@ import qt_ui.uiconstants as CONST from game.sim.gameupdateevents import GameUpdateEvents from game.timeofday import TimeOfDay from game.utils import mps -from game.weather import Conditions +from game.weather.conditions import Conditions from qt_ui.simcontroller import SimController