MONKEY (fix-wind): visible foliage wind + Foliage Wind Strength slider

The owner's "wind does not work" was the motion being too small to see (WXL's
0.06 yd grass tips on a 6 s wave, 0.1 yd tree crowns). Retuned at the 9 yd/s
base speed:
- grass: tips about +-0.2-0.3 yd, 2.5 s main wave (182 cycles/wrap), 1.1 s
  cross flutter (410); 55 yd gust bands roll downwind (68), swelling the sway
  to 1.2x and lulling it to 0.55x; a bent tip dips (no stretched blades).
  Parting around units unchanged.
- trees/bushes: 4 s crown sway (114), 5.5 s side wave (82), 0.8 s leaf
  flutter (569); crown weight grows with height (0.43x at 1.5 yd, 1x at 6 yd,
  1.3x cap), so canopies sway about +-0.3 yd while bushes nod and bases stay
  planted.
- new cvar foliageWindStrength 0.25..3 (default 1, not on the preset ladder):
  Advanced Graphics slider under Foliage Wind (EN+RU label/tooltip), greyed
  while Foliage Wind is Off; carried in sway_strength, so the parting ignores
  it; a storm adds up to +60 % sway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pkuzic
2026-09-26 17:53:32 +01:00
co-authored by Claude Opus 5.5
parent 37dcd39e51
commit 0299979121
6 changed files with 142 additions and 24 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ behind each constant.
| Sky quality | Enhanced: the five Light.dbc sky stops through a smooth monotone curve in linear light (no bands at the rings), a soft sun glow tinted by sun and fog colour (fades at night and under cloud), a procedural star field with twinkle and a faint Milky Way over the stock `Stars.m2`. High adds domain-warped cloud detail and sun-lit clouds (self-shadow, silver lining; technique from WarcraftXL, see `THIRD-PARTY.md`). Classic is the reference sky unchanged | Advanced Graphics → Sky Quality, cvar `skyQuality` 0 Classic / 1 Enhanced / 2 High, env `WOW_SKY_QUALITY` |
| Modern fog | MONKEY (fog): radial distance fog with a gradual exponential curve, a daylight sun lobe, and an end colour shared by the world, WDL hull, sky horizon and volumetric haze. Each zone keeps its authored fog end up to the reference view-distance limit; optional `LightFogBand.dbc` rows provide height, sun and end-fog controls. Interior WMO fog stays classic | Advanced Graphics → Modern Fog, cvar `fogModel` 0 Classic (default) / 1 Modern (High = 1), env `WOW_FOGMODEL=0\|1`; `fog_hook.wgsl`, `lighting/fog_model.rs`, `light/fog_band.rs` |
| Rain on surfaces | MONKEY (wet): rain gradually darkens and saturates exposed terrain and models, adds puddles and a sky/sun sheen, and produces rings on exterior Enhanced water. Wetness rises over roughly 90 seconds and dries over roughly four minutes; MONKEY (rainshelter): a camera-centred rain-occlusion map (128² cells of 0.75 yd, filled by vertical collision rays) keeps ground, walls and water under roofs, porches and bridges dry, with a ~1.5 yd drip margin and a soft edge; tree canopies without colliders do not shelter | Advanced Graphics → Wet Surfaces in Rain, cvar `rainSurfaces` 0/1 (default and High 1), env `WOW_RAIN_SURFACES`, `WOW_WETNESS`, `WOW_WET_T`; `weather/wetness.rs`, `weather/shelter.rs`, `wet_hook.wgsl` |
| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), capture override `WOW_FOLIAGE_WIND` (dev build + `WOW_CAPTURE` only); field and benders in `benilla-world/src/wind/`. The wave phase is the CPU-integrated travel (speed integrated over time, wrapped seamlessly), and the waves' spatial term uses the fixed profile heading; the veer turns only the bend. Tree exile copies sway through material marker bit 14 (`FOLIAGE_WIND_MARKER`). Capture offset `WOW_CAPTURE_WIND_T` (s). Known: shadow casters (world-shadow proxies, `torch_depth.wgsl`) do not apply the sway, so leaves sample a static shadow; a future world-camera DepthPrepass must call the same offsets |
| Foliage wind | one weather-fed gust/veer field drives grass and classified tree/bush foliage; grass also parts around the player and nearby units | Advanced Graphics → Foliage Wind, cvar `foliageWind` 0 Off / 1 Grass / 2 Grass + Trees (High = 2), plus the slider Foliage Wind Strength, cvar `foliageWindStrength` 0.25..3 (default 1, not on the preset ladder; a gain on the sway, not the parting; a storm adds up to +60 %). Tuning at 1 (fix-wind): grass tips about ±0.2-0.3 yd on a 2.5 s wave with 1.1 s cross flutter, gust bands 55 yd wide rolling downwind; tree crowns about ±0.3 yd (lever-arm weight grows with height to 1.3x at 8.5 yd) on a 4 s sway, 5.5 s side wave, 0.8 s leaf flutter; bent tips dip slightly. capture override `WOW_FOLIAGE_WIND` (dev build + `WOW_CAPTURE` only); field and benders in `benilla-world/src/wind/`. The wave phase is the CPU-integrated travel (speed integrated over time, wrapped seamlessly), and the waves' spatial term uses the fixed profile heading; the veer turns only the bend. Tree exile copies sway through material marker bit 14 (`FOLIAGE_WIND_MARKER`). Capture offset `WOW_CAPTURE_WIND_T` (s). Known: shadow casters (world-shadow proxies, `torch_depth.wgsl`) do not apply the sway, so leaves sample a static shadow; a future world-camera DepthPrepass must call the same offsets |
| Ambient occlusion | soft contact shadows under props, in wall-floor corners and around feet; depth-only SSAO at half resolution (depth-reconstructed normals, 6/12 spiral taps, 4x4 depth-aware blur, bilateral upsample) multiplied into the opaque scene after the opaque pass and before the water copy, so water, particles and UI are untouched; fades out 45-90 yd (Low) / 60-120 yd (High), spares sky and bright pixels; on alpha-tested foliage (a dense window of two-sided depth spikes) the blur widens its depth tolerance and the AO drops 40 %, which removes the leaf grain while planes, silhouettes and contact creases keep full AO (`benilla-app/src/ssao.rs`, `benilla-app/src/shaders/ssao.wgsl`) | Advanced Graphics → Ambient Occlusion (Off/Low/High; default Off, High preset 2), cvar `ambientOcclusion`, env `WOW_AO=0\|1\|2`, `WOW_AO_DEBUG=1..5` diagnostic views (factor, protection, distance, raw occlusion, foliage mask), `WOW_AO_GAIN`/`_RADIUS`/`_BIAS`/`_STRENGTH` tuning |
| Lamp fog | MONKEY (lampfog): the fog fullscreen pass selects the nearest resolved exterior and interior point-table sources and analytically integrates their softened inverse-square light over the camera-to-scene-depth ray. A near-field haze floor makes lamps visible inside the ordinary volumetric haze's 10 yd clear zone; it is zero by day, stronger in rain/fog, and deliberately unshadowed. The table colour already includes intensity, flame flicker and `fireLightGain`. Halo output uses destination headroom and stays below 1.0, so a later bloom pass cannot blow it out | Advanced Graphics → Lamp Fog, cvar `lampFog` 0 Off (default, unchanged) / 1 Low (16 lamps) / 2 High (32 lamps; Graphics preset High = 2), env `WOW_LAMPFOG=0\|1\|2`; `benilla-app/src/volumetric_fog.rs` |
| City window daylight | MONKEY (fix-daylight/integration): a district WMO's oversized window batch (e.g. the Cathedral of Light's stained-glass walls) is split into window-sized daylight apertures, so the nave takes daylight pane by pane. Applies to WMOs loaded after a change | Advanced Graphics → City Window Daylight, cvar `daylightWindowSplit` 0/1 (default 1, the merged behaviour); `benilla-world/src/lighting/daylight.rs` (`set_window_split`), bridge `benilla-app/src/dynamic_interior.rs` |
+32 -2
View File
@@ -543,6 +543,7 @@
foliageWind = "Foliage Wind",
windGrass = "Grass",
windTrees = "Grass + Trees",
foliageWindStrength = "Foliage Wind Strength", -- MONKEY (fix-wind)
-- MONKEY (ao): localised contact-shadow row.
ambientOcclusion = "Ambient Occlusion",
-- MONKEY (lampfog)
@@ -707,6 +708,11 @@
"Grass bends in two travelling wind waves and parts around nearby characters. "
.. "Grass + Trees also gives classified tree and bush foliage a gentle sway.\n\n"
.. "Off makes no vertex displacement and restores the unchanged image. Tree shadows stay static.",
-- MONKEY (fix-wind)
FOLIAGE_WIND_STRENGTH =
"How far grass and foliage swing in the wind. 100% is the standard sway; lower "
.. "is calmer, higher is a blustery day. Storms swing everything harder on top. "
.. "Grass still parts around characters the same way. Needs Foliage Wind on.",
-- MONKEY (ao): what the contact shadows touch and what they spare.
AMBIENT_OCCLUSION =
"Soft contact shadows where surfaces meet: under props and furniture, in the corners "
@@ -782,6 +788,7 @@
foliageWind = "Ветер листвы",
windGrass = "Трава",
windTrees = "Трава + деревья",
foliageWindStrength = "Сила ветра листвы", -- MONKEY (fix-wind)
-- MONKEY (ao): Russian contact-shadow row.
ambientOcclusion = "Затенение окружения",
-- MONKEY (lampfog)
@@ -956,6 +963,11 @@
"Трава гнётся двумя бегущими волнами ветра и расступается вокруг персонажей. "
.. "Трава + деревья также добавляет мягкое качание листвы деревьев и кустов.\n\n"
.. "Выключено не смещает вершины и восстанавливает исходный вид. Тени деревьев остаются неподвижными.",
-- MONKEY (fix-wind)
FOLIAGE_WIND_STRENGTH =
"Насколько сильно трава и листва качаются на ветру. 100% — обычное качание; меньше — "
.. "спокойнее, больше — ветреный день. В грозу всё качается ещё сильнее. "
.. "Трава по-прежнему расступается вокруг персонажей. Работает, если включён ветер листвы.",
-- MONKEY (ao): Russian contact-shadow explanation.
AMBIENT_OCCLUSION =
"Мягкие контактные тени там, где поверхности соприкасаются: под предметами и мебелью, "
@@ -1049,6 +1061,8 @@
"RowRainSurfaces",
-- MONKEY (wind): appended for conflict-free lane merges.
"RowFoliageWind",
-- MONKEY (fix-wind): the sway strength, right under its tier.
"RowFoliageWindStrength",
-- MONKEY (ao): appended so the other rows keep their indices.
"RowAmbientOcclusion",
-- MONKEY (lampfog)
@@ -1574,6 +1588,9 @@
-- MONKEY (daylight: terrain torch casters): the ground only casts for outdoor fires.
OptionsRow_SetEnabled(getglobal(base .. "RowTorchTerrainShadows"),
GetCVar("exteriorShadows") ~= "0")
-- MONKEY (fix-wind): the strength only matters while some foliage sways.
OptionsRow_SetEnabled(getglobal(base .. "RowFoliageWindStrength"),
GetCVar("foliageWind") ~= "0")
return
end
if key == "Controls" then
@@ -3260,12 +3277,25 @@
})
</OnLoad></Scripts>
</Frame>
<!-- MONKEY (ao): numeric tiers through the existing localised dropdown. -->
<Frame name="$parentRowAmbientOcclusion" inherits="OptionsDropdownRowTemplate">
<!-- MONKEY (fix-wind): the sway gain, 0.25..3 (100% = the shipped tuning). -->
<Frame name="$parentRowFoliageWindStrength" inherits="OptionsSliderRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowFoliageWind" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowFoliageWind" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "foliageWindStrength",
BENILLA_ADVGFX.foliageWindStrength,
"BENILLA_TOOLTIP_FOLIAGE_WIND_STRENGTH")
OptionsSliderRow_Setup(self, 0.25, 3, 0.25)
</OnLoad></Scripts>
</Frame>
<!-- MONKEY (ao): numeric tiers through the existing localised dropdown. -->
<Frame name="$parentRowAmbientOcclusion" inherits="OptionsDropdownRowTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentRowFoliageWindStrength" relativePoint="BOTTOMLEFT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
<Anchor point="TOPRIGHT" relativeTo="$parentRowFoliageWindStrength" relativePoint="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="-9"/></Offset></Anchor>
</Anchors>
<Scripts><OnLoad>
OptionsRow_OnLoad(self, "ambientOcclusion", BENILLA_ADVGFX.ambientOcclusion,
"BENILLA_TOOLTIP_AMBIENT_OCCLUSION")
+7
View File
@@ -964,6 +964,12 @@ pub(crate) const REGISTERED: &[Registered] = &[
"2",
"benilla's own: foliage wind, 0 Off / 1 Grass / 2 Grass + trees",
),
// MONKEY (fix-wind): the sway strength slider under Foliage Wind; not on the preset ladder.
ours(
"foliageWindStrength",
"1",
"benilla's own: foliage wind strength, a gain on the sway, 0.25..3 (1 = the shipped tuning)",
),
// MONKEY (fog): the distance-fog model. 0 = the 1.12 linear fog (byte-identical), 1 = Modern
// (exponential, sun/horizon colour, end-fog shift, fog end decoupled from farclip past 777).
// Default 0; the Graphics preset's High sets 1.
@@ -4420,6 +4426,7 @@ mod tests {
app.init_resource::<benilla_world::lighting::FogModelSetting>();
// MONKEY (wind)
app.init_resource::<benilla_world::wind::FoliageWind>();
app.init_resource::<benilla_world::wind::FoliageWindStrength>(); // MONKEY (fix-wind)
app.init_resource::<benilla_world::weather::RainSurfaces>(); // MONKEY (wet)
app.add_observer(crate::monkey_gfx::on_cvar);
},
+11 -1
View File
@@ -6,12 +6,13 @@
//! |---|---|---|---|---|
//! | `skyDither` | 0 Off / 1 On | 0 | 1 | `benilla_world::ffx_glow::SkyDither` |
//! | `foliageWind` | 0 Off / 1 Grass / 2 Grass + trees | 2 | 2 | `benilla_world::wind::FoliageWind` |
//! | `foliageWindStrength` | 0.25..3 sway gain | 1 | not governed | `benilla_world::wind::FoliageWindStrength` |
//! | `fogModel` | 0 Classic / 1 Modern | 0 | 1 | `benilla_world::lighting::FogModelSetting` |
//! | `rainSurfaces` | 0 Off / 1 On | 1 | 1 | `benilla_world::weather::RainSurfaces` |
use benilla_world::ffx_glow::SkyDither;
use benilla_world::lighting::FogModelSetting;
use benilla_world::wind::FoliageWind;
use benilla_world::wind::{FoliageWind, FoliageWindStrength};
use bevy::prelude::*;
use benilla_world::weather::RainSurfaces; // MONKEY (wet)
@@ -20,6 +21,7 @@ pub(crate) fn on_cvar(
ev: On<crate::cvars::CvarChanged>,
mut dither: ResMut<SkyDither>,
mut foliage_wind: ResMut<FoliageWind>,
mut wind_strength: ResMut<FoliageWindStrength>, // MONKEY (fix-wind)
// MONKEY (fog)
mut fog_model: ResMut<FogModelSetting>,
mut rain: ResMut<RainSurfaces>, // MONKEY (wet)
@@ -41,6 +43,13 @@ pub(crate) fn on_cvar(
foliage_wind.0 = want;
}
}
// MONKEY (fix-wind): the sway gain, live, clamped to the slider's 0.25..3.
"foliagewindstrength" => {
let want = FoliageWindStrength::from_cvar(ev.num());
if *wind_strength != want {
*wind_strength = want;
}
}
// MONKEY (fog): `WOW_FOGMODEL` still wins for the session (captures).
"fogmodel" => fog_model.set_from_cvar(ev.num()),
// MONKEY (wet): wet surfaces + rain ripples.
@@ -74,6 +83,7 @@ impl Plugin for MonkeyGfxPlugin {
.insert_resource(FoliageWind(
foliage_wind_pin().unwrap_or(FoliageWind::REGISTERED),
))
.init_resource::<FoliageWindStrength>() // MONKEY (fix-wind)
.init_resource::<FogModelSetting>()
.init_resource::<RainSurfaces>() // MONKEY (wet)
.add_observer(on_cvar);
@@ -21,6 +21,14 @@ fn wind_base_dir(frame: MonkeyFrame) -> vec2<f32> {
return vec2<f32>(cos(frame.wind_a.z), sin(frame.wind_a.z));
}
// MONKEY (fix-wind): gusts rolling across a field: a 55 yd band travelling downwind (68 cycles
// per wrap, about 8 yd/s at the base speed) that swells the sway to 1.2x and lulls it to 0.55x.
fn wind_gust_roll(frame: MonkeyFrame, xz: vec2<f32>) -> f32 {
let band = sin(dot(wind_base_dir(frame), xz) * (WIND_TAU / 55.0)
- wind_travel_phase(frame.wind_b.x, 68.0));
return mix(0.55, 1.2, 0.5 + 0.5 * band);
}
fn wind_cross_dir(dir: vec2<f32>) -> vec2<f32> {
// WarcraftXL's secondary wave is 35 degrees off the primary.
return vec2<f32>(
@@ -51,19 +59,20 @@ fn grass_offset(
let base_secondary = wind_cross_dir(base_primary);
let wave_primary = WIND_TAU / 18.0;
let wave_secondary = WIND_TAU / 6.5;
// WarcraftXL's k * 0.33 * speed (primary) and k * 0.33 * 1.37 * speed (secondary), as whole
// cycles per wrap: 75 and 285.
// MONKEY (fix-wind): visible tuning. At the 9 yd/s base speed the primary wave runs a 2.5 s
// period (182 cycles per wrap) and the cross flutter 1.1 s (410); both speed up with gusts.
let phase_primary = wave_primary * dot(base_primary, world.xz)
- wind_travel_phase(frame.wind_b.x, 75.0) + tuft_phase;
- wind_travel_phase(frame.wind_b.x, 182.0) + tuft_phase;
let phase_secondary = wave_secondary * dot(base_secondary, world.xz)
- wind_travel_phase(frame.wind_b.x, 285.0) + tuft_phase * 1.7;
- wind_travel_phase(frame.wind_b.x, 410.0) + tuft_phase * 1.7;
let gust = 1.0 + 0.5 * (frame.wind_a.w - 0.5) * 2.0;
let variance = mix(0.7, 1.3, fract(sin(tuft_phase * 12.9898) * 43758.5453));
let distance_fade = 1.0 / (1.0 + distance(camera, world) * 0.015);
var xz = (
dir_primary * 0.060 * (sin(phase_primary) + 0.35)
+ dir_secondary * 0.020 * sin(phase_secondary)
) * gust * variance * weight * distance_fade * frame.wind_b.y * frame.wind_b.z;
dir_primary * 0.18 * (sin(phase_primary) + 0.35)
+ dir_secondary * 0.06 * sin(phase_secondary)
) * gust * variance * wind_gust_roll(frame, world.xz) * weight * distance_fade
* frame.wind_b.y * frame.wind_b.z;
let count = min(u32(frame.misc.x), 8u);
for (var i = 0u; i < 8u; i += 1u) {
@@ -77,7 +86,8 @@ fn grass_offset(
xz += radial * 0.50 * edge * cone * weight * frame.wind_b.z;
}
}
return vec3<f32>(xz.x, 0.0, xz.y);
// MONKEY (fix-wind): a bent tip dips, so a blade reads as bending rather than stretching.
return vec3<f32>(xz.x, -min(0.6 * dot(xz, xz), 0.25), xz.y);
}
// MONKEY (wind): our tree/bush design (WarcraftXL has no tree wind). Only leaf-card batches call
@@ -95,26 +105,29 @@ fn tree_offset(
// A one-yard bush still needs a crown; tree leaf cards generally begin above this range and
// therefore reach full weight, while the first 0.15 yd remains a planted base.
let crown = smoothstep(0.15, 1.5, h);
let weight = crown * crown;
// MONKEY (fix-wind): a taller crown swings further (lever arm): 0.43x at 1.5 yd, 1x at 6 yd,
// capped at 1.3x from 8.5 yd, so a bush nods while a canopy sways.
let weight = crown * crown * clamp(0.25 + h * 0.12, 0.25, 1.3);
let dir = normalize(frame.wind_a.xy + vec2<f32>(1.0e-6, 0.0));
let cross = vec2<f32>(-dir.y, dir.x);
let base = wind_base_dir(frame);
let base_cross = vec2<f32>(-base.y, base.x);
let phase = fract(sin(dot(anchor.xz, vec2<f32>(0.173, 0.317))) * 43758.5453) * WIND_TAU;
// 0.08 rad/yd sway, 0.61x side wave and a 2.3 rad/s-at-base-speed flutter, as whole cycles
// per wrap of travel: 52, 32 and 167.
// MONKEY (fix-wind): visible tuning. At the 9 yd/s base speed: a 4 s crown sway (114 cycles
// per wrap), a 5.5 s side wave (82) and a 0.8 s leaf flutter (569).
let slow = sin(dot(base, world.xz) * (WIND_TAU / 34.0)
- wind_travel_phase(frame.wind_b.x, 52.0) + phase);
- wind_travel_phase(frame.wind_b.x, 114.0) + phase);
let side = sin(dot(base_cross, world.xz) * (WIND_TAU / 21.0)
- wind_travel_phase(frame.wind_b.x, 32.0) + phase * 1.7);
let flutter = sin(wind_travel_phase(frame.wind_b.x, 167.0)
- wind_travel_phase(frame.wind_b.x, 82.0) + phase * 1.7);
let flutter = sin(wind_travel_phase(frame.wind_b.x, 569.0)
+ dot(world.xz, vec2<f32>(1.7, 2.1)) + phase * 2.0);
let gust = 0.75 + frame.wind_a.w * 0.5;
let fade = 1.0 / (1.0 + distance(camera, world) * 0.006);
let xz = (
dir * (0.045 + slow * 0.055)
+ cross * side * 0.025
+ dir * flutter * 0.012
) * weight * gust * fade * frame.wind_b.y * frame.wind_b.w;
return vec3<f32>(xz.x, 0.0, xz.y);
dir * (0.08 + slow * 0.20)
+ cross * side * 0.08
+ dir * flutter * 0.03
) * weight * gust * mix(0.75, 1.05, (wind_gust_roll(frame, anchor.xz) - 0.55) / 0.65)
* fade * frame.wind_b.y * frame.wind_b.w;
return vec3<f32>(xz.x, -0.15 * dot(xz, xz), xz.y);
}
+59 -1
View File
@@ -91,6 +91,37 @@ impl FoliageWind {
}
}
/// MONKEY (fix-wind): the player's foliage sway strength (`foliageWindStrength`), a plain gain on
/// the sway amplitude: 0.25..3, 1 = the shipped tuning. Parting around units ignores it.
#[derive(Resource, Clone, Copy, Debug, PartialEq)]
pub struct FoliageWindStrength(pub f32);
impl FoliageWindStrength {
pub const MIN: f32 = 0.25;
pub const MAX: f32 = 3.0;
/// The registered default.
pub const REGISTERED: f32 = 1.0;
/// A CVar value into the slider's range (non-finite reads as the default).
pub fn from_cvar(v: f32) -> Self {
Self(if v.is_finite() {
v.clamp(Self::MIN, Self::MAX)
} else {
Self::REGISTERED
})
}
}
impl Default for FoliageWindStrength {
fn default() -> Self {
Self(Self::REGISTERED)
}
}
/// MONKEY (fix-wind): a storm swings the foliage harder, up to +60 % at a full storm, on top of the
/// faster travel the storm's wind speed already gives.
pub const STORM_SWAY_GAIN: f32 = 0.6;
impl Default for FoliageWind {
fn default() -> Self {
// The resource exists before the CVar host. Its ordinary default therefore has to equal
@@ -186,6 +217,7 @@ fn update_wind(
time: Res<Time>,
weather: Res<WeatherState>,
quality: Res<FoliageWind>,
strength: Option<Res<FoliageWindStrength>>,
mut wind: ResMut<WindField>,
mut frame: ResMut<MonkeyFrame>,
mut clock: Local<WindClock>,
@@ -221,7 +253,16 @@ fn update_wind(
frame.wind_base_heading = wind.profile.heading_deg.to_radians();
frame.wind_gust = wind.sample.gust;
frame.wind_travel = clock.travel as f32;
frame.sway_strength = if quality.0 > 0 { 1.0 } else { 0.0 };
// MONKEY (fix-wind): `sway_strength` carries the player's gain and the storm swing; the tier
// flags stay 0/1 (the grass parting reads only the grass flag, so it keeps its size).
let gain = strength.map_or(FoliageWindStrength::REGISTERED, |s| {
FoliageWindStrength::from_cvar(s.0).0
});
frame.sway_strength = if quality.0 > 0 {
gain * (1.0 + STORM_SWAY_GAIN * storm)
} else {
0.0
};
frame.grass_strength = if quality.0 >= 1 { 1.0 } else { 0.0 };
frame.tree_strength = if quality.0 >= 2 { 1.0 } else { 0.0 };
}
@@ -291,6 +332,7 @@ impl Plugin for WindPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<WindField>()
.init_resource::<FoliageWind>()
.init_resource::<FoliageWindStrength>() // MONKEY (fix-wind)
// MONKEY (integration): one ordered writer run of `MonkeyFrame`'s wind rows, before
// the lighting resolve (the fog model writes the same resource there).
.add_systems(
@@ -367,12 +409,28 @@ mod tests {
travel.push(rows[5][0]);
}
assert!(travel[0] < travel[1] && travel[1] < travel[2], "{travel:?}");
// The player's strength scales the sway only; a storm swings it harder still.
world.insert_resource(FoliageWindStrength::from_cvar(2.0));
world.run_system_once(update_wind).unwrap();
assert_eq!(world.resource::<MonkeyFrame>().pack(0.5, 0.0)[5][1..], [2.0, 1.0, 1.0]);
world.resource_mut::<WeatherState>().sky_density = 1.0;
world.run_system_once(update_wind).unwrap();
let sway = world.resource::<MonkeyFrame>().sway_strength;
assert!((sway - 2.0 * (1.0 + STORM_SWAY_GAIN)).abs() < 1.0e-6, "{sway}");
world.insert_resource(FoliageWind(0));
world.run_system_once(update_wind).unwrap();
let rows = world.resource::<MonkeyFrame>().pack(0.5, 0.0);
assert_eq!(rows[5][1..], [0.0, 0.0, 0.0]);
}
#[test]
fn strength_clamps_to_the_slider_range() {
assert_eq!(FoliageWindStrength::from_cvar(0.0).0, 0.25);
assert_eq!(FoliageWindStrength::from_cvar(9.0).0, 3.0);
assert_eq!(FoliageWindStrength::from_cvar(f32::NAN).0, 1.0);
assert_eq!(FoliageWindStrength::default().0, 1.0);
}
#[test]
fn travel_rate_stays_bounded_late_in_a_session() {
// MONKEY (fix-wind): the published travel advances at the wind speed, never speed x age.