diff --git a/game/theater/conflicttheater.py b/game/theater/conflicttheater.py index e0f4d69a..43cd2c9d 100644 --- a/game/theater/conflicttheater.py +++ b/game/theater/conflicttheater.py @@ -497,6 +497,17 @@ class ReferencePoint: image_coordinates: Point +@dataclass(frozen=True) +class SeasonalConditions: + # Units are inHg and degrees Celsius + # Future improvement: add clouds/precipitation + summer_avg_pressure: float + winter_avg_pressure: float + summer_avg_temperature: float + winter_avg_temperature: float + temperature_day_night_difference: float + + class ConflictTheater: terrain: Terrain @@ -719,6 +730,10 @@ class ConflictTheater: MizCampaignLoader(directory / miz, t).populate_theater() return t + @property + def seasonal_conditions(self) -> SeasonalConditions: + raise NotImplementedError + @property def projection_parameters(self) -> TransverseMercator: raise NotImplementedError @@ -748,6 +763,16 @@ class CaucasusTheater(ConflictTheater): "night": (0, 5), } + @property + def seasonal_conditions(self) -> SeasonalConditions: + return SeasonalConditions( + summer_avg_pressure=30.02, # TODO: More science + winter_avg_pressure=29.72, # TODO: More science + summer_avg_temperature=22.5, + winter_avg_temperature=3.0, + temperature_day_night_difference=6.0, + ) + @property def projection_parameters(self) -> TransverseMercator: from .caucasus import PARAMETERS @@ -770,6 +795,16 @@ class PersianGulfTheater(ConflictTheater): "night": (0, 5), } + @property + def seasonal_conditions(self) -> SeasonalConditions: + return SeasonalConditions( + summer_avg_pressure=29.98, # TODO: More science + winter_avg_pressure=29.80, # TODO: More science + summer_avg_temperature=32.5, + winter_avg_temperature=15.0, + temperature_day_night_difference=2.0, + ) + @property def projection_parameters(self) -> TransverseMercator: from .persiangulf import PARAMETERS @@ -792,6 +827,16 @@ class NevadaTheater(ConflictTheater): "night": (0, 5), } + @property + def seasonal_conditions(self) -> SeasonalConditions: + return SeasonalConditions( + summer_avg_pressure=30.02, # TODO: More science + winter_avg_pressure=29.72, # TODO: More science + summer_avg_temperature=31.5, + winter_avg_temperature=5.0, + temperature_day_night_difference=6.0, + ) + @property def projection_parameters(self) -> TransverseMercator: from .nevada import PARAMETERS @@ -814,6 +859,16 @@ class NormandyTheater(ConflictTheater): "night": (0, 5), } + @property + def seasonal_conditions(self) -> SeasonalConditions: + return SeasonalConditions( + summer_avg_pressure=30.02, # TODO: More science + winter_avg_pressure=29.72, # TODO: More science + summer_avg_temperature=20.0, + winter_avg_temperature=0.0, + temperature_day_night_difference=5.0, + ) + @property def projection_parameters(self) -> TransverseMercator: from .normandy import PARAMETERS @@ -836,6 +891,16 @@ class TheChannelTheater(ConflictTheater): "night": (0, 5), } + @property + def seasonal_conditions(self) -> SeasonalConditions: + return SeasonalConditions( + summer_avg_pressure=30.02, # TODO: More science + winter_avg_pressure=29.72, # TODO: More science + summer_avg_temperature=20.0, + winter_avg_temperature=0.0, + temperature_day_night_difference=5.0, + ) + @property def projection_parameters(self) -> TransverseMercator: from .thechannel import PARAMETERS @@ -858,6 +923,16 @@ class SyriaTheater(ConflictTheater): "night": (0, 5), } + @property + def seasonal_conditions(self) -> SeasonalConditions: + return SeasonalConditions( + summer_avg_pressure=29.98, # TODO: More science + winter_avg_pressure=29.86, # TODO: More science + summer_avg_temperature=28.5, + winter_avg_temperature=10.0, + temperature_day_night_difference=8.0, + ) + @property def projection_parameters(self) -> TransverseMercator: from .syria import PARAMETERS @@ -877,6 +952,16 @@ class MarianaIslandsTheater(ConflictTheater): "night": (0, 5), } + @property + def seasonal_conditions(self) -> SeasonalConditions: + return SeasonalConditions( + summer_avg_pressure=30.02, # TODO: More science + winter_avg_pressure=29.82, # TODO: More science + summer_avg_temperature=28.0, + winter_avg_temperature=27.0, + temperature_day_night_difference=1.0, + ) + @property def projection_parameters(self) -> TransverseMercator: from .marianaislands import PARAMETERS diff --git a/game/utils.py b/game/utils.py index 2370c56f..291e098b 100644 --- a/game/utils.py +++ b/game/utils.py @@ -189,3 +189,15 @@ def pairwise(iterable: Iterable[Any]) -> Iterable[tuple[Any, Any]]: a, b = itertools.tee(iterable) next(b, None) return zip(a, b) + + +def interpolate(value1: float, value2: float, factor: float, clamp: bool) -> float: + """Inerpolate between two values, factor 0-1""" + interpolated = value1 + (value2 - value1) * factor + + if clamp: + bigger_value = max(value1, value2) + smaller_value = min(value1, value2) + return min(bigger_value, max(smaller_value, interpolated)) + else: + return interpolated diff --git a/game/weather.py b/game/weather.py index fae1d5a0..ae31fa7f 100644 --- a/game/weather.py +++ b/game/weather.py @@ -11,10 +11,11 @@ from dcs.cloud_presets import Clouds as PydcsClouds from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind from game.settings import Settings -from game.utils import Distance, meters +from game.utils import Distance, meters, interpolate if TYPE_CHECKING: from game.theater import ConflictTheater + from game.theater.conflicttheater import SeasonalConditions class TimeOfDay(Enum): @@ -71,15 +72,56 @@ class Fog: class Weather: - def __init__(self) -> None: + def __init__( + self, + seasonal_conditions: SeasonalConditions, + day: datetime.date, + time_of_day: TimeOfDay, + ) -> None: # Future improvement: Use theater, day and time of day # to get a more realistic conditions - self.atmospheric = self.generate_atmospheric() + self.atmospheric = self.generate_atmospheric( + seasonal_conditions, day, time_of_day + ) self.clouds = self.generate_clouds() self.fog = self.generate_fog() self.wind = self.generate_wind() - def generate_atmospheric(self) -> AtmosphericConditions: + def generate_atmospheric( + self, + seasonal_conditions: SeasonalConditions, + day: datetime.date, + time_of_day: TimeOfDay, + ) -> AtmosphericConditions: + pressure = self.interpolate_summer_winter( + seasonal_conditions.summer_avg_pressure, + seasonal_conditions.winter_avg_pressure, + day, + ) + temperature = self.interpolate_summer_winter( + seasonal_conditions.summer_avg_temperature, + seasonal_conditions.winter_avg_temperature, + day, + ) + + if time_of_day == TimeOfDay.Day: + temperature += seasonal_conditions.temperature_day_night_difference / 2 + if time_of_day == TimeOfDay.Night: + temperature -= seasonal_conditions.temperature_day_night_difference / 2 + pressure += self.pressure_adjustment + temperature += self.temperature_adjustment + conditions = AtmosphericConditions( + qnh_inches_mercury=self.random_pressure(pressure), + temperature_celsius=self.random_temperature(temperature), + ) + return conditions + + @property + def pressure_adjustment(self) -> float: + raise NotImplementedError + + @property + def temperature_adjustment(self) -> float: raise NotImplementedError def generate_clouds(self) -> Optional[Clouds]: @@ -126,7 +168,7 @@ class Weather: SAFE_MIN = 28.4 SAFE_MAX = 30.9 # Use normalvariate to get normal distribution, more realistic than uniform - pressure = random.normalvariate(average_pressure, 0.2) + pressure = random.normalvariate(average_pressure, 0.1) return max(SAFE_MIN, min(SAFE_MAX, pressure)) @staticmethod @@ -136,17 +178,29 @@ class Weather: SAFE_MIN = -12 SAFE_MAX = 49 # Use normalvariate to get normal distribution, more realistic than uniform - temperature = random.normalvariate(average_temperature, 4) + temperature = random.normalvariate(average_temperature, 2) temperature = round(temperature) return max(SAFE_MIN, min(SAFE_MAX, temperature)) + @staticmethod + def interpolate_summer_winter( + summer_value: float, winter_value: float, day: datetime.date + ) -> float: + day_of_year = day.timetuple().tm_yday + day_of_year_peak_summer = 183 + distance_from_peak_summer = abs(-day_of_year_peak_summer + day_of_year) + winter_factor = distance_from_peak_summer / day_of_year_peak_summer + return interpolate(summer_value, winter_value, winter_factor, clamp=True) + class ClearSkies(Weather): - def generate_atmospheric(self) -> AtmosphericConditions: - return AtmosphericConditions( - qnh_inches_mercury=self.random_pressure(29.96), - temperature_celsius=self.random_temperature(22), - ) + @property + def pressure_adjustment(self) -> float: + return 0.22 + + @property + def temperature_adjustment(self) -> float: + return 3.0 def generate_clouds(self) -> Optional[Clouds]: return None @@ -159,11 +213,13 @@ class ClearSkies(Weather): class Cloudy(Weather): - def generate_atmospheric(self) -> AtmosphericConditions: - return AtmosphericConditions( - qnh_inches_mercury=self.random_pressure(29.90), - temperature_celsius=self.random_temperature(20), - ) + @property + def pressure_adjustment(self) -> float: + return 0.0 + + @property + def temperature_adjustment(self) -> float: + return 0.0 def generate_clouds(self) -> Optional[Clouds]: return Clouds.random_preset(rain=False) @@ -177,11 +233,13 @@ class Cloudy(Weather): class Raining(Weather): - def generate_atmospheric(self) -> AtmosphericConditions: - return AtmosphericConditions( - qnh_inches_mercury=self.random_pressure(29.70), - temperature_celsius=self.random_temperature(16), - ) + @property + def pressure_adjustment(self) -> float: + return -0.22 + + @property + def temperature_adjustment(self) -> float: + return -3.0 def generate_clouds(self) -> Optional[Clouds]: return Clouds.random_preset(rain=True) @@ -195,11 +253,13 @@ class Raining(Weather): class Thunderstorm(Weather): - def generate_atmospheric(self) -> AtmosphericConditions: - return AtmosphericConditions( - qnh_inches_mercury=self.random_pressure(29.60), - temperature_celsius=self.random_temperature(15), - ) + @property + def pressure_adjustment(self) -> float: + return 0.1 + + @property + def temperature_adjustment(self) -> float: + return -3.0 def generate_clouds(self) -> Optional[Clouds]: return Clouds( @@ -233,7 +293,7 @@ class Conditions: return cls( time_of_day=time_of_day, start_time=_start_time, - weather=cls.generate_weather(), + weather=cls.generate_weather(theater.seasonal_conditions, day, time_of_day), ) @classmethod @@ -259,7 +319,13 @@ class Conditions: return datetime.datetime.combine(day, time) @classmethod - def generate_weather(cls) -> Weather: + def generate_weather( + cls, + seasonal_conditions: SeasonalConditions, + day: datetime.date, + time_of_day: TimeOfDay, + ) -> Weather: + # Future improvement: use seasonal weights for theaters chances = { Thunderstorm: 1, Raining: 20, @@ -269,4 +335,4 @@ class Conditions: weather_type = random.choices( list(chances.keys()), weights=list(chances.values()) )[0] - return weather_type() + return weather_type(seasonal_conditions, day, time_of_day)