mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
Make wind speed moddable.
These should probably be overridable per theater and per season, but even with that we'll want some defaults. https://github.com/dcs-liberation/dcs_liberation/issues/2862
This commit is contained in:
@@ -79,7 +79,9 @@ class Conditions:
|
||||
season = determine_season(day)
|
||||
logging.debug("Weather: Season {}".format(season))
|
||||
weather_chances = seasonal_conditions.weather_type_chances[season]
|
||||
chances = {
|
||||
chances: dict[
|
||||
type[ClearSkies] | type[Cloudy] | type[Raining] | type[Thunderstorm], float
|
||||
] = {
|
||||
Thunderstorm: weather_chances.thunderstorm,
|
||||
Raining: weather_chances.raining,
|
||||
Cloudy: weather_chances.cloudy,
|
||||
|
||||
@@ -4,25 +4,23 @@ import datetime
|
||||
import logging
|
||||
import math
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from dcs.weather import Weather as PydcsWeather, Wind
|
||||
from dcs.weather import Weather as PydcsWeather
|
||||
|
||||
from game.timeofday import TimeOfDay
|
||||
from game.utils import (
|
||||
Heading,
|
||||
Pressure,
|
||||
inches_hg,
|
||||
interpolate,
|
||||
knots,
|
||||
meters,
|
||||
Speed,
|
||||
)
|
||||
from game.weather.atmosphericconditions import AtmosphericConditions
|
||||
from game.weather.clouds import Clouds
|
||||
from game.weather.fog import Fog
|
||||
from game.weather.weatherarchetype import WeatherArchetype, WeatherArchetypes
|
||||
from game.weather.wind import WindConditions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -35,13 +33,7 @@ class NightMissions(Enum):
|
||||
OnlyNight = "nightmissions_onlynight"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WeibullWindSpeedParameters:
|
||||
shape: float
|
||||
scale: Speed
|
||||
|
||||
|
||||
class Weather:
|
||||
class Weather(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
seasonal_conditions: SeasonalConditions,
|
||||
@@ -114,6 +106,11 @@ class Weather:
|
||||
)
|
||||
return conditions
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def archetype(self) -> WeatherArchetype:
|
||||
...
|
||||
|
||||
@property
|
||||
def pressure_adjustment(self) -> float:
|
||||
raise NotImplementedError
|
||||
@@ -138,47 +135,7 @@ class Weather:
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def random_wind(
|
||||
params_at_msl: WeibullWindSpeedParameters,
|
||||
params_at_2000m: WeibullWindSpeedParameters,
|
||||
params_at_8000m: WeibullWindSpeedParameters,
|
||||
) -> WindConditions:
|
||||
"""Generates random wind."""
|
||||
wind_direction = Heading.random()
|
||||
wind_direction_2000m = wind_direction + Heading.random(-90, 90)
|
||||
wind_direction_8000m = wind_direction + Heading.random(-90, 90)
|
||||
|
||||
# The first parameter is the scale. 63.2% of all results will fall below that
|
||||
# value.
|
||||
# https://www.itl.nist.gov/div898/handbook/eda/section3/weibplot.htm
|
||||
msl = random.weibullvariate(
|
||||
params_at_msl.scale.meters_per_second, params_at_msl.shape
|
||||
)
|
||||
at_2000m = random.weibullvariate(
|
||||
msl + params_at_2000m.scale.meters_per_second, params_at_2000m.shape
|
||||
)
|
||||
at_8000m = random.weibullvariate(
|
||||
at_2000m + params_at_8000m.scale.meters_per_second, params_at_8000m.shape
|
||||
)
|
||||
|
||||
# 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.0, msl)),
|
||||
at_2000m=Wind(
|
||||
wind_direction_2000m.degrees,
|
||||
min(max_supported_wind_speed, at_2000m),
|
||||
),
|
||||
at_8000m=Wind(
|
||||
wind_direction_8000m.degrees,
|
||||
min(max_supported_wind_speed, at_8000m),
|
||||
),
|
||||
)
|
||||
return self.archetype.wind_parameters.speed.random_wind()
|
||||
|
||||
@staticmethod
|
||||
def random_cloud_base() -> int:
|
||||
@@ -257,6 +214,10 @@ class Weather:
|
||||
|
||||
|
||||
class ClearSkies(Weather):
|
||||
@property
|
||||
def archetype(self) -> WeatherArchetype:
|
||||
return WeatherArchetypes.with_id("clear")
|
||||
|
||||
@property
|
||||
def pressure_adjustment(self) -> float:
|
||||
return 0.22
|
||||
@@ -275,15 +236,12 @@ class ClearSkies(Weather):
|
||||
def generate_fog(self) -> Optional[Fog]:
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(
|
||||
WeibullWindSpeedParameters(1.5, knots(5)),
|
||||
WeibullWindSpeedParameters(3.5, knots(20)),
|
||||
WeibullWindSpeedParameters(6.4, knots(20)),
|
||||
)
|
||||
|
||||
|
||||
class Cloudy(Weather):
|
||||
@property
|
||||
def archetype(self) -> WeatherArchetype:
|
||||
return WeatherArchetypes.with_id("cloudy")
|
||||
|
||||
@property
|
||||
def pressure_adjustment(self) -> float:
|
||||
return 0.0
|
||||
@@ -303,15 +261,12 @@ class Cloudy(Weather):
|
||||
# DCS 2.7 says to not use fog with the cloud presets.
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(
|
||||
WeibullWindSpeedParameters(1.6, knots(6.5)),
|
||||
WeibullWindSpeedParameters(3.5, knots(22)),
|
||||
WeibullWindSpeedParameters(6.4, knots(18)),
|
||||
)
|
||||
|
||||
|
||||
class Raining(Weather):
|
||||
@property
|
||||
def archetype(self) -> WeatherArchetype:
|
||||
return WeatherArchetypes.with_id("raining")
|
||||
|
||||
@property
|
||||
def pressure_adjustment(self) -> float:
|
||||
return -0.22
|
||||
@@ -331,15 +286,12 @@ class Raining(Weather):
|
||||
# DCS 2.7 says to not use fog with the cloud presets.
|
||||
return None
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(
|
||||
WeibullWindSpeedParameters(2.6, knots(8)),
|
||||
WeibullWindSpeedParameters(4.2, knots(20)),
|
||||
WeibullWindSpeedParameters(6.4, knots(20)),
|
||||
)
|
||||
|
||||
|
||||
class Thunderstorm(Weather):
|
||||
@property
|
||||
def archetype(self) -> WeatherArchetype:
|
||||
return WeatherArchetypes.with_id("thunderstorm")
|
||||
|
||||
@property
|
||||
def pressure_adjustment(self) -> float:
|
||||
return 0.1
|
||||
@@ -359,10 +311,3 @@ class Thunderstorm(Weather):
|
||||
thickness=self.random_cloud_thickness(),
|
||||
precipitation=PydcsWeather.Preceptions.Thunderstorm,
|
||||
)
|
||||
|
||||
def generate_wind(self) -> WindConditions:
|
||||
return self.random_wind(
|
||||
WeibullWindSpeedParameters(6, knots(20)),
|
||||
WeibullWindSpeedParameters(6.2, knots(20)),
|
||||
WeibullWindSpeedParameters(6.4, knots(20)),
|
||||
)
|
||||
|
||||
58
game/weather/weatherarchetype.py
Normal file
58
game/weather/weatherarchetype.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from .windspeedgenerators import WindSpeedGenerator
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindParameters:
|
||||
speed: WindSpeedGenerator
|
||||
|
||||
@staticmethod
|
||||
def from_data(data: dict[str, Any]) -> WindParameters:
|
||||
return WindParameters(speed=WindSpeedGenerator.from_data(data["speed"]))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WeatherArchetype:
|
||||
id: str
|
||||
wind_parameters: WindParameters
|
||||
|
||||
@staticmethod
|
||||
def from_data(data: dict[str, Any]) -> WeatherArchetype:
|
||||
return WeatherArchetype(
|
||||
id=data["id"], wind_parameters=WindParameters.from_data(data["wind"])
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_yaml(path: Path) -> WeatherArchetype:
|
||||
with path.open(encoding="utf-8") as yaml_file:
|
||||
data = yaml.safe_load(yaml_file)
|
||||
return WeatherArchetype.from_data(data)
|
||||
|
||||
|
||||
class WeatherArchetypes:
|
||||
_by_id: dict[str, WeatherArchetype] | None = None
|
||||
|
||||
@classmethod
|
||||
def with_id(cls, ident: str) -> WeatherArchetype:
|
||||
if cls._by_id is None:
|
||||
cls._by_id = cls.load()
|
||||
return cls._by_id[ident]
|
||||
|
||||
@staticmethod
|
||||
def load() -> dict[str, WeatherArchetype]:
|
||||
by_id = {}
|
||||
for path in Path("resources/weather/archetypes").glob("*.yaml"):
|
||||
archetype = WeatherArchetype.from_yaml(path)
|
||||
if archetype.id in by_id:
|
||||
raise RuntimeError(
|
||||
f"Found duplicate weather archetype ID: {archetype.id}"
|
||||
)
|
||||
by_id[archetype.id] = archetype
|
||||
return by_id
|
||||
95
game/weather/windspeedgenerators.py
Normal file
95
game/weather/windspeedgenerators.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from dcs.weather import Wind
|
||||
|
||||
from game.utils import Speed, knots, Heading
|
||||
from .wind import WindConditions
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WeibullWindSpeedParameters:
|
||||
shape: float
|
||||
scale: Speed
|
||||
|
||||
@staticmethod
|
||||
def from_data(data: dict[str, Any]) -> WeibullWindSpeedParameters:
|
||||
return WeibullWindSpeedParameters(
|
||||
shape=data["shape"], scale=knots(data["scale_kts"])
|
||||
)
|
||||
|
||||
|
||||
class WindSpeedGenerator(ABC):
|
||||
@abstractmethod
|
||||
def random_wind(self) -> WindConditions:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def from_data(data: dict[str, Any]) -> WindSpeedGenerator:
|
||||
if len(data) != 1:
|
||||
raise ValueError(
|
||||
f"Wind speed dict has wrong number of keys ({len(data)}). Expected 1."
|
||||
)
|
||||
name = list(data.keys())[0]
|
||||
match name:
|
||||
case "weibull":
|
||||
return WeibullWindSpeedGenerator.from_data(data["weibull"])
|
||||
raise KeyError(f"Unknown wind speed generator type: {name}")
|
||||
|
||||
|
||||
class WeibullWindSpeedGenerator(WindSpeedGenerator):
|
||||
def __init__(
|
||||
self,
|
||||
at_msl: WeibullWindSpeedParameters,
|
||||
at_2000m: WeibullWindSpeedParameters,
|
||||
at_8000m: WeibullWindSpeedParameters,
|
||||
) -> None:
|
||||
self.at_msl = at_msl
|
||||
self.at_2000m = at_2000m
|
||||
self.at_8000m = at_8000m
|
||||
|
||||
def random_wind(self) -> WindConditions:
|
||||
wind_direction = Heading.random()
|
||||
wind_direction_2000m = wind_direction + Heading.random(-90, 90)
|
||||
wind_direction_8000m = wind_direction + Heading.random(-90, 90)
|
||||
|
||||
# The first parameter is the scale. 63.2% of all results will fall below that
|
||||
# value.
|
||||
# https://www.itl.nist.gov/div898/handbook/eda/section3/weibplot.htm
|
||||
msl = random.weibullvariate(
|
||||
self.at_msl.scale.meters_per_second, self.at_msl.shape
|
||||
)
|
||||
at_2000m = random.weibullvariate(
|
||||
msl + self.at_2000m.scale.meters_per_second, self.at_2000m.shape
|
||||
)
|
||||
at_8000m = random.weibullvariate(
|
||||
at_2000m + self.at_8000m.scale.meters_per_second, self.at_8000m.shape
|
||||
)
|
||||
|
||||
# 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.0, msl)),
|
||||
at_2000m=Wind(
|
||||
wind_direction_2000m.degrees,
|
||||
min(max_supported_wind_speed, at_2000m),
|
||||
),
|
||||
at_8000m=Wind(
|
||||
wind_direction_8000m.degrees,
|
||||
min(max_supported_wind_speed, at_8000m),
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def from_data(data: dict[str, Any]) -> WindSpeedGenerator:
|
||||
return WeibullWindSpeedGenerator(
|
||||
at_msl=WeibullWindSpeedParameters.from_data(data["at_msl"]),
|
||||
at_2000m=WeibullWindSpeedParameters.from_data(data["at_2000m"]),
|
||||
at_8000m=WeibullWindSpeedParameters.from_data(data["at_8000m"]),
|
||||
)
|
||||
Reference in New Issue
Block a user