From f5547a63bd3aa2a7036f790d04c268a2daddabf6 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Sat, 3 Oct 2026 11:48:53 +0100 Subject: [PATCH] MONKEY (gfx): keep named rock doodads and ground clutter rigid in wind Reject rock, stone, boulder, pebble, gravel and rubble model filenames from retained foliage. Give matching ground-detail models zero bend height so neither grass wind nor player parting moves them. Limit the name veto to the filename to preserve vegetation in stone-named zones. Verified on Windows: two foliage classifier tests and one rigid-model test pass; benilla-app lib tests have 4313 passed, 0 failed, 10 ignored (no-install run); play build passes. Elwynn wind captures at 0 and 3 seconds preserve coloured foliage motion. The tester's specific small-rock assets still need an in-game retest. Co-Authored-By: GPT-6 Astra (Codex) --- crates/benilla-world/src/clutter.rs | 10 +++++++- crates/benilla-world/src/static_gx/mod.rs | 23 +++++++++++++++--- crates/benilla-world/src/wind/mod.rs | 29 +++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/crates/benilla-world/src/clutter.rs b/crates/benilla-world/src/clutter.rs index b43113a8..8fa2cdbd 100644 --- a/crates/benilla-world/src/clutter.rs +++ b/crates/benilla-world/src/clutter.rs @@ -334,6 +334,9 @@ fn build_chunk_clutter( (lo.min(p[2]), hi.max(p[2])) }); let height_span = (max_z - min_z).max(1.0e-4); + // MONKEY (wind): the detail table includes stones as well as grass. Zero bend weight + // keeps their geometry fixed under both wind and the player-parting field. + let bends = !crate::wind::rigid_model(model_path); for sub in subs.iter() { let vcount = sub.positions.len() * placements.len(); let mut positions = Vec::with_capacity(vcount); @@ -359,7 +362,12 @@ fn build_chunk_clutter( for (i, p) in sub.positions.iter().enumerate() { positions.push((rot * (wow_to_bevy(*p) * inst_scale) + origin).to_array()); uvs.push(sub.uvs[i]); - wind.push([((p[2] - min_z) / height_span).clamp(0.0, 1.0), phase]); + let bend_height = if bends { + ((p[2] - min_z) / height_span).clamp(0.0, 1.0) + } else { + 0.0 + }; + wind.push([bend_height, phase]); normals.push(sp.ground_normal); colors.push(tint); } diff --git a/crates/benilla-world/src/static_gx/mod.rs b/crates/benilla-world/src/static_gx/mod.rs index 737443b8..a9dbc73b 100644 --- a/crates/benilla-world/src/static_gx/mod.rs +++ b/crates/benilla-world/src/static_gx/mod.rs @@ -700,11 +700,11 @@ impl StaticGx { } } -/// MONKEY (wind): conservative leaf classification. Alpha test excludes trunks/rocks even when a -/// whole model path says tree; the name terms cover vanilla's tree/bush/plant families. Animated +/// MONKEY (wind): conservative leaf classification. Alpha test alone does not exclude rock cards; +/// rigid model names veto plant-family directories. Animated /// doodads never reach this function because `assemble.rs` excludes them before `StaticGx::divert`. pub(crate) fn foliage_wind_batch(path: &str, blend: ModelBlend, wmo_geometry: bool) -> bool { - if wmo_geometry || blend != ModelBlend::AlphaTest { + if wmo_geometry || blend != ModelBlend::AlphaTest || crate::wind::rigid_model(path) { return false; } let p = path.to_ascii_lowercase(); @@ -920,6 +920,23 @@ mod tests { )); } + #[test] + fn foliage_classifier_vetoes_rigid_cards_in_plant_families() { + for path in [ + "World\\Generic\\PassiveDoodads\\Bush\\BushRock01.m2", + "World\\Azeroth\\Elwynn\\PassiveDoodads\\Trees\\Stone01.m2", + "World/Generic/Foliage/Boulder01.m2", + "World/Generic/Plants/Pebble01.m2", + "World/Generic/Foliage/Gravel01.m2", + "World/Generic/Foliage/Rubble01.m2", + ] { + assert!( + !foliage_wind_batch(path, ModelBlend::AlphaTest, false), + "{path}" + ); + } + } + #[test] fn a_fader_divert_registers_its_placement_seed() { let mut gx = StaticGx::default(); diff --git a/crates/benilla-world/src/wind/mod.rs b/crates/benilla-world/src/wind/mod.rs index 1f88610c..9fd631b3 100644 --- a/crates/benilla-world/src/wind/mod.rs +++ b/crates/benilla-world/src/wind/mod.rs @@ -15,6 +15,19 @@ const TAU: f64 = std::f64::consts::TAU; const RATIOS: [f64; 3] = [1.0, 0.437, 0.1913]; const WEIGHTS: [f64; 3] = [0.60, 0.28, 0.12]; +/// MONKEY (wind): rigid detail models can have alpha-tested cards and live in plant directories. +/// Inspect the filename so a zone such as Stonetalon does not disable all its vegetation. +pub(crate) fn rigid_model(path: &str) -> bool { + let name = path + .rsplit(['/', '\\']) + .next() + .unwrap_or(path) + .to_ascii_lowercase(); + ["rock", "stone", "boulder", "pebble", "gravel", "rubble"] + .iter() + .any(|term| name.contains(term)) +} + /// The WarcraftXL wind profile. Units are yards, seconds and degrees. #[derive(Clone, Copy, Debug, PartialEq)] pub struct WindProfile { @@ -352,6 +365,22 @@ impl Plugin for WindPlugin { mod tests { use super::*; + #[test] + fn rigid_details_ignore_case_and_plant_directory_names() { + for path in [ + "World/Plants/BushRock01.m2", + "World\\Trees\\STONE01.mdx", + "Boulder02.m2", + "Pebble01.m2", + "Gravel01.m2", + "Rubble01.m2", + ] { + assert!(rigid_model(path), "{path}"); + } + assert!(!rigid_model("World/Stonetalon/Trees/Pine01.m2")); + assert!(!rigid_model("World/RockyZone/Grass01.m2")); + } + #[test] fn defaults_are_the_warcraft_xl_profile() { let p = WindProfile::default();