From 9fa04702aff79b38984bb0cba80c5c6444fa1f3d Mon Sep 17 00:00:00 2001 From: SnappyComebacks <74509817+SnappyComebacks@users.noreply.github.com> Date: Fri, 18 Nov 2022 00:58:15 -0700 Subject: [PATCH] Wind & Turbulence updates from Liberation Tune turbulence values. Modify the range of values used to choose a wind speed. Wind speed at high elevation IRL can range from 20 to 160 knots around the globe. You may see wind speed generated here up to 100+ knots, but generally around 40 or so. IRL wind speed appears to depend on the latitude of the sun, not in this implementation. Note increased wind speeds in the changelog. Limit wind speed to 97 knots. Made minor adjustments to wind speed calculation. Calculate turbulance. Turbulance is based off time of day, and day of year. Each theatre may adjust their turbulance parameters. Spell turbulence correctly. --- changelog.md | 2 + game/missiongenerator/environmentgenerator.py | 1 + game/missiongenerator/kneeboard.py | 3 + game/theater/seasonalconditions.py | 5 + game/theater/theaterloader.py | 38 +++++ game/weather.py | 137 +++++++++++++++++- resources/theaters/caucasus/info.yaml | 5 + resources/theaters/falklands/info.yaml | 5 + resources/theaters/marianaislands/info.yaml | 5 + resources/theaters/nevada/info.yaml | 5 + resources/theaters/normandy/info.yaml | 5 + resources/theaters/persian gulf/info.yaml | 5 + resources/theaters/syria/info.yaml | 5 + resources/theaters/the channel/info.yaml | 5 + 14 files changed, 220 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 3cde6a51..b809738d 100644 --- a/changelog.md +++ b/changelog.md @@ -41,6 +41,8 @@ Saves from 5.x are not compatible with 6.0. * **[Mission Generation]** Added performance option to not cull IADS when culling would affect how mission is played at target area. * **[Mission Generation]** Reworked the ground object generation which now uses a new layout system * **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC. +* **[Mission Generation]** Adjusted wind speeds. Wind speeds at high altitude are generally higher now. +* **[Mission Generation]** Added turbulence. Higher in Summer and Winter, also higher at day time than at night time. * **[Factions]** Updated the Faction file structure. Older custom faction files will not work correctly and have to be updated to the new structure. * **[Flight Planning]** Added preset formations for different flight types at hold, join, ingress, and split waypoints. Air to Air flights will tend toward line-abreast and spread-four formations. Air to ground flights will tend towards trail formation. * **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan. diff --git a/game/missiongenerator/environmentgenerator.py b/game/missiongenerator/environmentgenerator.py index dec34969..24b1dc8b 100644 --- a/game/missiongenerator/environmentgenerator.py +++ b/game/missiongenerator/environmentgenerator.py @@ -17,6 +17,7 @@ class EnvironmentGenerator: def set_atmospheric(self, atmospheric: AtmosphericConditions) -> None: self.mission.weather.qnh = atmospheric.qnh.mm_hg self.mission.weather.season_temperature = atmospheric.temperature_celsius + self.mission.weather.turbulence_at_ground = int(atmospheric.turbulence_per_10cm) def set_clouds(self, clouds: Optional[Clouds]) -> None: if clouds is None: diff --git a/game/missiongenerator/kneeboard.py b/game/missiongenerator/kneeboard.py index a1bc0c6f..382a5298 100644 --- a/game/missiongenerator/kneeboard.py +++ b/game/missiongenerator/kneeboard.py @@ -400,6 +400,9 @@ class BriefingPage(KneeboardPage): f"Temperature: {round(self.weather.atmospheric.temperature_celsius)} °C at sea level" ) writer.text(f"QNH: {qnh_in_hg} inHg / {qnh_mm_hg} mmHg / {qnh_hpa} hPa") + writer.text( + f"Turbulence: {round(self.weather.atmospheric.turbulence_per_10cm)} per 10cm at ground level." + ) fl = self.flight diff --git a/game/theater/seasonalconditions.py b/game/theater/seasonalconditions.py index 2280d15e..fa91764f 100644 --- a/game/theater/seasonalconditions.py +++ b/game/theater/seasonalconditions.py @@ -45,4 +45,9 @@ class SeasonalConditions: winter_avg_temperature: float temperature_day_night_difference: float + high_avg_yearly_turbulence_per_10cm: float + low_avg_yearly_turbulence_per_10cm: float + solar_noon_turbulence_per_10cm: float + midnight_turbulence_per_10cm: float + weather_type_chances: dict[Season, WeatherTypeChances] diff --git a/game/theater/theaterloader.py b/game/theater/theaterloader.py index 3dad8c19..7ce1e626 100644 --- a/game/theater/theaterloader.py +++ b/game/theater/theaterloader.py @@ -57,6 +57,23 @@ class SeasonData: ) +@dataclass(frozen=True) +class TurbulenceData: + high_avg_yearly_turbulence_per_10cm: float | None + low_avg_yearly_turbulence_per_10cm: float | None + solar_noon_turbulence_per_10cm: float | None + midnight_turbulence_per_10cm: float | None + + @staticmethod + def from_yaml(data: dict[str, Any]) -> TurbulenceData: + return TurbulenceData( + data.get("high_avg_yearly_turbulence_per_10cm"), + data.get("low_avg_yearly_turbulence_per_10cm"), + data.get("solar_noon_turbulence_per_10cm"), + data.get("midnight_turbulence_per_10cm"), + ) + + class TheaterLoader: THEATER_RESOURCE_DIR = Path("resources/theaters") @@ -113,6 +130,7 @@ class TheaterLoader: spring = SeasonData.from_yaml(climate_data["seasons"]["spring"]) summer = SeasonData.from_yaml(climate_data["seasons"]["summer"]) fall = SeasonData.from_yaml(climate_data["seasons"]["fall"]) + turbulence = TurbulenceData.from_yaml(climate_data["turbulence"]) if summer.average_pressure is None: raise RuntimeError( f"{self.descriptor_path} does not define a summer average pressure" @@ -129,12 +147,32 @@ class TheaterLoader: raise RuntimeError( f"{self.descriptor_path} does not define a winter average temperature" ) + if turbulence.high_avg_yearly_turbulence_per_10cm is None: + raise RuntimeError( + f"{self.descriptor_path} does not define a yearly average high turbulence" + ) + if turbulence.low_avg_yearly_turbulence_per_10cm is None: + raise RuntimeError( + f"{self.descriptor_path} does not define a yearly average low turbulence" + ) + if turbulence.solar_noon_turbulence_per_10cm is None: + raise RuntimeError( + f"{self.descriptor_path} does not define a solar noon turbulence" + ) + if turbulence.midnight_turbulence_per_10cm is None: + raise RuntimeError( + f"{self.descriptor_path} does not define a midnight turbulence" + ) return SeasonalConditions( summer.average_pressure, winter.average_pressure, summer.average_temperature, winter.average_temperature, climate_data["day_night_temperature_difference"], + turbulence.high_avg_yearly_turbulence_per_10cm, + turbulence.low_avg_yearly_turbulence_per_10cm, + turbulence.solar_noon_turbulence_per_10cm, + turbulence.midnight_turbulence_per_10cm, { Season.Winter: winter.weather, Season.Spring: spring.weather, diff --git a/game/weather.py b/game/weather.py index 3aa73156..22deb6c7 100644 --- a/game/weather.py +++ b/game/weather.py @@ -2,6 +2,7 @@ from __future__ import annotations import datetime import logging +import math import random from dataclasses import dataclass, field from typing import Optional, TYPE_CHECKING @@ -11,7 +12,15 @@ from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind from game.theater.seasonalconditions import determine_season from game.timeofday import TimeOfDay -from game.utils import Distance, Heading, Pressure, inches_hg, interpolate, meters +from game.utils import ( + Distance, + Heading, + Pressure, + inches_hg, + interpolate, + knots, + meters, +) if TYPE_CHECKING: from game.settings import Settings @@ -27,6 +36,9 @@ class AtmosphericConditions: #: Temperature at sea level in Celcius. temperature_celsius: float + #: Turbulence per 10 cm. + turbulence_per_10cm: float + @dataclass(frozen=True) class WindConditions: @@ -99,18 +111,38 @@ class Weather: day, ) + seasonal_turbulence = self.interpolate_seasonal_turbulence( + seasonal_conditions.high_avg_yearly_turbulence_per_10cm, + seasonal_conditions.low_avg_yearly_turbulence_per_10cm, + day, + ) + + day_turbulence = seasonal_conditions.solar_noon_turbulence_per_10cm + night_turbulence = seasonal_conditions.midnight_turbulence_per_10cm + time_of_day_turbulence = self.interpolate_solar_activity( + time_of_day, day_turbulence, night_turbulence + ) + + random_turbulence = random.normalvariate(mu=0, sigma=0.5) + + turbulence = abs( + seasonal_turbulence + time_of_day_turbulence + random_turbulence + ) + 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 + turbulence += self.turbulence_adjustment logging.debug( "Weather: Before random: temp {} press {}".format(temperature, pressure) ) conditions = AtmosphericConditions( qnh=self.random_pressure(pressure), temperature_celsius=self.random_temperature(temperature), + turbulence_per_10cm=turbulence, ) logging.debug( "Weather: After random: temp {} press {}".format( @@ -127,6 +159,10 @@ class Weather: def temperature_adjustment(self) -> float: raise NotImplementedError + @property + def turbulence_adjustment(self) -> float: + raise NotImplementedError + def generate_clouds(self) -> Optional[Clouds]: raise NotImplementedError @@ -147,15 +183,52 @@ class Weather: wind_direction_2000m = wind_direction + Heading.random(-90, 90) wind_direction_8000m = wind_direction + Heading.random(-90, 90) at_0m_factor = 1 - at_2000m_factor = 2 - at_8000m_factor = 3 + at_2000m_factor = 3 + random.choice([0, 0, 0, 0, 0, 1, 1]) + + high_alt_variation = random.choice( + [ + -3, + -3, + -2, + -2, + -2, + -2, + -2, + -2, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 0, + 0, + 0, + 1, + 1, + 2, + 3, + ] + ) + at_8000m_factor = at_2000m_factor + 5 + high_alt_variation + base_wind = random.randint(minimum, maximum) + # DCS is limited to 97 knots wind speed. + max_supported_wind_speed = knots(97).meters_per_second + return WindConditions( # Always some wind to make the smoke move a bit. at_0m=Wind(wind_direction.degrees, max(1, base_wind * at_0m_factor)), - at_2000m=Wind(wind_direction_2000m.degrees, base_wind * at_2000m_factor), - at_8000m=Wind(wind_direction_8000m.degrees, base_wind * at_8000m_factor), + at_2000m=Wind( + wind_direction_2000m.degrees, + min(max_supported_wind_speed, base_wind * at_2000m_factor), + ), + at_8000m=Wind( + wind_direction_8000m.degrees, + min(max_supported_wind_speed, base_wind * at_8000m_factor), + ), ) @staticmethod @@ -198,6 +271,42 @@ class Weather: winter_factor = distance_from_peak_summer / day_of_year_peak_summer return interpolate(summer_value, winter_value, winter_factor, clamp=True) + @staticmethod + def interpolate_seasonal_turbulence( + high_value: float, low_value: float, day: datetime.date + ) -> float: + day_of_year = day.timetuple().tm_yday + day_of_year_peak_summer = 183 + distance_from_peak_summer = -day_of_year_peak_summer + day_of_year + + amplitude = 0.5 * (high_value - low_value) + offset = amplitude + low_value + + # A high peak in summer and winter, between high_value and low_value. + return ( + amplitude * math.cos(4 * math.pi * distance_from_peak_summer / 365.25) + + offset + ) + + @staticmethod + def interpolate_solar_activity( + time_of_day: TimeOfDay, high: float, low: float + ) -> float: + + scale: float = 0 + + match time_of_day: + case TimeOfDay.Dawn: + scale = 0.4 + case TimeOfDay.Day: + scale = 1 + case TimeOfDay.Dusk: + scale = 0.6 + case TimeOfDay.Night: + scale = 0 + + return interpolate(value1=low, value2=high, factor=scale, clamp=True) + class ClearSkies(Weather): @property @@ -208,6 +317,10 @@ class ClearSkies(Weather): def temperature_adjustment(self) -> float: return 3.0 + @property + def turbulence_adjustment(self) -> float: + return 0.0 + def generate_clouds(self) -> Optional[Clouds]: return None @@ -227,6 +340,10 @@ class Cloudy(Weather): def temperature_adjustment(self) -> float: return 0.0 + @property + def turbulence_adjustment(self) -> float: + return 0.75 + def generate_clouds(self) -> Optional[Clouds]: return Clouds.random_preset(rain=False) @@ -247,6 +364,10 @@ class Raining(Weather): def temperature_adjustment(self) -> float: return -3.0 + @property + def turbulence_adjustment(self) -> float: + return 1.5 + def generate_clouds(self) -> Optional[Clouds]: return Clouds.random_preset(rain=True) @@ -255,7 +376,7 @@ class Raining(Weather): return None def generate_wind(self) -> WindConditions: - return self.random_wind(1, 6) + return self.random_wind(2, 6) class Thunderstorm(Weather): @@ -267,6 +388,10 @@ class Thunderstorm(Weather): def temperature_adjustment(self) -> float: return -3.0 + @property + def turbulence_adjustment(self) -> float: + return 3.0 + def generate_clouds(self) -> Optional[Clouds]: return Clouds( base=self.random_cloud_base(), diff --git a/resources/theaters/caucasus/info.yaml b/resources/theaters/caucasus/info.yaml index 54b232d8..aac34370 100644 --- a/resources/theaters/caucasus/info.yaml +++ b/resources/theaters/caucasus/info.yaml @@ -37,3 +37,8 @@ climate: raining: 30 cloudy: 50 clear: 20 + turbulence: + high_avg_yearly_turbulence_per_10cm: 9 + low_avg_yearly_turbulence_per_10cm: 3.5 + solar_noon_turbulence_per_10cm: 3.5 + midnight_turbulence_per_10cm: -3 \ No newline at end of file diff --git a/resources/theaters/falklands/info.yaml b/resources/theaters/falklands/info.yaml index 1d4be26d..3f9e42c0 100644 --- a/resources/theaters/falklands/info.yaml +++ b/resources/theaters/falklands/info.yaml @@ -46,3 +46,8 @@ climate: raining: 30 cloudy: 45 clear: 25 + turbulence: + high_avg_yearly_turbulence_per_10cm: 8 + low_avg_yearly_turbulence_per_10cm: 4.5 + solar_noon_turbulence_per_10cm: 3 + midnight_turbulence_per_10cm: -2 \ No newline at end of file diff --git a/resources/theaters/marianaislands/info.yaml b/resources/theaters/marianaislands/info.yaml index 6cb3340c..e8a0bcf8 100644 --- a/resources/theaters/marianaislands/info.yaml +++ b/resources/theaters/marianaislands/info.yaml @@ -38,3 +38,8 @@ climate: raining: 45 cloudy: 30 clear: 20 + turbulence: + high_avg_yearly_turbulence_per_10cm: 6.5 + low_avg_yearly_turbulence_per_10cm: 4.5 + solar_noon_turbulence_per_10cm: 2 + midnight_turbulence_per_10cm: -1 \ No newline at end of file diff --git a/resources/theaters/nevada/info.yaml b/resources/theaters/nevada/info.yaml index 401071fc..d98f1064 100644 --- a/resources/theaters/nevada/info.yaml +++ b/resources/theaters/nevada/info.yaml @@ -37,3 +37,8 @@ climate: raining: 10 cloudy: 45 clear: 45 + turbulence: + high_avg_yearly_turbulence_per_10cm: 17 + low_avg_yearly_turbulence_per_10cm: 3.5 + solar_noon_turbulence_per_10cm: 3.5 + midnight_turbulence_per_10cm: -3 \ No newline at end of file diff --git a/resources/theaters/normandy/info.yaml b/resources/theaters/normandy/info.yaml index 109039cf..320c6f08 100644 --- a/resources/theaters/normandy/info.yaml +++ b/resources/theaters/normandy/info.yaml @@ -37,3 +37,8 @@ climate: raining: 30 cloudy: 50 clear: 20 + turbulence: + high_avg_yearly_turbulence_per_10cm: 9 + low_avg_yearly_turbulence_per_10cm: 3.5 + solar_noon_turbulence_per_10cm: 3.5 + midnight_turbulence_per_10cm: -3 \ No newline at end of file diff --git a/resources/theaters/persian gulf/info.yaml b/resources/theaters/persian gulf/info.yaml index 0f9d01ce..c4529c39 100644 --- a/resources/theaters/persian gulf/info.yaml +++ b/resources/theaters/persian gulf/info.yaml @@ -38,3 +38,8 @@ climate: raining: 2 cloudy: 28 clear: 70 + turbulence: + high_avg_yearly_turbulence_per_10cm: 9 + low_avg_yearly_turbulence_per_10cm: 4.5 + solar_noon_turbulence_per_10cm: 5.5 + midnight_turbulence_per_10cm: -2 \ No newline at end of file diff --git a/resources/theaters/syria/info.yaml b/resources/theaters/syria/info.yaml index 2b3eec9e..cb763e89 100644 --- a/resources/theaters/syria/info.yaml +++ b/resources/theaters/syria/info.yaml @@ -37,3 +37,8 @@ climate: raining: 15 cloudy: 35 clear: 50 + turbulence: + high_avg_yearly_turbulence_per_10cm: 9 + low_avg_yearly_turbulence_per_10cm: 3.5 + solar_noon_turbulence_per_10cm: 3.5 + midnight_turbulence_per_10cm: -3 \ No newline at end of file diff --git a/resources/theaters/the channel/info.yaml b/resources/theaters/the channel/info.yaml index f6ed1f7b..b10e34f5 100644 --- a/resources/theaters/the channel/info.yaml +++ b/resources/theaters/the channel/info.yaml @@ -38,3 +38,8 @@ climate: raining: 30 cloudy: 50 clear: 20 + turbulence: + high_avg_yearly_turbulence_per_10cm: 9 + low_avg_yearly_turbulence_per_10cm: 3.5 + solar_noon_turbulence_per_10cm: 3.5 + midnight_turbulence_per_10cm: -3 \ No newline at end of file