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