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:
Dan Albert
2023-05-16 00:05:29 -07:00
committed by Raffson
parent 8ed843a9cf
commit 0e139b8640
11 changed files with 321 additions and 83 deletions

View File

@@ -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,

View File

@@ -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)),
)

View 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

View 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"]),
)