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
No known key found for this signature in database
GPG Key ID: B0402B2C9B764D99
11 changed files with 321 additions and 83 deletions

View File

@ -140,6 +140,11 @@ Saves from 6.x are not compatible with 7.0.
* **[Modding]** Add support for VSN F-4B and F-4C mod.
* **[Modding]** Aircraft task capabilities and preferred aircraft for each task are now moddable in the aircraft unit yaml files. Each aircraft has a weight per task. Higher weights are given higher preference.
* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings.
* **[Modding]** Wind speed generation inputs are now moddable. See https://dcs-liberation.rtfd.io/en/latest/modding/weather.html.
* **[New Game Wizard]** Choices for some options will be remembered for the next new game. Not all settings will be preserved, as many are campaign dependent.
* **[New Game Wizard]** Lua plugins can now be set while creating a new game.
* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron.
* **[New Game Wizard]** Squadron liveries can now be selected during air wing configuration.
* **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences.
* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron.
* **[UI]** The orientation of objects like SAMs, EWRs, garrisons, and ships can now be manually adjusted.

View File

@ -6,4 +6,5 @@ Modding guide
:caption: Contents:
fuel-consumption-measurement.md
layouts.rst
layouts.rst
weather.rst

76
docs/modding/weather.rst Normal file
View File

@ -0,0 +1,76 @@
#######
Weather
#######
Weather conditions in DCS Liberation are randomly generated at the start of each
turn. Some of the inputs to that generator (more to come) can be controlled via
the config files in ``resources/weather``.
**********
Archetypes
**********
A weather archetype defines the the conditions for a style of weather, such as
"clear", or "raining". There are currently four archetypes:
1. clear
2. cloudy
3. raining
4. thunderstorm
The odds of each archetype appearing in each season are defined in the theater
yaml files (``resources/theaters/*/info.yaml``).
.. literalinclude:: ../../resources/weather/archetypes/clear.yaml
:language: yaml
:linenos:
:caption: resources/weather/archetypes/clear.yaml
Wind speeds
===========
DCS missions define wind with a speed and heading at each of three altitudes:
1. MSL
2. 2000 meters
3. 8000 meters
Blending between each altitude band is done in a manner defined by DCS.
Liberation randomly generates a direction for the wind at MSL, and each other
altitude band will have wind within +/- 90 degrees of that heading.
Wind speeds can be modded by altering the ``speed`` dict in the archetype yaml.
The only random distribution currently supported is the Weibull distribution, so
all archetypes currently use:
.. code:: yaml
speed:
weibull:
...
The Weibull distribution has two parameters: a shape and a scale.
The scale is simplest to understand. 63.2% of all outcomes of the distribution
are below the scale parameter.
The shape controls where the peak of the distribution is. See the examples in
the links below for illustrations and guidelines, but generally speaking low
values (between 1 and 2.6) will cause low speeds to be more common, medium
values (around 3) will be fairly evenly distributed around the median, and high
values (greater than 3.7) will cause high speeds to be more common. As wind
speeds tend to be higher at higher altitudes and fairly slow close to the
ground, you typically want a low value for MSL, a medium value for 2000m, and a
high value for 8000m.
For examples, see https://statisticsbyjim.com/probability/weibull-distribution/.
To experiment with different inputs, use Wolfram Alpha, e.g.
https://www.wolframalpha.com/input?i=weibull+distribution+1.5+5.
When generating wind speeds, each subsequent altitude band will have the lower
band's speed added to its scale parameter. That is, for the example above, the
actual scale parameter of ``at_2000m`` will be ``20 + wind speed at MSL``, and
the scale parameter of ``at_8000m`` will be ``20 + wind speed at 2000m``. This
is to ensure that a generally windy day (high wind speed at MSL) will create
similarly high winds at higher altitudes and vice versa.

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

View File

@ -0,0 +1,14 @@
---
id: clear
wind:
speed:
weibull:
at_msl:
shape: 1.5
scale_kts: 5
at_2000m:
shape: 3.5
scale_kts: 20
at_8000m:
shape: 6.4
scale_kts: 20

View File

@ -0,0 +1,14 @@
---
id: cloudy
wind:
speed:
weibull:
at_msl:
shape: 1.6
scale_kts: 6.5
at_2000m:
shape: 3.5
scale_kts: 22
at_8000m:
shape: 6.4
scale_kts: 18

View File

@ -0,0 +1,14 @@
---
id: raining
wind:
speed:
weibull:
at_msl:
shape: 2.6
scale_kts: 8
at_2000m:
shape: 4.2
scale_kts: 20
at_8000m:
shape: 6.4
scale_kts: 20

View File

@ -0,0 +1,14 @@
---
id: thunderstorm
wind:
speed:
weibull:
at_msl:
shape: 6
scale_kts: 20
at_2000m:
shape: 6.2
scale_kts: 20
at_8000m:
shape: 6.4
scale_kts: 20