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:
parent
8ed843a9cf
commit
0e139b8640
@ -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.
|
||||
|
||||
@ -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
76
docs/modding/weather.rst
Normal 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.
|
||||
@ -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"]),
|
||||
)
|
||||
14
resources/weather/archetypes/clear.yaml
Normal file
14
resources/weather/archetypes/clear.yaml
Normal 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
|
||||
14
resources/weather/archetypes/cloudy.yaml
Normal file
14
resources/weather/archetypes/cloudy.yaml
Normal 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
|
||||
14
resources/weather/archetypes/raining.yaml
Normal file
14
resources/weather/archetypes/raining.yaml
Normal 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
|
||||
14
resources/weather/archetypes/thunderstorm.yaml
Normal file
14
resources/weather/archetypes/thunderstorm.yaml
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user