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();