Skip to main content

proof_engine/terrain/
vegetation.rs

1//! Vegetation system — tree/grass/rock placement, LOD, wind, seasonal changes.
2//!
3//! Handles placement of trees, grass clusters, and rocks based on heightmap
4//! and biome data. Includes wind simulation, seasonal variation, procedural
5//! L-system tree skeletons, and frustum/distance culling.
6
7use glam::{Vec2, Vec3, Vec4};
8use crate::terrain::heightmap::HeightMap;
9use crate::terrain::biome::{BiomeMap, BiomeType, VegetationDensity, SeasonFactor};
10
11// ── Internal RNG ──────────────────────────────────────────────────────────────
12
13#[derive(Clone)]
14struct Rng {
15    state: [u64; 4],
16}
17
18impl Rng {
19    fn new(seed: u64) -> Self {
20        let mut s = seed;
21        let mut next = || {
22            s = s.wrapping_add(0x9e3779b97f4a7c15);
23            let mut z = s;
24            z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
25            z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
26            z ^ (z >> 31)
27        };
28        Self { state: [next(), next(), next(), next()] }
29    }
30    fn rol64(x: u64, k: u32) -> u64 { (x << k) | (x >> (64 - k)) }
31    fn next_u64(&mut self) -> u64 {
32        let r = Self::rol64(self.state[1].wrapping_mul(5), 7).wrapping_mul(9);
33        let t = self.state[1] << 17;
34        self.state[2] ^= self.state[0];
35        self.state[3] ^= self.state[1];
36        self.state[1] ^= self.state[2];
37        self.state[0] ^= self.state[3];
38        self.state[2] ^= t;
39        self.state[3] = Self::rol64(self.state[3], 45);
40        r
41    }
42    fn next_f32(&mut self) -> f32 { (self.next_u64() >> 11) as f32 / (1u64 << 53) as f32 }
43    fn next_f32_range(&mut self, lo: f32, hi: f32) -> f32 { lo + self.next_f32() * (hi - lo) }
44    fn next_usize(&mut self, n: usize) -> usize { (self.next_u64() % n as u64) as usize }
45}
46
47// ── TreeType ──────────────────────────────────────────────────────────────────
48
49/// Tree species variants.
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
51pub enum TreeType {
52    Oak,
53    Pine,
54    Birch,
55    Tropical,
56    Dead,
57    Palm,
58    Willow,
59    Cactus,
60    Fern,
61    Mushroom,
62}
63
64impl TreeType {
65    pub fn name(self) -> &'static str {
66        match self {
67            TreeType::Oak      => "Oak",
68            TreeType::Pine     => "Pine",
69            TreeType::Birch    => "Birch",
70            TreeType::Tropical => "Tropical",
71            TreeType::Dead     => "Dead",
72            TreeType::Palm     => "Palm",
73            TreeType::Willow   => "Willow",
74            TreeType::Cactus   => "Cactus",
75            TreeType::Fern     => "Fern",
76            TreeType::Mushroom => "Mushroom",
77        }
78    }
79
80    /// Typical tree types for a given biome.
81    pub fn for_biome(biome: BiomeType) -> &'static [TreeType] {
82        match biome {
83            BiomeType::TemperateForest => &[TreeType::Oak, TreeType::Birch],
84            BiomeType::TropicalForest  => &[TreeType::Tropical, TreeType::Palm],
85            BiomeType::Boreal | BiomeType::Taiga => &[TreeType::Pine],
86            BiomeType::Tundra          => &[TreeType::Dead, TreeType::Fern],
87            BiomeType::Savanna         => &[TreeType::Oak, TreeType::Dead],
88            BiomeType::Desert          => &[TreeType::Cactus],
89            BiomeType::Swamp           => &[TreeType::Willow],
90            BiomeType::Mangrove        => &[TreeType::Tropical],
91            BiomeType::Mountain        => &[TreeType::Pine, TreeType::Dead],
92            BiomeType::Mushroom        => &[TreeType::Mushroom],
93            _                          => &[TreeType::Oak],
94        }
95    }
96}
97
98// ── TreeParams ────────────────────────────────────────────────────────────────
99
100/// Parameters describing a single tree's shape.
101#[derive(Clone, Debug)]
102pub struct TreeParams {
103    pub height:           f32,
104    pub crown_radius:     f32,
105    pub trunk_radius:     f32,
106    pub lean_angle:       f32,  // radians, from vertical
107    pub color_variation:  f32,  // [0,1] hue shift
108    pub branch_density:   f32,  // relative number of branches
109    pub root_spread:      f32,  // radius of root buttresses
110}
111
112impl TreeParams {
113    /// Default parameters for a given tree type.
114    pub fn for_type(tt: TreeType, rng: &mut Rng) -> Self {
115        let var = rng.next_f32_range(-0.1, 0.1);
116        match tt {
117            TreeType::Oak => Self {
118                height: rng.next_f32_range(6.0, 14.0),
119                crown_radius: rng.next_f32_range(3.0, 6.0),
120                trunk_radius: rng.next_f32_range(0.2, 0.5),
121                lean_angle: rng.next_f32_range(0.0, 0.15),
122                color_variation: var.abs(),
123                branch_density: 0.8,
124                root_spread: 0.6,
125            },
126            TreeType::Pine => Self {
127                height: rng.next_f32_range(8.0, 20.0),
128                crown_radius: rng.next_f32_range(1.5, 3.0),
129                trunk_radius: rng.next_f32_range(0.15, 0.35),
130                lean_angle: rng.next_f32_range(0.0, 0.1),
131                color_variation: var.abs() * 0.5,
132                branch_density: 1.2,
133                root_spread: 0.3,
134            },
135            TreeType::Birch => Self {
136                height: rng.next_f32_range(5.0, 12.0),
137                crown_radius: rng.next_f32_range(1.5, 3.0),
138                trunk_radius: rng.next_f32_range(0.1, 0.2),
139                lean_angle: rng.next_f32_range(0.0, 0.2),
140                color_variation: var.abs() * 0.3,
141                branch_density: 0.7,
142                root_spread: 0.3,
143            },
144            TreeType::Tropical => Self {
145                height: rng.next_f32_range(10.0, 25.0),
146                crown_radius: rng.next_f32_range(4.0, 8.0),
147                trunk_radius: rng.next_f32_range(0.3, 0.7),
148                lean_angle: rng.next_f32_range(0.0, 0.25),
149                color_variation: var.abs() * 0.4,
150                branch_density: 0.6,
151                root_spread: 1.5,
152            },
153            TreeType::Dead => Self {
154                height: rng.next_f32_range(3.0, 8.0),
155                crown_radius: rng.next_f32_range(1.0, 2.5),
156                trunk_radius: rng.next_f32_range(0.1, 0.3),
157                lean_angle: rng.next_f32_range(0.0, 0.4),
158                color_variation: 0.0,
159                branch_density: 0.3,
160                root_spread: 0.2,
161            },
162            TreeType::Palm => Self {
163                height: rng.next_f32_range(6.0, 15.0),
164                crown_radius: rng.next_f32_range(3.0, 5.0),
165                trunk_radius: rng.next_f32_range(0.15, 0.3),
166                lean_angle: rng.next_f32_range(0.05, 0.35),
167                color_variation: var.abs() * 0.2,
168                branch_density: 0.2,
169                root_spread: 0.4,
170            },
171            TreeType::Willow => Self {
172                height: rng.next_f32_range(8.0, 16.0),
173                crown_radius: rng.next_f32_range(4.0, 7.0),
174                trunk_radius: rng.next_f32_range(0.2, 0.4),
175                lean_angle: rng.next_f32_range(0.0, 0.3),
176                color_variation: var.abs() * 0.15,
177                branch_density: 1.5,
178                root_spread: 1.2,
179            },
180            TreeType::Cactus => Self {
181                height: rng.next_f32_range(2.0, 6.0),
182                crown_radius: rng.next_f32_range(0.5, 1.5),
183                trunk_radius: rng.next_f32_range(0.15, 0.4),
184                lean_angle: rng.next_f32_range(0.0, 0.1),
185                color_variation: var.abs() * 0.1,
186                branch_density: 0.2,
187                root_spread: 0.5,
188            },
189            TreeType::Fern => Self {
190                height: rng.next_f32_range(0.3, 1.0),
191                crown_radius: rng.next_f32_range(0.3, 0.8),
192                trunk_radius: rng.next_f32_range(0.02, 0.06),
193                lean_angle: rng.next_f32_range(0.0, 0.5),
194                color_variation: var.abs() * 0.2,
195                branch_density: 2.0,
196                root_spread: 0.1,
197            },
198            TreeType::Mushroom => Self {
199                height: rng.next_f32_range(1.0, 4.0),
200                crown_radius: rng.next_f32_range(0.8, 2.5),
201                trunk_radius: rng.next_f32_range(0.1, 0.25),
202                lean_angle: rng.next_f32_range(0.0, 0.15),
203                color_variation: rng.next_f32_range(0.0, 0.5),
204                branch_density: 0.0,
205                root_spread: 0.15,
206            },
207        }
208    }
209}
210
211// ── L-System Tree Skeleton ────────────────────────────────────────────────────
212
213/// A single segment in a procedural tree skeleton.
214#[derive(Clone, Debug)]
215pub struct TreeSegment {
216    pub start:    Vec3,
217    pub end:      Vec3,
218    pub radius:   f32,
219    pub depth:    u32,
220}
221
222/// Procedural tree skeleton generated by an L-system.
223#[derive(Clone, Debug)]
224pub struct TreeSkeleton {
225    pub segments: Vec<TreeSegment>,
226    pub tree_type: TreeType,
227    pub params: TreeParams,
228}
229
230impl TreeSkeleton {
231    /// Generate a tree skeleton using L-system-style recursive branching.
232    pub fn generate(tree_type: TreeType, params: &TreeParams, seed: u64) -> Self {
233        let mut rng = Rng::new(seed);
234        let mut segments = Vec::new();
235        let base_pos = Vec3::ZERO;
236        let lean_x = params.lean_angle * rng.next_f32_range(-1.0, 1.0);
237        let lean_z = params.lean_angle * rng.next_f32_range(-1.0, 1.0);
238        let trunk_dir = Vec3::new(lean_x, 1.0, lean_z).normalize();
239
240        let max_depth = match tree_type {
241            TreeType::Pine | TreeType::Oak | TreeType::Birch => 5u32,
242            TreeType::Tropical | TreeType::Willow           => 4,
243            TreeType::Fern | TreeType::Cactus               => 3,
244            TreeType::Dead                                   => 4,
245            TreeType::Palm | TreeType::Mushroom              => 2,
246        };
247
248        Self::branch(
249            base_pos,
250            trunk_dir,
251            params.height,
252            params.trunk_radius,
253            0,
254            max_depth,
255            params,
256            tree_type,
257            &mut rng,
258            &mut segments,
259        );
260
261        Self { segments, tree_type, params: params.clone() }
262    }
263
264    fn branch(
265        pos:      Vec3,
266        dir:      Vec3,
267        length:   f32,
268        radius:   f32,
269        depth:    u32,
270        max_depth: u32,
271        params:   &TreeParams,
272        tt:       TreeType,
273        rng:      &mut Rng,
274        out:      &mut Vec<TreeSegment>,
275    ) {
276        if depth > max_depth || length < 0.1 || radius < 0.01 { return; }
277
278        let end = pos + dir * length;
279        out.push(TreeSegment { start: pos, end, radius, depth });
280
281        if depth == max_depth { return; }
282
283        let branch_count = match tt {
284            TreeType::Palm | TreeType::Cactus | TreeType::Mushroom => 2,
285            TreeType::Willow => (3.0 * params.branch_density) as u32 + 1,
286            TreeType::Pine   => (4.0 * params.branch_density) as u32 + 2,
287            _                => (3.0 * params.branch_density) as u32 + 2,
288        };
289
290        for _ in 0..branch_count {
291            let spread = match tt {
292                TreeType::Pine     => 0.4,
293                TreeType::Willow   => 0.9,
294                TreeType::Palm     => 1.2,
295                TreeType::Tropical => 0.7,
296                _                  => 0.6,
297            };
298            let dx = rng.next_f32_range(-spread, spread);
299            let dz = rng.next_f32_range(-spread, spread);
300            let branch_dir = (dir + Vec3::new(dx, rng.next_f32_range(-0.1, 0.3), dz)).normalize();
301            let branch_len  = length  * rng.next_f32_range(0.55, 0.75);
302            let branch_rad  = radius  * rng.next_f32_range(0.55, 0.7);
303            let branch_start = pos + dir * (length * rng.next_f32_range(0.5, 0.85));
304            Self::branch(
305                branch_start, branch_dir, branch_len, branch_rad,
306                depth + 1, max_depth, params, tt, rng, out,
307            );
308        }
309    }
310
311    /// Bounding box of the skeleton: (min, max).
312    pub fn bounds(&self) -> (Vec3, Vec3) {
313        let mut mn = Vec3::splat(f32::INFINITY);
314        let mut mx = Vec3::splat(f32::NEG_INFINITY);
315        for seg in &self.segments {
316            mn = mn.min(seg.start).min(seg.end);
317            mx = mx.max(seg.start).max(seg.end);
318        }
319        (mn, mx)
320    }
321}
322
323// ── VegetationInstance ────────────────────────────────────────────────────────
324
325/// A single placed vegetation object (tree, grass cluster, rock).
326#[derive(Clone, Debug)]
327pub struct VegetationInstance {
328    pub position:  Vec3,
329    pub rotation:  f32,   // Y-axis rotation in radians
330    pub scale:     Vec3,
331    pub lod_level: u8,
332    pub visible:   bool,
333    pub kind:      VegetationKind,
334}
335
336/// What kind of vegetation this instance is.
337#[derive(Clone, Debug, PartialEq)]
338pub enum VegetationKind {
339    Tree(TreeType),
340    Grass,
341    Rock { size_class: u8 },
342    Shrub,
343    Flower,
344}
345
346// ── GrassCluster ─────────────────────────────────────────────────────────────
347
348/// A cluster of grass blades sharing placement and animation parameters.
349#[derive(Clone, Debug)]
350pub struct GrassCluster {
351    pub center:          Vec3,
352    pub radius:          f32,
353    pub density:         f32,
354    pub blade_height:    f32,
355    pub blade_width:     f32,
356    pub sway_frequency:  f32,
357    pub sway_amplitude:  f32,
358    pub color:           Vec4,
359    pub biome:           BiomeType,
360}
361
362impl GrassCluster {
363    pub fn new(center: Vec3, radius: f32, biome: BiomeType, rng: &mut Rng) -> Self {
364        let (height, color) = match biome {
365            BiomeType::Grassland => (
366                rng.next_f32_range(0.3, 0.7),
367                Vec4::new(0.3 + rng.next_f32() * 0.1, 0.6 + rng.next_f32() * 0.1, 0.15, 1.0)
368            ),
369            BiomeType::Savanna => (
370                rng.next_f32_range(0.4, 1.2),
371                Vec4::new(0.65 + rng.next_f32() * 0.1, 0.6, 0.15, 1.0)
372            ),
373            BiomeType::TropicalForest => (
374                rng.next_f32_range(0.2, 0.5),
375                Vec4::new(0.15, 0.55 + rng.next_f32() * 0.1, 0.1, 1.0)
376            ),
377            BiomeType::Tundra => (
378                rng.next_f32_range(0.05, 0.2),
379                Vec4::new(0.45, 0.5, 0.3, 1.0)
380            ),
381            _ => (
382                rng.next_f32_range(0.2, 0.5),
383                Vec4::new(0.3, 0.55, 0.15, 1.0)
384            ),
385        };
386        Self {
387            center,
388            radius,
389            density: rng.next_f32_range(0.4, 1.0),
390            blade_height: height,
391            blade_width: rng.next_f32_range(0.02, 0.05),
392            sway_frequency: rng.next_f32_range(0.5, 2.0),
393            sway_amplitude: rng.next_f32_range(0.05, 0.15),
394            color,
395            biome,
396        }
397    }
398
399    /// Compute sway offset at given time for wind animation.
400    pub fn sway_offset(&self, time: f32, wind: Vec2) -> Vec2 {
401        let phase = self.center.x * 0.1 + self.center.z * 0.1;
402        let sway = (time * self.sway_frequency + phase).sin() * self.sway_amplitude;
403        wind.normalize_or_zero() * sway
404    }
405}
406
407// ── GrassField ────────────────────────────────────────────────────────────────
408
409/// Collection of grass clusters over a terrain region.
410#[derive(Clone, Debug)]
411pub struct GrassField {
412    pub clusters: Vec<GrassCluster>,
413}
414
415impl GrassField {
416    /// Generate grass placement from heightmap and biome data.
417    pub fn generate(
418        heightmap: &HeightMap,
419        biome_map: &BiomeMap,
420        density_scale: f32,
421        seed: u64,
422    ) -> Self {
423        let mut rng = Rng::new(seed);
424        let mut clusters = Vec::new();
425        let w = heightmap.width;
426        let h = heightmap.height;
427        let grid_step = 3usize;
428
429        for y in (0..h).step_by(grid_step) {
430            for x in (0..w).step_by(grid_step) {
431                let biome = biome_map.get(x, y);
432                let density = VegetationDensity::for_biome(biome);
433                if density.grass_density * density_scale < rng.next_f32() { continue; }
434                let alt = heightmap.get(x, y);
435                if alt < 0.1 { continue; } // skip ocean
436                let pos = Vec3::new(
437                    x as f32 + rng.next_f32_range(-1.0, 1.0),
438                    alt * 100.0,
439                    y as f32 + rng.next_f32_range(-1.0, 1.0),
440                );
441                let radius = rng.next_f32_range(1.0, 3.0);
442                clusters.push(GrassCluster::new(pos, radius, biome, &mut rng));
443            }
444        }
445        Self { clusters }
446    }
447
448    /// Update sway for all clusters given current time and wind.
449    pub fn update_wind(&mut self, _time: f32, _wind: Vec2) {
450        // In a real system this would update GPU buffers; here we just note the call.
451    }
452}
453
454// ── RockPlacement ─────────────────────────────────────────────────────────────
455
456/// A placed rock or boulder.
457#[derive(Clone, Debug)]
458pub struct RockPlacement {
459    pub position: Vec3,
460    pub rotation: Vec3,
461    pub scale:    Vec3,
462    pub biome:    BiomeType,
463}
464
465/// A cluster of rocks using Poisson disk sampling.
466#[derive(Clone, Debug)]
467pub struct RockCluster {
468    pub rocks:  Vec<RockPlacement>,
469    pub center: Vec3,
470    pub radius: f32,
471}
472
473impl RockCluster {
474    /// Generate a cluster of rocks around a center point.
475    pub fn generate(center: Vec3, radius: f32, biome: BiomeType, count: usize, seed: u64) -> Self {
476        let mut rng = Rng::new(seed);
477        let mut rocks = Vec::new();
478
479        // Poisson disk sampling (simplified dart-throwing)
480        let min_dist = radius / (count as f32).sqrt().max(1.0) * 0.8;
481        let mut positions: Vec<Vec2> = Vec::new();
482        let max_attempts = count * 30;
483
484        for _ in 0..max_attempts {
485            if rocks.len() >= count { break; }
486            let angle = rng.next_f32() * std::f32::consts::TAU;
487            let dist  = rng.next_f32() * radius;
488            let px = center.x + angle.cos() * dist;
489            let pz = center.z + angle.sin() * dist;
490            let candidate = Vec2::new(px, pz);
491            // Check min distance from existing rocks
492            let too_close = positions.iter().any(|&p| p.distance(candidate) < min_dist);
493            if too_close { continue; }
494            positions.push(candidate);
495            let size_class = (rng.next_f32() * 3.0) as u8; // 0=small, 1=med, 2=large
496            let base_scale = match size_class {
497                0 => rng.next_f32_range(0.2, 0.6),
498                1 => rng.next_f32_range(0.6, 1.5),
499                _ => rng.next_f32_range(1.5, 4.0),
500            };
501            let scale_var = Vec3::new(
502                base_scale * rng.next_f32_range(0.8, 1.2),
503                base_scale * rng.next_f32_range(0.6, 1.0),
504                base_scale * rng.next_f32_range(0.8, 1.2),
505            );
506            rocks.push(RockPlacement {
507                position: Vec3::new(px, center.y, pz),
508                rotation: Vec3::new(
509                    rng.next_f32_range(-0.3, 0.3),
510                    rng.next_f32() * std::f32::consts::TAU,
511                    rng.next_f32_range(-0.3, 0.3),
512                ),
513                scale: scale_var,
514                biome,
515            });
516        }
517        Self { rocks, center, radius }
518    }
519}
520
521// ── VegetationLod ─────────────────────────────────────────────────────────────
522
523/// LOD configuration for a vegetation type.
524#[derive(Clone, Debug)]
525pub struct VegetationLod {
526    /// Distance threshold for LOD 0 (full detail).
527    pub lod0_distance: f32,
528    /// Distance threshold for LOD 1 (reduced).
529    pub lod1_distance: f32,
530    /// Distance threshold for billboard impostors.
531    pub billboard_distance: f32,
532    /// Maximum visibility distance (beyond this, cull).
533    pub cull_distance: f32,
534    /// Billboard dimensions (width, height).
535    pub billboard_size: Vec2,
536}
537
538impl VegetationLod {
539    pub fn for_tree(tree_type: TreeType) -> Self {
540        let base = match tree_type {
541            TreeType::Oak | TreeType::Tropical | TreeType::Willow => 40.0f32,
542            TreeType::Pine | TreeType::Birch   => 35.0,
543            TreeType::Palm                      => 30.0,
544            TreeType::Dead | TreeType::Fern     => 20.0,
545            TreeType::Cactus | TreeType::Mushroom => 15.0,
546        };
547        Self {
548            lod0_distance:     base,
549            lod1_distance:     base * 2.0,
550            billboard_distance: base * 4.0,
551            cull_distance:     base * 8.0,
552            billboard_size:    Vec2::new(4.0, 8.0),
553        }
554    }
555
556    pub fn for_grass() -> Self {
557        Self {
558            lod0_distance: 15.0,
559            lod1_distance: 25.0,
560            billboard_distance: 40.0,
561            cull_distance: 60.0,
562            billboard_size: Vec2::new(1.0, 0.5),
563        }
564    }
565
566    pub fn for_rock() -> Self {
567        Self {
568            lod0_distance: 20.0,
569            lod1_distance: 50.0,
570            billboard_distance: 80.0,
571            cull_distance: 150.0,
572            billboard_size: Vec2::new(2.0, 1.5),
573        }
574    }
575
576    /// Return the LOD level for a given distance (0 = full, 1 = reduced, 2 = billboard, 3 = culled).
577    pub fn lod_for_distance(&self, dist: f32) -> u8 {
578        if dist > self.cull_distance      { 3 }
579        else if dist > self.billboard_distance { 2 }
580        else if dist > self.lod1_distance { 1 }
581        else                              { 0 }
582    }
583}
584
585// ── VegetationSystem ──────────────────────────────────────────────────────────
586
587/// Top-level manager for all vegetation instances.
588#[derive(Debug)]
589pub struct VegetationSystem {
590    pub instances:     Vec<VegetationInstance>,
591    pub grass_field:   GrassField,
592    pub rock_clusters: Vec<RockCluster>,
593    pub wind_vector:   Vec2,
594    pub time:          f32,
595}
596
597impl VegetationSystem {
598    pub fn new() -> Self {
599        Self {
600            instances:     Vec::new(),
601            grass_field:   GrassField { clusters: Vec::new() },
602            rock_clusters: Vec::new(),
603            wind_vector:   Vec2::new(0.5, 0.2),
604            time:          0.0,
605        }
606    }
607
608    /// Generate all vegetation from a heightmap and biome map.
609    pub fn generate(
610        heightmap: &HeightMap,
611        biome_map: &BiomeMap,
612        density_scale: f32,
613        seed: u64,
614    ) -> Self {
615        let mut rng = Rng::new(seed);
616        let mut instances = Vec::new();
617        let w = heightmap.width;
618        let h = heightmap.height;
619
620        let slope_map = heightmap.slope_map();
621
622        // Tree placement
623        let tree_grid_step = 4usize;
624        for y in (0..h).step_by(tree_grid_step) {
625            for x in (0..w).step_by(tree_grid_step) {
626                let biome = biome_map.get(x, y);
627                let density = VegetationDensity::for_biome(biome);
628                if density.tree_density * density_scale < 0.05 { continue; }
629                if density.tree_density * density_scale < rng.next_f32() { continue; }
630                let alt = heightmap.get(x, y);
631                if alt < 0.1 { continue; }
632                let slope = slope_map.get(x, y);
633                if slope > 0.6 { continue; } // no trees on cliffs
634
635                let types = TreeType::for_biome(biome);
636                if types.is_empty() { continue; }
637                let tt = types[rng.next_usize(types.len())];
638                let mut tree_rng = Rng::new(seed.wrapping_add(y as u64 * 1000 + x as u64));
639                let params = TreeParams::for_type(tt, &mut tree_rng);
640                let scale_f = params.height / 10.0;
641
642                instances.push(VegetationInstance {
643                    position: Vec3::new(
644                        x as f32 + rng.next_f32_range(-1.5, 1.5),
645                        alt * 100.0,
646                        y as f32 + rng.next_f32_range(-1.5, 1.5),
647                    ),
648                    rotation: rng.next_f32() * std::f32::consts::TAU,
649                    scale: Vec3::splat(scale_f),
650                    lod_level: 0,
651                    visible: true,
652                    kind: VegetationKind::Tree(tt),
653                });
654            }
655        }
656
657        // Rock placement
658        let rock_grid_step = 6usize;
659        for y in (0..h).step_by(rock_grid_step) {
660            for x in (0..w).step_by(rock_grid_step) {
661                let biome = biome_map.get(x, y);
662                let density = VegetationDensity::for_biome(biome);
663                if density.rock_density * density_scale < rng.next_f32() { continue; }
664                let alt = heightmap.get(x, y);
665                if alt < 0.05 { continue; }
666                let size_class = (rng.next_f32() * 3.0) as u8;
667                let base = match size_class { 0 => 0.3f32, 1 => 0.8, _ => 2.0 };
668                instances.push(VegetationInstance {
669                    position: Vec3::new(x as f32, alt * 100.0, y as f32),
670                    rotation: rng.next_f32() * std::f32::consts::TAU,
671                    scale: Vec3::splat(base) * rng.next_f32_range(0.8, 1.2),
672                    lod_level: 0,
673                    visible: true,
674                    kind: VegetationKind::Rock { size_class },
675                });
676            }
677        }
678
679        // Shrub placement
680        let shrub_grid = 5usize;
681        for y in (0..h).step_by(shrub_grid) {
682            for x in (0..w).step_by(shrub_grid) {
683                let biome = biome_map.get(x, y);
684                let density = VegetationDensity::for_biome(biome);
685                if density.shrub_density * density_scale < rng.next_f32() { continue; }
686                let alt = heightmap.get(x, y);
687                if alt < 0.08 { continue; }
688                instances.push(VegetationInstance {
689                    position: Vec3::new(x as f32, alt * 100.0, y as f32),
690                    rotation: rng.next_f32() * std::f32::consts::TAU,
691                    scale: Vec3::splat(rng.next_f32_range(0.3, 0.9)),
692                    lod_level: 0,
693                    visible: true,
694                    kind: VegetationKind::Shrub,
695                });
696            }
697        }
698
699        let grass = GrassField::generate(heightmap, biome_map, density_scale, seed.wrapping_add(0xABCD));
700        let rocks = Self::generate_rock_clusters(heightmap, biome_map, density_scale, seed.wrapping_add(0x1234));
701
702        Self {
703            instances,
704            grass_field: grass,
705            rock_clusters: rocks,
706            wind_vector: Vec2::new(0.5, 0.2),
707            time: 0.0,
708        }
709    }
710
711    fn generate_rock_clusters(
712        heightmap: &HeightMap,
713        biome_map: &BiomeMap,
714        density_scale: f32,
715        seed: u64,
716    ) -> Vec<RockCluster> {
717        let mut rng = Rng::new(seed);
718        let mut clusters = Vec::new();
719        let w = heightmap.width;
720        let h = heightmap.height;
721        let step = 12usize;
722        for y in (0..h).step_by(step) {
723            for x in (0..w).step_by(step) {
724                let biome = biome_map.get(x, y);
725                let density = VegetationDensity::for_biome(biome);
726                if density.rock_density * density_scale * 0.3 < rng.next_f32() { continue; }
727                let alt = heightmap.get(x, y);
728                let center = Vec3::new(x as f32, alt * 100.0, y as f32);
729                let count = rng.next_usize(8) + 2;
730                clusters.push(RockCluster::generate(center, 5.0, biome, count, rng.next_u64()));
731            }
732        }
733        clusters
734    }
735
736    /// Update LOD levels for all instances based on camera position.
737    pub fn update_lod(&mut self, camera_pos: Vec3) {
738        for inst in self.instances.iter_mut() {
739            let dist = (inst.position - camera_pos).length();
740            let lod = match &inst.kind {
741                VegetationKind::Tree(tt) => VegetationLod::for_tree(*tt).lod_for_distance(dist),
742                VegetationKind::Grass    => VegetationLod::for_grass().lod_for_distance(dist),
743                VegetationKind::Rock {..} => VegetationLod::for_rock().lod_for_distance(dist),
744                VegetationKind::Shrub    => VegetationLod::for_rock().lod_for_distance(dist),
745                VegetationKind::Flower   => VegetationLod::for_grass().lod_for_distance(dist),
746            };
747            inst.lod_level = lod;
748            inst.visible   = lod < 3;
749        }
750    }
751
752    /// Frustum cull instances. `planes` is 6 plane normal+distance pairs.
753    pub fn frustum_cull(&mut self, planes: &[(Vec3, f32); 6]) {
754        for inst in self.instances.iter_mut() {
755            if !inst.visible { continue; }
756            let p = inst.position;
757            let inside = planes.iter().all(|(normal, dist)| {
758                normal.dot(p) + dist >= 0.0
759            });
760            inst.visible = inside;
761        }
762    }
763
764    /// Apply seasonal variation to all instances.
765    pub fn apply_season(&mut self, month: u32) {
766        for inst in self.instances.iter_mut() {
767            let biome = match &inst.kind {
768                VegetationKind::Tree(tt) => {
769                    // Approximate biome from tree type
770                    match tt {
771                        TreeType::Pine | TreeType::Fern => BiomeType::Taiga,
772                        TreeType::Tropical | TreeType::Palm => BiomeType::TropicalForest,
773                        TreeType::Willow => BiomeType::Swamp,
774                        _ => BiomeType::TemperateForest,
775                    }
776                }
777                _ => BiomeType::Grassland,
778            };
779            let sf = SeasonFactor::season_factor(biome, month);
780            // Scale by density and season
781            inst.scale = inst.scale * sf.density_scale;
782            inst.visible = inst.visible && sf.density_scale > 0.05;
783        }
784    }
785
786    /// Update wind simulation for the given time step.
787    pub fn update(&mut self, dt: f32, wind: Vec2) {
788        self.time += dt;
789        self.wind_vector = wind;
790        self.grass_field.update_wind(self.time, wind);
791    }
792
793    /// Count visible instances.
794    pub fn visible_count(&self) -> usize {
795        self.instances.iter().filter(|i| i.visible).count()
796    }
797
798    /// Get all visible instances at a given LOD level.
799    pub fn instances_at_lod(&self, lod: u8) -> impl Iterator<Item = &VegetationInstance> {
800        self.instances.iter().filter(move |i| i.visible && i.lod_level == lod)
801    }
802}
803
804impl Default for VegetationSystem {
805    fn default() -> Self { Self::new() }
806}
807
808// ── VegetationPainter ─────────────────────────────────────────────────────────
809
810/// Manual vegetation painting API for terrain editors.
811#[derive(Debug, Clone)]
812pub struct VegetationPainter {
813    pub brush_radius: f32,
814    pub brush_strength: f32,
815    pub brush_kind: VegetationKind,
816}
817
818impl VegetationPainter {
819    pub fn new(radius: f32, strength: f32, kind: VegetationKind) -> Self {
820        Self { brush_radius: radius, brush_strength: strength, brush_kind: kind }
821    }
822
823    /// Add vegetation instances within brush circle around `center`.
824    pub fn paint(
825        &self,
826        center: Vec3,
827        system: &mut VegetationSystem,
828        heightmap: &HeightMap,
829        seed: u64,
830    ) {
831        let mut rng = Rng::new(seed.wrapping_add(center.x.to_bits() as u64).wrapping_add(center.z.to_bits() as u64));
832        let count = (self.brush_radius * self.brush_strength * 2.0) as usize + 1;
833        for _ in 0..count {
834            let angle = rng.next_f32() * std::f32::consts::TAU;
835            let dist  = rng.next_f32() * self.brush_radius;
836            let px = center.x + angle.cos() * dist;
837            let pz = center.z + angle.sin() * dist;
838            let xi = px as usize;
839            let zi = pz as usize;
840            let alt = heightmap.get(
841                xi.min(heightmap.width.saturating_sub(1)),
842                zi.min(heightmap.height.saturating_sub(1)),
843            );
844            system.instances.push(VegetationInstance {
845                position: Vec3::new(px, alt * 100.0, pz),
846                rotation: rng.next_f32() * std::f32::consts::TAU,
847                scale: Vec3::splat(rng.next_f32_range(0.8, 1.2)),
848                lod_level: 0,
849                visible: true,
850                kind: self.brush_kind.clone(),
851            });
852        }
853    }
854
855    /// Remove vegetation instances within brush circle around `center`.
856    pub fn erase(&self, center: Vec3, system: &mut VegetationSystem) {
857        let r2 = self.brush_radius * self.brush_radius;
858        system.instances.retain(|inst| {
859            let dx = inst.position.x - center.x;
860            let dz = inst.position.z - center.z;
861            dx * dx + dz * dz > r2
862        });
863    }
864
865    /// Resize brush radius.
866    pub fn resize(&mut self, new_radius: f32) {
867        self.brush_radius = new_radius.max(0.1);
868    }
869}
870
871// ── Impostor Billboard Generation ─────────────────────────────────────────────
872
873/// Represents a billboard impostor for far-distance rendering.
874#[derive(Clone, Debug)]
875pub struct ImpostorBillboard {
876    pub position:   Vec3,
877    pub size:       Vec2,
878    pub lod_level:  u8,
879    pub atlas_uv:   [f32; 4],  // u0, v0, u1, v1
880}
881
882/// Generate billboard impostors from a set of tree instances.
883pub fn generate_impostors(
884    instances: &[VegetationInstance],
885    camera_pos: Vec3,
886    max_distance: f32,
887) -> Vec<ImpostorBillboard> {
888    instances.iter()
889        .filter(|i| i.visible && i.lod_level == 2)
890        .filter(|i| (i.position - camera_pos).length() < max_distance)
891        .enumerate()
892        .map(|(idx, inst)| {
893            // Atlas UV depends on tree type slot
894            let slot = match &inst.kind {
895                VegetationKind::Tree(tt) => *tt as usize,
896                _ => 0,
897            };
898            let u0 = (slot % 4) as f32 * 0.25;
899            let v0 = (slot / 4) as f32 * 0.25;
900            ImpostorBillboard {
901                position: inst.position,
902                size: Vec2::new(4.0 * inst.scale.x, 8.0 * inst.scale.y),
903                lod_level: inst.lod_level,
904                atlas_uv: [u0, v0, u0 + 0.25, v0 + 0.25],
905            }
906        })
907        .collect()
908}
909
910// ── Tests ─────────────────────────────────────────────────────────────────────
911
912#[cfg(test)]
913mod tests {
914    use super::*;
915    use crate::terrain::heightmap::FractalNoise;
916    use crate::terrain::biome::{ClimateSimulator, BiomeMap};
917
918    fn make_test_terrain(size: usize, seed: u64) -> (HeightMap, BiomeMap) {
919        let hm = FractalNoise::generate(size, size, 4, 2.0, 0.5, 3.0, seed);
920        let sim = ClimateSimulator::default();
921        let climate = sim.simulate(&hm);
922        let bm = BiomeMap::from_heightmap(&hm, &climate);
923        (hm, bm)
924    }
925
926    #[test]
927    fn test_tree_params_all_types() {
928        let types = [
929            TreeType::Oak, TreeType::Pine, TreeType::Birch, TreeType::Tropical,
930            TreeType::Dead, TreeType::Palm, TreeType::Willow, TreeType::Cactus,
931            TreeType::Fern, TreeType::Mushroom,
932        ];
933        let mut rng = Rng::new(42);
934        for tt in types {
935            let p = TreeParams::for_type(tt, &mut rng);
936            assert!(p.height > 0.0);
937            assert!(p.crown_radius > 0.0);
938        }
939    }
940
941    #[test]
942    fn test_tree_skeleton_generates_segments() {
943        let mut rng = Rng::new(42);
944        let p = TreeParams::for_type(TreeType::Oak, &mut rng);
945        let skel = TreeSkeleton::generate(TreeType::Oak, &p, 42);
946        assert!(!skel.segments.is_empty());
947    }
948
949    #[test]
950    fn test_tree_skeleton_bounds() {
951        let mut rng = Rng::new(42);
952        let p = TreeParams::for_type(TreeType::Pine, &mut rng);
953        let skel = TreeSkeleton::generate(TreeType::Pine, &p, 42);
954        let (mn, mx) = skel.bounds();
955        assert!(mx.y > mn.y, "tree should have positive height");
956    }
957
958    #[test]
959    fn test_grass_cluster_creation() {
960        let mut rng = Rng::new(42);
961        let center = Vec3::new(5.0, 0.0, 5.0);
962        let cluster = GrassCluster::new(center, 2.0, BiomeType::Grassland, &mut rng);
963        assert!(cluster.blade_height > 0.0);
964        assert!(cluster.density > 0.0);
965    }
966
967    #[test]
968    fn test_grass_field_generation() {
969        let (hm, bm) = make_test_terrain(32, 42);
970        let field = GrassField::generate(&hm, &bm, 1.0, 42);
971        // Should produce some clusters (terrain has mixed land/water)
972        // Just verify it doesn't panic
973        let _ = field.clusters.len();
974    }
975
976    #[test]
977    fn test_rock_cluster_poisson() {
978        let center = Vec3::new(10.0, 0.0, 10.0);
979        let cluster = RockCluster::generate(center, 5.0, BiomeType::Mountain, 10, 99);
980        assert!(!cluster.rocks.is_empty());
981        // All rocks within radius (with tolerance)
982        for rock in &cluster.rocks {
983            let dx = rock.position.x - center.x;
984            let dz = rock.position.z - center.z;
985            assert!(dx * dx + dz * dz <= 5.0 * 5.0 + 0.01);
986        }
987    }
988
989    #[test]
990    fn test_vegetation_lod_distances() {
991        let lod = VegetationLod::for_tree(TreeType::Oak);
992        assert_eq!(lod.lod_for_distance(10.0), 0);
993        assert_eq!(lod.lod_for_distance(lod.lod1_distance + 1.0), 1);
994        assert_eq!(lod.lod_for_distance(lod.billboard_distance + 1.0), 2);
995        assert_eq!(lod.lod_for_distance(lod.cull_distance + 1.0), 3);
996    }
997
998    #[test]
999    fn test_vegetation_system_generation() {
1000        let (hm, bm) = make_test_terrain(32, 42);
1001        let sys = VegetationSystem::generate(&hm, &bm, 1.0, 42);
1002        // Should produce instances
1003        let _ = sys.instances.len();
1004        let _ = sys.grass_field.clusters.len();
1005    }
1006
1007    #[test]
1008    fn test_vegetation_system_lod_update() {
1009        let (hm, bm) = make_test_terrain(32, 42);
1010        let mut sys = VegetationSystem::generate(&hm, &bm, 1.0, 42);
1011        sys.update_lod(Vec3::new(16.0, 50.0, 16.0));
1012        // All instances should have a valid LOD
1013        for inst in &sys.instances {
1014            assert!(inst.lod_level <= 3);
1015        }
1016    }
1017
1018    #[test]
1019    fn test_vegetation_painter_paint() {
1020        let (hm, bm) = make_test_terrain(32, 42);
1021        let mut sys = VegetationSystem::generate(&hm, &bm, 0.1, 42);
1022        let painter = VegetationPainter::new(5.0, 1.0, VegetationKind::Tree(TreeType::Oak));
1023        let before = sys.instances.len();
1024        painter.paint(Vec3::new(16.0, 0.0, 16.0), &mut sys, &hm, 1234);
1025        assert!(sys.instances.len() > before);
1026    }
1027
1028    #[test]
1029    fn test_vegetation_painter_erase() {
1030        let (hm, bm) = make_test_terrain(32, 42);
1031        let mut sys = VegetationSystem::generate(&hm, &bm, 1.0, 42);
1032        let painter = VegetationPainter::new(100.0, 1.0, VegetationKind::Grass);
1033        painter.erase(Vec3::new(16.0, 0.0, 16.0), &mut sys);
1034        // After erasing with huge radius, should be empty
1035        assert_eq!(sys.instances.len(), 0);
1036    }
1037
1038    #[test]
1039    fn test_generate_impostors() {
1040        let mut instances = vec![
1041            VegetationInstance {
1042                position: Vec3::new(5.0, 0.0, 5.0),
1043                rotation: 0.0,
1044                scale: Vec3::ONE,
1045                lod_level: 2,
1046                visible: true,
1047                kind: VegetationKind::Tree(TreeType::Oak),
1048            },
1049            VegetationInstance {
1050                position: Vec3::new(100.0, 0.0, 100.0),
1051                rotation: 0.0,
1052                scale: Vec3::ONE,
1053                lod_level: 2,
1054                visible: true,
1055                kind: VegetationKind::Tree(TreeType::Pine),
1056            },
1057        ];
1058        let billboards = generate_impostors(&instances, Vec3::ZERO, 50.0);
1059        // Only the first (distance 7) is within 50.0
1060        assert_eq!(billboards.len(), 1);
1061    }
1062}
1063
1064// ── Wind System ───────────────────────────────────────────────────────────────
1065
1066/// Models global wind for vegetation animation.
1067#[derive(Clone, Debug)]
1068pub struct WindSystem {
1069    /// Base wind direction and strength.
1070    pub base_wind:    Vec2,
1071    /// Wind gustiness [0, 1]: how much the wind varies.
1072    pub gustiness:    f32,
1073    /// Wind turbulence frequency.
1074    pub turbulence:   f32,
1075    /// Current resolved wind vector.
1076    pub current_wind: Vec2,
1077    /// Internal time.
1078    time:             f32,
1079    /// Gust cycle phase.
1080    gust_phase:       f32,
1081}
1082
1083impl WindSystem {
1084    pub fn new(base_wind: Vec2, gustiness: f32) -> Self {
1085        Self {
1086            base_wind,
1087            gustiness,
1088            turbulence: 0.3,
1089            current_wind: base_wind,
1090            time: 0.0,
1091            gust_phase: 0.0,
1092        }
1093    }
1094
1095    pub fn update(&mut self, dt: f32) {
1096        self.time += dt;
1097        self.gust_phase += dt * 0.3;
1098        // Gust cycle: sine wave modulation of wind strength
1099        let gust_mul = 1.0 + self.gustiness * (self.gust_phase * 0.7).sin()
1100                                            * (self.gust_phase * 1.3).sin();
1101        // Direction variation: slow oscillation
1102        let dir_angle = (self.base_wind.y.atan2(self.base_wind.x))
1103            + (self.time * 0.1).sin() * self.gustiness * 0.4;
1104        let base_speed = self.base_wind.length();
1105        self.current_wind = Vec2::new(
1106            dir_angle.cos() * base_speed * gust_mul,
1107            dir_angle.sin() * base_speed * gust_mul,
1108        );
1109    }
1110
1111    /// Compute local wind at a position (adds turbulence based on position).
1112    pub fn local_wind(&self, pos: Vec2) -> Vec2 {
1113        let turb = self.turbulence;
1114        let phase_x = pos.x * 0.02 + self.time * 0.5;
1115        let phase_y = pos.y * 0.02 + self.time * 0.7;
1116        let turb_x = phase_x.sin() * turb;
1117        let turb_y = phase_y.sin() * turb;
1118        self.current_wind + Vec2::new(turb_x, turb_y) * self.current_wind.length()
1119    }
1120}
1121
1122// ── Vegetation Density Map ────────────────────────────────────────────────────
1123
1124/// A 2D map of vegetation density values.
1125#[derive(Clone, Debug)]
1126pub struct VegetationDensityMap {
1127    pub width:  usize,
1128    pub height: usize,
1129    pub data:   Vec<f32>,
1130}
1131
1132impl VegetationDensityMap {
1133    pub fn new(width: usize, height: usize) -> Self {
1134        Self { width, height, data: vec![0.0; width * height] }
1135    }
1136
1137    pub fn get(&self, x: usize, y: usize) -> f32 {
1138        if x < self.width && y < self.height { self.data[y * self.width + x] } else { 0.0 }
1139    }
1140
1141    pub fn set(&mut self, x: usize, y: usize, v: f32) {
1142        if x < self.width && y < self.height {
1143            self.data[y * self.width + x] = v.clamp(0.0, 1.0);
1144        }
1145    }
1146
1147    /// Build density map from heightmap and biome map for trees.
1148    pub fn for_trees(
1149        heightmap: &crate::terrain::heightmap::HeightMap,
1150        biome_map: &BiomeMap,
1151        slope_map: &crate::terrain::heightmap::HeightMap,
1152    ) -> Self {
1153        let w = heightmap.width;
1154        let h = heightmap.height;
1155        let mut dm = Self::new(w, h);
1156        for y in 0..h {
1157            for x in 0..w {
1158                let alt   = heightmap.get(x, y);
1159                let slope = slope_map.get(x, y);
1160                let biome = biome_map.get(x, y);
1161                if alt < 0.1 || slope > 0.6 { continue; }
1162                let base = VegetationDensity::for_biome(biome).tree_density;
1163                // Altitude penalty: fewer trees at high altitude
1164                let alt_factor = if alt > 0.7 { 1.0 - (alt - 0.7) / 0.3 } else { 1.0 };
1165                // Slope penalty: fewer trees on steep slopes
1166                let slope_factor = (1.0 - slope / 0.6).max(0.0);
1167                dm.set(x, y, base * alt_factor * slope_factor);
1168            }
1169        }
1170        dm
1171    }
1172
1173    /// Build density map for grass.
1174    pub fn for_grass(
1175        heightmap: &crate::terrain::heightmap::HeightMap,
1176        biome_map: &BiomeMap,
1177    ) -> Self {
1178        let w = heightmap.width;
1179        let h = heightmap.height;
1180        let mut dm = Self::new(w, h);
1181        for y in 0..h {
1182            for x in 0..w {
1183                let alt   = heightmap.get(x, y);
1184                let biome = biome_map.get(x, y);
1185                if alt < 0.08 { continue; }
1186                let base = VegetationDensity::for_biome(biome).grass_density;
1187                // Grass prefers moderate altitude
1188                let alt_factor = if alt > 0.8 { 0.1 } else if alt > 0.6 { 0.5 } else { 1.0 };
1189                dm.set(x, y, base * alt_factor);
1190            }
1191        }
1192        dm
1193    }
1194
1195    /// Sample with bilinear interpolation.
1196    pub fn sample_bilinear(&self, x: f32, y: f32) -> f32 {
1197        let cx = x.clamp(0.0, (self.width  - 1) as f32);
1198        let cy = y.clamp(0.0, (self.height - 1) as f32);
1199        let x0 = cx.floor() as usize;
1200        let y0 = cy.floor() as usize;
1201        let x1 = (x0 + 1).min(self.width  - 1);
1202        let y1 = (y0 + 1).min(self.height - 1);
1203        let tx = cx - x0 as f32;
1204        let ty = cy - y0 as f32;
1205        let h00 = self.get(x0, y0);
1206        let h10 = self.get(x1, y0);
1207        let h01 = self.get(x0, y1);
1208        let h11 = self.get(x1, y1);
1209        let lerp = |a: f32, b: f32, t: f32| a + t * (b - a);
1210        lerp(lerp(h00, h10, tx), lerp(h01, h11, tx), ty)
1211    }
1212}
1213
1214// ── Vegetation Cluster ────────────────────────────────────────────────────────
1215
1216/// A group of same-species instances sharing a spatial cluster for efficient culling.
1217#[derive(Debug, Clone)]
1218pub struct VegetationCluster {
1219    pub center:     Vec3,
1220    pub radius:     f32,
1221    pub instances:  Vec<usize>,  // indices into VegetationSystem.instances
1222    pub kind:       VegetationKind,
1223    pub lod_level:  u8,
1224}
1225
1226impl VegetationCluster {
1227    pub fn new(center: Vec3, radius: f32, kind: VegetationKind) -> Self {
1228        Self { center, radius, instances: Vec::new(), kind, lod_level: 0 }
1229    }
1230
1231    pub fn contains_point(&self, p: Vec3) -> bool {
1232        let dx = p.x - self.center.x;
1233        let dz = p.z - self.center.z;
1234        dx * dx + dz * dz <= self.radius * self.radius
1235    }
1236
1237    pub fn distance_to(&self, p: Vec3) -> f32 {
1238        let dx = p.x - self.center.x;
1239        let dz = p.z - self.center.z;
1240        (dx * dx + dz * dz).sqrt()
1241    }
1242}
1243
1244// ── VegetationAtlas ───────────────────────────────────────────────────────────
1245
1246/// Manages texture atlas slots for vegetation impostor billboards.
1247#[derive(Debug, Clone)]
1248pub struct VegetationAtlas {
1249    /// Width and height of the atlas in pixels.
1250    pub resolution: u32,
1251    /// Number of slots per row.
1252    pub slots_per_row: u32,
1253    /// Slot size (in atlas pixels).
1254    pub slot_size: u32,
1255    /// Mapping from tree type to atlas slot index.
1256    pub slot_map: std::collections::HashMap<String, u32>,
1257    /// Next available slot.
1258    pub next_slot: u32,
1259}
1260
1261impl VegetationAtlas {
1262    pub fn new(resolution: u32, slots_per_row: u32) -> Self {
1263        Self {
1264            resolution,
1265            slots_per_row,
1266            slot_size: resolution / slots_per_row,
1267            slot_map: std::collections::HashMap::new(),
1268            next_slot: 0,
1269        }
1270    }
1271
1272    /// Allocate an atlas slot for a named vegetation type.
1273    pub fn allocate_slot(&mut self, name: &str) -> Option<u32> {
1274        let max_slots = self.slots_per_row * self.slots_per_row;
1275        if self.next_slot >= max_slots { return None; }
1276        let slot = self.next_slot;
1277        self.slot_map.insert(name.to_string(), slot);
1278        self.next_slot += 1;
1279        Some(slot)
1280    }
1281
1282    /// Get UV coordinates for a given slot.
1283    pub fn slot_uv(&self, slot: u32) -> [f32; 4] {
1284        let row = slot / self.slots_per_row;
1285        let col = slot % self.slots_per_row;
1286        let sz = 1.0 / self.slots_per_row as f32;
1287        let u0 = col as f32 * sz;
1288        let v0 = row as f32 * sz;
1289        [u0, v0, u0 + sz, v0 + sz]
1290    }
1291
1292    /// Get UV for a named type (returns default if not found).
1293    pub fn get_uv(&self, name: &str) -> [f32; 4] {
1294        let slot = self.slot_map.get(name).copied().unwrap_or(0);
1295        self.slot_uv(slot)
1296    }
1297}
1298
1299// ── Forest Generator ──────────────────────────────────────────────────────────
1300
1301/// Specialized forest generator that creates realistic forest patterns.
1302pub struct ForestGenerator {
1303    pub min_tree_spacing: f32,
1304    pub edge_density:     f32,  // trees are denser at forest edges
1305    pub clustering:       f32,  // 0 = uniform, 1 = highly clustered
1306}
1307
1308impl Default for ForestGenerator {
1309    fn default() -> Self {
1310        Self {
1311            min_tree_spacing: 2.0,
1312            edge_density: 1.5,
1313            clustering: 0.4,
1314        }
1315    }
1316}
1317
1318impl ForestGenerator {
1319    pub fn new(min_spacing: f32, clustering: f32) -> Self {
1320        Self { min_tree_spacing: min_spacing, edge_density: 1.5, clustering }
1321    }
1322
1323    /// Generate tree positions in a forest region using clustered Poisson disk sampling.
1324    pub fn generate_positions(
1325        &self,
1326        density_map: &VegetationDensityMap,
1327        heightmap: &crate::terrain::heightmap::HeightMap,
1328        biome_map: &BiomeMap,
1329        seed: u64,
1330    ) -> Vec<Vec3> {
1331        let mut rng = Rng::new(seed);
1332        let mut positions: Vec<Vec3> = Vec::new();
1333        let w = density_map.width;
1334        let h = density_map.height;
1335
1336        // Generate candidate positions on a grid with jitter
1337        let grid_step = (self.min_tree_spacing * 0.8) as usize + 1;
1338        for y in (0..h).step_by(grid_step) {
1339            for x in (0..w).step_by(grid_step) {
1340                let density = density_map.get(x, y);
1341                if density < rng.next_f32() { continue; }
1342
1343                // Add spatial jitter
1344                let jx = rng.next_f32_range(-(grid_step as f32 * 0.4), grid_step as f32 * 0.4);
1345                let jz = rng.next_f32_range(-(grid_step as f32 * 0.4), grid_step as f32 * 0.4);
1346                let px = (x as f32 + jx).clamp(0.0, w as f32 - 1.0);
1347                let pz = (y as f32 + jz).clamp(0.0, h as f32 - 1.0);
1348
1349                // Check min spacing
1350                let too_close = positions.iter().any(|p| {
1351                    let dx = p.x - px;
1352                    let dz = p.z - pz;
1353                    dx * dx + dz * dz < self.min_tree_spacing * self.min_tree_spacing
1354                });
1355                if too_close { continue; }
1356
1357                let alt = heightmap.get(x, y);
1358                positions.push(Vec3::new(px, alt * 100.0, pz));
1359            }
1360        }
1361
1362        // Clustering: move some trees toward existing cluster centers
1363        if self.clustering > 0.0 {
1364            let cluster_radius = self.min_tree_spacing * 4.0;
1365            let n = positions.len();
1366            for i in 0..n {
1367                if rng.next_f32() < self.clustering {
1368                    // Find a nearby position to cluster toward
1369                    let target_idx = rng.next_usize(n);
1370                    if target_idx == i { continue; }
1371                    let tp = positions[target_idx];
1372                    let dx = tp.x - positions[i].x;
1373                    let dz = tp.z - positions[i].z;
1374                    let dist = (dx * dx + dz * dz).sqrt();
1375                    if dist < cluster_radius && dist > self.min_tree_spacing {
1376                        let move_frac = self.clustering * 0.3;
1377                        positions[i].x += dx * move_frac;
1378                        positions[i].z += dz * move_frac;
1379                    }
1380                }
1381            }
1382        }
1383
1384        positions
1385    }
1386}
1387
1388// ── Snow Accumulation ─────────────────────────────────────────────────────────
1389
1390/// Computes snow accumulation on vegetation based on slope and temperature.
1391pub struct SnowAccumulation;
1392
1393impl SnowAccumulation {
1394    /// Compute snow coverage [0, 1] for a vegetation instance.
1395    /// `temperature` is normalized (0 = freezing, 1 = hot).
1396    /// `slope_normal` is the surface normal at the instance position.
1397    pub fn coverage(temperature: f32, slope_normal: Vec3, altitude: f32) -> f32 {
1398        if temperature > 0.35 { return 0.0; }
1399        // Cold factor: how cold is it?
1400        let cold = (0.35 - temperature) / 0.35;
1401        // Vertical surface receives less snow
1402        let vertical_factor = slope_normal.y.clamp(0.0, 1.0);
1403        // Higher altitude = more snow
1404        let alt_factor = if altitude > 0.7 { 1.0 } else { altitude / 0.7 };
1405        (cold * vertical_factor * (0.5 + 0.5 * alt_factor)).clamp(0.0, 1.0)
1406    }
1407
1408    /// Apply snow tinting to a vegetation instance's color.
1409    pub fn apply_tint(base_color: Vec4, snow_coverage: f32) -> Vec4 {
1410        let snow_color = Vec4::new(0.92, 0.95, 1.0, 1.0);
1411        Vec4::new(
1412            base_color.x + (snow_color.x - base_color.x) * snow_coverage,
1413            base_color.y + (snow_color.y - base_color.y) * snow_coverage,
1414            base_color.z + (snow_color.z - base_color.z) * snow_coverage,
1415            base_color.w,
1416        )
1417    }
1418}
1419
1420// ── Leaf Color System ─────────────────────────────────────────────────────────
1421
1422/// Computes leaf colors based on tree type, season, and variation.
1423pub struct LeafColorSystem;
1424
1425impl LeafColorSystem {
1426    /// Base leaf color for a tree type.
1427    pub fn base_color(tt: TreeType) -> Vec3 {
1428        match tt {
1429            TreeType::Oak      => Vec3::new(0.2, 0.5,  0.1),
1430            TreeType::Pine     => Vec3::new(0.1, 0.35, 0.08),
1431            TreeType::Birch    => Vec3::new(0.35, 0.6, 0.15),
1432            TreeType::Tropical => Vec3::new(0.1, 0.5,  0.05),
1433            TreeType::Dead     => Vec3::new(0.4, 0.3,  0.15),
1434            TreeType::Palm     => Vec3::new(0.15, 0.5, 0.1),
1435            TreeType::Willow   => Vec3::new(0.25, 0.55, 0.12),
1436            TreeType::Cactus   => Vec3::new(0.2, 0.45, 0.1),
1437            TreeType::Fern     => Vec3::new(0.15, 0.55, 0.1),
1438            TreeType::Mushroom => Vec3::new(0.7, 0.3,  0.1),
1439        }
1440    }
1441
1442    /// Seasonal leaf color: spring=bright green, summer=deep green,
1443    ///                      autumn=orange/red, winter=bare/brown.
1444    pub fn seasonal_color(tt: TreeType, month: u32, variation: f32) -> Vec3 {
1445        let base = Self::base_color(tt);
1446        let m = (month % 12) as f32;
1447        // Summer peak at month 6, winter at 0/12
1448        let summer_t = ((m - 6.0) * std::f32::consts::PI / 6.0).cos() * 0.5 + 0.5;
1449        let autumn_t = {
1450            // Autumn: Sept-Nov (months 8-10)
1451            if m >= 8.0 && m <= 11.0 {
1452                ((m - 8.0) / 3.0).min(1.0)
1453            } else { 0.0 }
1454        };
1455        let dead_trees = matches!(tt, TreeType::Dead);
1456        if dead_trees {
1457            return Vec3::new(0.35, 0.25, 0.1);
1458        }
1459        let evergreen = matches!(tt, TreeType::Pine | TreeType::Fern | TreeType::Cactus | TreeType::Tropical | TreeType::Palm);
1460        if evergreen {
1461            return base * (0.8 + 0.2 * summer_t);
1462        }
1463        // Deciduous: green summer → orange/red autumn → brown winter
1464        let autumn_color = Vec3::new(0.8 + variation * 0.1, 0.3 + variation * 0.1, 0.05);
1465        let winter_color = Vec3::new(0.3, 0.2, 0.1);
1466        let spring_color = Vec3::new(base.x * 1.2, base.y * 1.3, base.z * 1.0);
1467        let summer_color = base;
1468
1469        if m < 3.0 {
1470            // Winter
1471            winter_color * (0.3 + summer_t * 0.2)
1472        } else if m < 5.0 {
1473            // Spring
1474            let t = (m - 3.0) / 2.0;
1475            winter_color + (spring_color - winter_color) * t
1476        } else if m < 8.0 {
1477            // Summer
1478            spring_color + (summer_color - spring_color) * ((m - 5.0) / 3.0)
1479        } else {
1480            // Autumn → Winter
1481            summer_color + (autumn_color - summer_color) * autumn_t
1482        }
1483    }
1484}
1485
1486// ── Undergrowth System ────────────────────────────────────────────────────────
1487
1488/// Generates undergrowth (ferns, mushrooms, flowers) beneath forest canopy.
1489pub struct UndergrowthSystem;
1490
1491impl UndergrowthSystem {
1492    /// Generate undergrowth instances beneath existing tree canopies.
1493    pub fn generate_under_canopy(
1494        tree_instances: &[VegetationInstance],
1495        heightmap: &crate::terrain::heightmap::HeightMap,
1496        seed: u64,
1497    ) -> Vec<VegetationInstance> {
1498        let mut rng = Rng::new(seed);
1499        let mut out = Vec::new();
1500        for tree in tree_instances {
1501            if !matches!(tree.kind, VegetationKind::Tree(_)) { continue; }
1502            let canopy_r = tree.scale.x * 3.0;
1503            let count = (canopy_r * canopy_r * 0.5) as usize + 1;
1504            for _ in 0..count {
1505                let angle = rng.next_f32() * std::f32::consts::TAU;
1506                let dist  = rng.next_f32() * canopy_r;
1507                let px = tree.position.x + angle.cos() * dist;
1508                let pz = tree.position.z + angle.sin() * dist;
1509                let xi = (px as usize).min(heightmap.width  - 1);
1510                let zi = (pz as usize).min(heightmap.height - 1);
1511                let alt = heightmap.get(xi, zi);
1512                // Under canopy: spawn ferns or mushrooms
1513                let kind = if rng.next_f32() < 0.7 {
1514                    VegetationKind::Tree(TreeType::Fern)
1515                } else {
1516                    VegetationKind::Tree(TreeType::Mushroom)
1517                };
1518                out.push(VegetationInstance {
1519                    position: Vec3::new(px, alt * 100.0, pz),
1520                    rotation: rng.next_f32() * std::f32::consts::TAU,
1521                    scale: Vec3::splat(rng.next_f32_range(0.2, 0.5)),
1522                    lod_level: 0,
1523                    visible: true,
1524                    kind,
1525                });
1526            }
1527        }
1528        out
1529    }
1530}
1531
1532// ── Extended Vegetation Tests ─────────────────────────────────────────────────
1533
1534#[cfg(test)]
1535mod extended_vegetation_tests {
1536    use super::*;
1537    use crate::terrain::heightmap::FractalNoise;
1538    use crate::terrain::biome::{ClimateSimulator, BiomeMap};
1539
1540    fn make_terrain(size: usize, seed: u64) -> (crate::terrain::heightmap::HeightMap, BiomeMap) {
1541        let hm = FractalNoise::generate(size, size, 4, 2.0, 0.5, 3.0, seed);
1542        let sim = ClimateSimulator::default();
1543        let climate = sim.simulate(&hm);
1544        let bm = BiomeMap::from_heightmap(&hm, &climate);
1545        (hm, bm)
1546    }
1547
1548    #[test]
1549    fn test_wind_system_update() {
1550        let mut wind = WindSystem::new(Vec2::new(1.0, 0.0), 0.3);
1551        wind.update(0.016);
1552        // After update, wind should be valid
1553        assert!(wind.current_wind.length() >= 0.0);
1554    }
1555
1556    #[test]
1557    fn test_wind_system_local() {
1558        let wind = WindSystem::new(Vec2::new(2.0, 1.0), 0.3);
1559        let local = wind.local_wind(Vec2::new(10.0, 20.0));
1560        assert!(local.length() > 0.0);
1561    }
1562
1563    #[test]
1564    fn test_vegetation_density_map_for_trees() {
1565        let (hm, bm) = make_terrain(32, 42);
1566        let slope = hm.slope_map();
1567        let dm = VegetationDensityMap::for_trees(&hm, &bm, &slope);
1568        assert_eq!(dm.data.len(), 32 * 32);
1569        assert!(dm.data.iter().all(|&v| v >= 0.0 && v <= 1.0));
1570    }
1571
1572    #[test]
1573    fn test_vegetation_density_map_for_grass() {
1574        let (hm, bm) = make_terrain(32, 42);
1575        let dm = VegetationDensityMap::for_grass(&hm, &bm);
1576        assert_eq!(dm.data.len(), 32 * 32);
1577    }
1578
1579    #[test]
1580    fn test_forest_generator() {
1581        let (hm, bm) = make_terrain(32, 42);
1582        let slope = hm.slope_map();
1583        let dm = VegetationDensityMap::for_trees(&hm, &bm, &slope);
1584        let fg = ForestGenerator::new(2.0, 0.3);
1585        let positions = fg.generate_positions(&dm, &hm, &bm, 42);
1586        // Should generate some positions
1587        let _ = positions.len();
1588    }
1589
1590    #[test]
1591    fn test_vegetation_cluster() {
1592        let c = VegetationCluster::new(Vec3::new(10.0, 0.0, 10.0), 5.0, VegetationKind::Grass);
1593        assert!(c.contains_point(Vec3::new(10.0, 0.0, 10.0)));
1594        assert!(!c.contains_point(Vec3::new(100.0, 0.0, 100.0)));
1595        assert!((c.distance_to(Vec3::new(10.0, 0.0, 15.0)) - 5.0).abs() < 0.01);
1596    }
1597
1598    #[test]
1599    fn test_vegetation_atlas() {
1600        let mut atlas = VegetationAtlas::new(512, 4);
1601        let slot = atlas.allocate_slot("Oak");
1602        assert!(slot.is_some());
1603        let uv = atlas.get_uv("Oak");
1604        assert!(uv[0] >= 0.0 && uv[2] <= 1.0);
1605    }
1606
1607    #[test]
1608    fn test_snow_accumulation() {
1609        let cold_coverage = SnowAccumulation::coverage(0.1, Vec3::Y, 0.8);
1610        assert!(cold_coverage > 0.0);
1611        let warm_coverage = SnowAccumulation::coverage(0.8, Vec3::Y, 0.5);
1612        assert_eq!(warm_coverage, 0.0);
1613    }
1614
1615    #[test]
1616    fn test_leaf_color_seasonal() {
1617        let summer = LeafColorSystem::seasonal_color(TreeType::Oak, 6, 0.0);
1618        let winter = LeafColorSystem::seasonal_color(TreeType::Oak, 1, 0.0);
1619        // Summer should be greener
1620        assert!(summer.y > winter.y);
1621        // Pine is evergreen: small variation
1622        let summer_pine = LeafColorSystem::seasonal_color(TreeType::Pine, 6, 0.0);
1623        let winter_pine = LeafColorSystem::seasonal_color(TreeType::Pine, 1, 0.0);
1624        assert!((summer_pine.y - winter_pine.y).abs() < 0.3);
1625    }
1626
1627    #[test]
1628    fn test_undergrowth_generation() {
1629        let (hm, _) = make_terrain(32, 42);
1630        let trees = vec![
1631            VegetationInstance {
1632                position: Vec3::new(16.0, 50.0, 16.0),
1633                rotation: 0.0,
1634                scale: Vec3::splat(2.0),
1635                lod_level: 0,
1636                visible: true,
1637                kind: VegetationKind::Tree(TreeType::Oak),
1638            },
1639        ];
1640        let undergrowth = UndergrowthSystem::generate_under_canopy(&trees, &hm, 42);
1641        assert!(!undergrowth.is_empty());
1642    }
1643
1644    #[test]
1645    fn test_all_tree_types_have_base_color() {
1646        let types = [
1647            TreeType::Oak, TreeType::Pine, TreeType::Birch, TreeType::Tropical,
1648            TreeType::Dead, TreeType::Palm, TreeType::Willow, TreeType::Cactus,
1649            TreeType::Fern, TreeType::Mushroom,
1650        ];
1651        for tt in types {
1652            let c = LeafColorSystem::base_color(tt);
1653            assert!(c.x >= 0.0 && c.y >= 0.0 && c.z >= 0.0);
1654        }
1655    }
1656}
1657
1658// ── Grass Simulation ──────────────────────────────────────────────────────────
1659
1660/// Per-blade grass animation state.
1661#[derive(Clone, Debug)]
1662pub struct GrassBlade {
1663    pub position:     Vec3,
1664    pub base_angle:   f32,   // natural lean angle in radians
1665    pub sway_phase:   f32,   // individual phase offset for wind sway
1666    pub height:       f32,
1667    pub width:        f32,
1668    pub color:        Vec4,
1669    pub roughness:    f32,
1670}
1671
1672impl GrassBlade {
1673    pub fn new(position: Vec3, height: f32, color: Vec4, seed: u64) -> Self {
1674        let mut rng = Rng::new(seed);
1675        Self {
1676            position,
1677            base_angle: rng.next_f32_range(-0.2, 0.2),
1678            sway_phase: rng.next_f32() * std::f32::consts::TAU,
1679            height,
1680            width: rng.next_f32_range(0.02, 0.05),
1681            color,
1682            roughness: rng.next_f32_range(0.7, 1.0),
1683        }
1684    }
1685
1686    /// Compute current sway angle given time and wind.
1687    pub fn current_angle(&self, time: f32, wind: Vec2) -> f32 {
1688        let wind_strength = wind.length();
1689        let sway = wind_strength * (time * 1.5 + self.sway_phase).sin() * 0.3;
1690        self.base_angle + sway
1691    }
1692
1693    /// Tip position after sway.
1694    pub fn tip_position(&self, time: f32, wind: Vec2) -> Vec3 {
1695        let angle = self.current_angle(time, wind);
1696        self.position + Vec3::new(
1697            angle.sin() * self.height,
1698            angle.cos() * self.height,
1699            (angle * 0.7).sin() * self.height * 0.3,
1700        )
1701    }
1702}
1703
1704/// A patch of individually simulated grass blades.
1705#[derive(Debug)]
1706pub struct GrassPatch {
1707    pub blades:   Vec<GrassBlade>,
1708    pub center:   Vec3,
1709    pub radius:   f32,
1710}
1711
1712impl GrassPatch {
1713    pub fn generate(center: Vec3, radius: f32, density: f32, biome: BiomeType, seed: u64) -> Self {
1714        let mut rng = Rng::new(seed);
1715        let count = (radius * radius * std::f32::consts::PI * density * 4.0) as usize;
1716        let color = match biome {
1717            BiomeType::Grassland | BiomeType::TemperateForest =>
1718                Vec4::new(0.3 + rng.next_f32() * 0.1, 0.6, 0.15, 1.0),
1719            BiomeType::Savanna =>
1720                Vec4::new(0.65 + rng.next_f32() * 0.1, 0.55, 0.12, 1.0),
1721            BiomeType::Tundra =>
1722                Vec4::new(0.5, 0.52, 0.32, 1.0),
1723            _ => Vec4::new(0.3, 0.55, 0.15, 1.0),
1724        };
1725        let blades: Vec<GrassBlade> = (0..count).map(|_| {
1726            let angle = rng.next_f32() * std::f32::consts::TAU;
1727            let dist  = rng.next_f32() * radius;
1728            let px = center.x + angle.cos() * dist;
1729            let pz = center.z + angle.sin() * dist;
1730            let height = rng.next_f32_range(0.15, 0.5);
1731            let col = Vec4::new(
1732                (color.x + rng.next_f32_range(-0.05, 0.05)).clamp(0.0, 1.0),
1733                (color.y + rng.next_f32_range(-0.05, 0.05)).clamp(0.0, 1.0),
1734                (color.z + rng.next_f32_range(-0.05, 0.05)).clamp(0.0, 1.0),
1735                1.0,
1736            );
1737            GrassBlade::new(Vec3::new(px, center.y, pz), height, col, rng.next_u64())
1738        }).collect();
1739        Self { blades, center, radius }
1740    }
1741
1742    pub fn blade_count(&self) -> usize { self.blades.len() }
1743}
1744
1745// ── Vegetation Query API ──────────────────────────────────────────────────────
1746
1747/// Query API for finding vegetation near a position.
1748pub struct VegetationQuery<'a> {
1749    system: &'a VegetationSystem,
1750}
1751
1752impl<'a> VegetationQuery<'a> {
1753    pub fn new(system: &'a VegetationSystem) -> Self { Self { system } }
1754
1755    /// Find all visible trees within `radius` of `pos`.
1756    pub fn trees_near(&self, pos: Vec3, radius: f32) -> Vec<&VegetationInstance> {
1757        let r2 = radius * radius;
1758        self.system.instances.iter()
1759            .filter(|i| i.visible && matches!(i.kind, VegetationKind::Tree(_)))
1760            .filter(|i| {
1761                let dx = i.position.x - pos.x;
1762                let dz = i.position.z - pos.z;
1763                dx * dx + dz * dz <= r2
1764            })
1765            .collect()
1766    }
1767
1768    /// Find the nearest tree to `pos`.
1769    pub fn nearest_tree(&self, pos: Vec3) -> Option<&VegetationInstance> {
1770        self.system.instances.iter()
1771            .filter(|i| i.visible && matches!(i.kind, VegetationKind::Tree(_)))
1772            .min_by(|a, b| {
1773                let da = (a.position - pos).length();
1774                let db = (b.position - pos).length();
1775                da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
1776            })
1777    }
1778
1779    /// Count instances of each kind within radius.
1780    pub fn count_by_kind(&self, pos: Vec3, radius: f32) -> std::collections::HashMap<String, usize> {
1781        let r2 = radius * radius;
1782        let mut counts = std::collections::HashMap::new();
1783        for inst in &self.system.instances {
1784            if !inst.visible { continue; }
1785            let dx = inst.position.x - pos.x;
1786            let dz = inst.position.z - pos.z;
1787            if dx * dx + dz * dz > r2 { continue; }
1788            let key = match &inst.kind {
1789                VegetationKind::Tree(tt) => tt.name().to_string(),
1790                VegetationKind::Grass   => "Grass".to_string(),
1791                VegetationKind::Rock { size_class } => format!("Rock({})", size_class),
1792                VegetationKind::Shrub   => "Shrub".to_string(),
1793                VegetationKind::Flower  => "Flower".to_string(),
1794            };
1795            *counts.entry(key).or_insert(0) += 1;
1796        }
1797        counts
1798    }
1799
1800    /// Find all rocks within radius.
1801    pub fn rocks_near(&self, pos: Vec3, radius: f32) -> Vec<&VegetationInstance> {
1802        let r2 = radius * radius;
1803        self.system.instances.iter()
1804            .filter(|i| matches!(i.kind, VegetationKind::Rock { .. }))
1805            .filter(|i| {
1806                let dx = i.position.x - pos.x;
1807                let dz = i.position.z - pos.z;
1808                dx * dx + dz * dz <= r2
1809            })
1810            .collect()
1811    }
1812}
1813
1814// ── Vegetation Serializer ─────────────────────────────────────────────────────
1815
1816/// Serializes and deserializes vegetation data for saving/loading.
1817pub struct VegetationSerializer;
1818
1819impl VegetationSerializer {
1820    /// Serialize vegetation instances to compact binary.
1821    /// Format per instance: [x:f32][y:f32][z:f32][rotation:f32][scale_x:f32][scale_y:f32][scale_z:f32][kind:u8][lod:u8]
1822    pub fn serialize(instances: &[VegetationInstance]) -> Vec<u8> {
1823        let mut out = Vec::with_capacity(instances.len() * 36 + 4);
1824        out.extend_from_slice(&(instances.len() as u32).to_le_bytes());
1825        for inst in instances {
1826            out.extend_from_slice(&inst.position.x.to_le_bytes());
1827            out.extend_from_slice(&inst.position.y.to_le_bytes());
1828            out.extend_from_slice(&inst.position.z.to_le_bytes());
1829            out.extend_from_slice(&inst.rotation.to_le_bytes());
1830            out.extend_from_slice(&inst.scale.x.to_le_bytes());
1831            out.extend_from_slice(&inst.scale.y.to_le_bytes());
1832            out.extend_from_slice(&inst.scale.z.to_le_bytes());
1833            let kind_byte: u8 = match &inst.kind {
1834                VegetationKind::Tree(tt) => *tt as u8,
1835                VegetationKind::Grass    => 20,
1836                VegetationKind::Rock { size_class } => 21 + size_class,
1837                VegetationKind::Shrub    => 24,
1838                VegetationKind::Flower   => 25,
1839            };
1840            out.push(kind_byte);
1841            out.push(inst.lod_level);
1842        }
1843        out
1844    }
1845
1846    /// Deserialize vegetation instances from binary.
1847    pub fn deserialize(bytes: &[u8]) -> Option<Vec<VegetationInstance>> {
1848        if bytes.len() < 4 { return None; }
1849        let count = u32::from_le_bytes(bytes[0..4].try_into().ok()?) as usize;
1850        let record_size = 4 * 7 + 2; // 7 floats + 2 bytes
1851        if bytes.len() < 4 + count * record_size { return None; }
1852        let mut instances = Vec::with_capacity(count);
1853        let mut pos = 4usize;
1854        for _ in 0..count {
1855            let read_f32 = |p: &mut usize| -> f32 {
1856                let v = f32::from_le_bytes(bytes[*p..*p+4].try_into().unwrap_or([0;4]));
1857                *p += 4;
1858                v
1859            };
1860            let x  = read_f32(&mut pos);
1861            let y  = read_f32(&mut pos);
1862            let z  = read_f32(&mut pos);
1863            let rot = read_f32(&mut pos);
1864            let sx  = read_f32(&mut pos);
1865            let sy  = read_f32(&mut pos);
1866            let sz  = read_f32(&mut pos);
1867            let kind_byte = bytes[pos]; pos += 1;
1868            let lod = bytes[pos]; pos += 1;
1869            let kind = match kind_byte {
1870                0  => VegetationKind::Tree(TreeType::Oak),
1871                1  => VegetationKind::Tree(TreeType::Pine),
1872                2  => VegetationKind::Tree(TreeType::Birch),
1873                3  => VegetationKind::Tree(TreeType::Tropical),
1874                4  => VegetationKind::Tree(TreeType::Dead),
1875                5  => VegetationKind::Tree(TreeType::Palm),
1876                6  => VegetationKind::Tree(TreeType::Willow),
1877                7  => VegetationKind::Tree(TreeType::Cactus),
1878                8  => VegetationKind::Tree(TreeType::Fern),
1879                9  => VegetationKind::Tree(TreeType::Mushroom),
1880                20 => VegetationKind::Grass,
1881                21 => VegetationKind::Rock { size_class: 0 },
1882                22 => VegetationKind::Rock { size_class: 1 },
1883                23 => VegetationKind::Rock { size_class: 2 },
1884                24 => VegetationKind::Shrub,
1885                _  => VegetationKind::Flower,
1886            };
1887            instances.push(VegetationInstance {
1888                position: Vec3::new(x, y, z),
1889                rotation: rot,
1890                scale: Vec3::new(sx, sy, sz),
1891                lod_level: lod,
1892                visible: true,
1893                kind,
1894            });
1895        }
1896        Some(instances)
1897    }
1898}
1899
1900// ── More Vegetation Tests ─────────────────────────────────────────────────────
1901
1902#[cfg(test)]
1903mod more_vegetation_tests {
1904    use super::*;
1905
1906    #[test]
1907    fn test_grass_blade_creation() {
1908        let blade = GrassBlade::new(Vec3::new(1.0, 0.0, 1.0), 0.4, Vec4::ONE, 42);
1909        assert!(blade.height > 0.0);
1910        assert!(blade.width > 0.0);
1911    }
1912
1913    #[test]
1914    fn test_grass_blade_sway() {
1915        let blade = GrassBlade::new(Vec3::ZERO, 0.5, Vec4::ONE, 99);
1916        let angle_t0 = blade.current_angle(0.0, Vec2::new(1.0, 0.0));
1917        let angle_t1 = blade.current_angle(1.0, Vec2::new(1.0, 0.0));
1918        // Angle should change over time (sway)
1919        let _ = (angle_t0, angle_t1);
1920    }
1921
1922    #[test]
1923    fn test_grass_blade_tip() {
1924        let blade = GrassBlade::new(Vec3::ZERO, 0.5, Vec4::ONE, 42);
1925        let tip = blade.tip_position(0.0, Vec2::ZERO);
1926        assert!(tip.y > 0.0); // tip should be above base
1927    }
1928
1929    #[test]
1930    fn test_grass_patch_generation() {
1931        let patch = GrassPatch::generate(Vec3::ZERO, 5.0, 1.0, BiomeType::Grassland, 42);
1932        assert!(!patch.blades.is_empty());
1933        for blade in &patch.blades {
1934            let dx = blade.position.x;
1935            let dz = blade.position.z;
1936            assert!(dx * dx + dz * dz <= 5.0 * 5.0 + 0.1);
1937        }
1938    }
1939
1940    #[test]
1941    fn test_vegetation_query_trees_near() {
1942        let mut sys = VegetationSystem::new();
1943        sys.instances.push(VegetationInstance {
1944            position: Vec3::new(5.0, 0.0, 0.0),
1945            rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true,
1946            kind: VegetationKind::Tree(TreeType::Oak),
1947        });
1948        sys.instances.push(VegetationInstance {
1949            position: Vec3::new(50.0, 0.0, 0.0),
1950            rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true,
1951            kind: VegetationKind::Tree(TreeType::Pine),
1952        });
1953        let q = VegetationQuery::new(&sys);
1954        let near = q.trees_near(Vec3::ZERO, 10.0);
1955        assert_eq!(near.len(), 1);
1956    }
1957
1958    #[test]
1959    fn test_vegetation_query_nearest() {
1960        let mut sys = VegetationSystem::new();
1961        sys.instances.push(VegetationInstance {
1962            position: Vec3::new(3.0, 0.0, 0.0),
1963            rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true,
1964            kind: VegetationKind::Tree(TreeType::Oak),
1965        });
1966        sys.instances.push(VegetationInstance {
1967            position: Vec3::new(10.0, 0.0, 0.0),
1968            rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true,
1969            kind: VegetationKind::Tree(TreeType::Pine),
1970        });
1971        let q = VegetationQuery::new(&sys);
1972        let nearest = q.nearest_tree(Vec3::ZERO);
1973        assert!(nearest.is_some());
1974        assert!((nearest.unwrap().position.x - 3.0).abs() < 1e-4);
1975    }
1976
1977    #[test]
1978    fn test_vegetation_serializer_roundtrip() {
1979        let instances = vec![
1980            VegetationInstance {
1981                position: Vec3::new(1.0, 2.0, 3.0),
1982                rotation: 1.5,
1983                scale: Vec3::new(1.0, 1.5, 1.0),
1984                lod_level: 0,
1985                visible: true,
1986                kind: VegetationKind::Tree(TreeType::Oak),
1987            },
1988            VegetationInstance {
1989                position: Vec3::new(5.0, 0.0, 7.0),
1990                rotation: 0.5,
1991                scale: Vec3::ONE,
1992                lod_level: 1,
1993                visible: true,
1994                kind: VegetationKind::Rock { size_class: 1 },
1995            },
1996        ];
1997        let bytes = VegetationSerializer::serialize(&instances);
1998        let restored = VegetationSerializer::deserialize(&bytes).unwrap();
1999        assert_eq!(restored.len(), instances.len());
2000        assert!((restored[0].position.x - 1.0).abs() < 1e-5);
2001        assert!((restored[0].position.y - 2.0).abs() < 1e-5);
2002    }
2003}
2004
2005// ─────────────────────────────────────────────────────────────────────────────
2006// Extended vegetation systems
2007// ─────────────────────────────────────────────────────────────────────────────
2008
2009/// Represents a single falling leaf particle used for visual effects.
2010#[derive(Debug, Clone)]
2011pub struct LeafParticle {
2012    pub position: Vec3,
2013    pub velocity: Vec3,
2014    pub rotation: f32,
2015    pub angular_velocity: f32,
2016    pub lifetime: f32,
2017    pub max_lifetime: f32,
2018    pub color: Vec3,
2019    pub size: f32,
2020    pub alpha: f32,
2021}
2022
2023impl LeafParticle {
2024    pub fn new(pos: Vec3, color: Vec3, size: f32, lifetime: f32) -> Self {
2025        Self {
2026            position: pos,
2027            velocity: Vec3::new(0.0, -0.5, 0.0),
2028            rotation: 0.0,
2029            angular_velocity: 1.2,
2030            lifetime,
2031            max_lifetime: lifetime,
2032            color,
2033            size,
2034            alpha: 1.0,
2035        }
2036    }
2037
2038    /// Advance simulation by `dt` seconds, applying gravity and wind drift.
2039    pub fn update(&mut self, dt: f32, wind: Vec3) {
2040        let gravity = Vec3::new(0.0, -0.3, 0.0);
2041        let drag = -self.velocity * 0.4;
2042        self.velocity += (gravity + wind + drag) * dt;
2043        self.position += self.velocity * dt;
2044        self.rotation += self.angular_velocity * dt;
2045        self.lifetime -= dt;
2046        self.alpha = (self.lifetime / self.max_lifetime).clamp(0.0, 1.0);
2047    }
2048
2049    pub fn is_alive(&self) -> bool {
2050        self.lifetime > 0.0
2051    }
2052}
2053
2054/// Emitter that spawns leaf particles from tree canopies.
2055#[derive(Debug, Clone)]
2056pub struct LeafParticleEmitter {
2057    pub origin: Vec3,
2058    pub emit_radius: f32,
2059    pub emit_rate: f32,      // particles per second
2060    pub particle_lifetime: f32,
2061    pub color: Vec3,
2062    accumulated: f32,
2063    rng_state: u64,
2064}
2065
2066impl LeafParticleEmitter {
2067    pub fn new(origin: Vec3, radius: f32, rate: f32, lifetime: f32, color: Vec3) -> Self {
2068        Self {
2069            origin,
2070            emit_radius: radius,
2071            emit_rate: rate,
2072            particle_lifetime: lifetime,
2073            color,
2074            accumulated: 0.0,
2075            rng_state: (origin.x.to_bits() as u64) ^ 0xDEADBEEF_u64,
2076        }
2077    }
2078
2079    fn rng_f32(&mut self) -> f32 {
2080        self.rng_state ^= self.rng_state << 13;
2081        self.rng_state ^= self.rng_state >> 7;
2082        self.rng_state ^= self.rng_state << 17;
2083        (self.rng_state as f32) / (u64::MAX as f32)
2084    }
2085
2086    /// Returns newly spawned particles for this timestep.
2087    pub fn emit(&mut self, dt: f32) -> Vec<LeafParticle> {
2088        self.accumulated += self.emit_rate * dt;
2089        let count = self.accumulated as usize;
2090        self.accumulated -= count as f32;
2091        let mut out = Vec::with_capacity(count);
2092        for _ in 0..count {
2093            let angle = self.rng_f32() * std::f32::consts::TAU;
2094            let r = self.rng_f32() * self.emit_radius;
2095            let offset = Vec3::new(r * angle.cos(), self.rng_f32() * 0.5, r * angle.sin());
2096            out.push(LeafParticle::new(
2097                self.origin + offset,
2098                self.color,
2099                0.05 + self.rng_f32() * 0.05,
2100                self.particle_lifetime * (0.8 + self.rng_f32() * 0.4),
2101            ));
2102        }
2103        out
2104    }
2105}
2106
2107/// Manages a pool of leaf particles across an entire scene.
2108#[derive(Debug, Clone, Default)]
2109pub struct LeafParticleSystem {
2110    pub particles: Vec<LeafParticle>,
2111    pub emitters: Vec<LeafParticleEmitter>,
2112    pub max_particles: usize,
2113}
2114
2115impl LeafParticleSystem {
2116    pub fn new(max_particles: usize) -> Self {
2117        Self { particles: Vec::new(), emitters: Vec::new(), max_particles }
2118    }
2119
2120    pub fn add_emitter(&mut self, e: LeafParticleEmitter) {
2121        self.emitters.push(e);
2122    }
2123
2124    pub fn update(&mut self, dt: f32, wind: Vec3) {
2125        // Update existing particles
2126        self.particles.retain_mut(|p| { p.update(dt, wind); p.is_alive() });
2127        // Emit new ones if budget allows
2128        let budget = self.max_particles.saturating_sub(self.particles.len());
2129        let mut new_particles = Vec::new();
2130        for emitter in &mut self.emitters {
2131            let batch = emitter.emit(dt);
2132            new_particles.extend(batch);
2133        }
2134        new_particles.truncate(budget);
2135        self.particles.extend(new_particles);
2136    }
2137
2138    pub fn live_count(&self) -> usize {
2139        self.particles.len()
2140    }
2141}
2142
2143// ─────────────────────────────────────────────────────────────────────────────
2144// Terrain-aware vegetation placement helpers
2145// ─────────────────────────────────────────────────────────────────────────────
2146
2147/// Placement constraint: the cell at (x, z) in the heightmap must satisfy
2148/// slope and altitude criteria for the given tree type.
2149pub struct PlacementConstraint {
2150    pub min_altitude: f32,
2151    pub max_altitude: f32,
2152    pub max_slope_deg: f32,
2153}
2154
2155impl PlacementConstraint {
2156    pub fn for_tree(tt: TreeType) -> Self {
2157        match tt {
2158            TreeType::Palm => Self { min_altitude: 0.02, max_altitude: 0.25, max_slope_deg: 20.0 },
2159            TreeType::Cactus => Self { min_altitude: 0.05, max_altitude: 0.40, max_slope_deg: 30.0 },
2160            TreeType::Oak | TreeType::Fern => Self { min_altitude: 0.10, max_altitude: 0.60, max_slope_deg: 35.0 },
2161            TreeType::Pine | TreeType::Tropical => Self { min_altitude: 0.20, max_altitude: 0.80, max_slope_deg: 40.0 },
2162            TreeType::Birch => Self { min_altitude: 0.15, max_altitude: 0.65, max_slope_deg: 38.0 },
2163            TreeType::Dead => Self { min_altitude: 0.05, max_altitude: 0.90, max_slope_deg: 50.0 },
2164            TreeType::Willow => Self { min_altitude: 0.02, max_altitude: 0.30, max_slope_deg: 15.0 },
2165            TreeType::Mushroom => Self { min_altitude: 0.05, max_altitude: 0.45, max_slope_deg: 25.0 },
2166        }
2167    }
2168
2169    pub fn check(&self, altitude: f32, slope_deg: f32) -> bool {
2170        altitude >= self.min_altitude
2171            && altitude <= self.max_altitude
2172            && slope_deg <= self.max_slope_deg
2173    }
2174}
2175
2176/// Filters a list of candidate positions against a heightmap using
2177/// `PlacementConstraint`.
2178pub struct TerrainAwarePlacement;
2179
2180impl TerrainAwarePlacement {
2181    /// `positions` are world-space XZ coordinates. `hm_scale` converts world
2182    /// units to [0,1] heightmap UV. Returns accepted positions with world Y.
2183    pub fn filter(
2184        positions: &[(f32, f32)],
2185        heights: &[f32],   // same length as positions, pre-sampled
2186        slopes: &[f32],    // degrees, same length
2187        constraint: &PlacementConstraint,
2188    ) -> Vec<(f32, f32, f32)> {
2189        positions.iter().zip(heights.iter()).zip(slopes.iter())
2190            .filter_map(|(((x, z), &h), &s)| {
2191                if constraint.check(h, s) { Some((*x, h, *z)) } else { None }
2192            })
2193            .collect()
2194    }
2195}
2196
2197// ─────────────────────────────────────────────────────────────────────────────
2198// Vegetation heat-map: tracks density per grid cell for editor visualization
2199// ─────────────────────────────────────────────────────────────────────────────
2200
2201/// 2-D grid counting how many vegetation instances fall in each cell.
2202#[derive(Debug, Clone)]
2203pub struct VegetationHeatMap {
2204    pub width: usize,
2205    pub height: usize,
2206    pub cell_size: f32,
2207    counts: Vec<u32>,
2208}
2209
2210impl VegetationHeatMap {
2211    pub fn new(width: usize, height: usize, cell_size: f32) -> Self {
2212        Self { width, height, cell_size, counts: vec![0; width * height] }
2213    }
2214
2215    pub fn accumulate(&mut self, x: f32, z: f32) {
2216        let cx = (x / self.cell_size) as usize;
2217        let cz = (z / self.cell_size) as usize;
2218        if cx < self.width && cz < self.height {
2219            self.counts[cz * self.width + cx] += 1;
2220        }
2221    }
2222
2223    pub fn build_from(system: &VegetationSystem, width: usize, height: usize, cell_size: f32) -> Self {
2224        let mut hm = Self::new(width, height, cell_size);
2225        for inst in &system.instances {
2226            hm.accumulate(inst.position.x, inst.position.z);
2227        }
2228        hm
2229    }
2230
2231    pub fn max_count(&self) -> u32 {
2232        self.counts.iter().copied().max().unwrap_or(0)
2233    }
2234
2235    pub fn normalized_at(&self, cx: usize, cz: usize) -> f32 {
2236        let max = self.max_count();
2237        if max == 0 { return 0.0; }
2238        self.counts[cz * self.width + cx] as f32 / max as f32
2239    }
2240
2241    pub fn total_instances(&self) -> u64 {
2242        self.counts.iter().map(|&c| c as u64).sum()
2243    }
2244}
2245
2246// ─────────────────────────────────────────────────────────────────────────────
2247// Vegetation export formats
2248// ─────────────────────────────────────────────────────────────────────────────
2249
2250/// Minimal OBJ-like text exporter for vegetation instances (positions only).
2251pub struct VegetationObjExporter;
2252
2253impl VegetationObjExporter {
2254    /// Produces a textual listing of instance positions as OBJ vertex lines.
2255    pub fn export(system: &VegetationSystem) -> String {
2256        let mut out = String::with_capacity(system.instances.len() * 32);
2257        out.push_str("# Vegetation export\n");
2258        for inst in &system.instances {
2259            let kind_str = match &inst.kind {
2260                VegetationKind::Tree(t) => format!("{:?}", t),
2261                VegetationKind::Grass => "Grass".to_owned(),
2262                VegetationKind::Rock { size_class } => format!("Rock{}", size_class),
2263                VegetationKind::Shrub => "Shrub".to_owned(),
2264                VegetationKind::Flower => "Flower".to_owned(),
2265            };
2266            out.push_str(&format!(
2267                "v {:.4} {:.4} {:.4} # {}\n",
2268                inst.position.x, inst.position.y, inst.position.z, kind_str
2269            ));
2270        }
2271        out
2272    }
2273}
2274
2275/// CSV exporter for spreadsheet analysis.
2276pub struct VegetationCsvExporter;
2277
2278impl VegetationCsvExporter {
2279    pub fn export(system: &VegetationSystem) -> String {
2280        let mut out = String::from("x,y,z,rotation,scale_x,scale_y,scale_z,lod,kind\n");
2281        for inst in &system.instances {
2282            let kind_str = match &inst.kind {
2283                VegetationKind::Tree(t) => format!("{:?}", t),
2284                VegetationKind::Grass => "Grass".to_owned(),
2285                VegetationKind::Rock { size_class } => format!("Rock{}", size_class),
2286                VegetationKind::Shrub => "Shrub".to_owned(),
2287                VegetationKind::Flower => "Flower".to_owned(),
2288            };
2289            out.push_str(&format!(
2290                "{:.4},{:.4},{:.4},{:.4},{:.4},{:.4},{:.4},{},{}\n",
2291                inst.position.x, inst.position.y, inst.position.z,
2292                inst.rotation,
2293                inst.scale.x, inst.scale.y, inst.scale.z,
2294                inst.lod_level,
2295                kind_str,
2296            ));
2297        }
2298        out
2299    }
2300}
2301
2302// ─────────────────────────────────────────────────────────────────────────────
2303// Vegetation culling helpers
2304// ─────────────────────────────────────────────────────────────────────────────
2305
2306/// Axis-aligned bounding box used for frustum / region culling.
2307#[derive(Debug, Clone, Copy)]
2308pub struct VegetationAabb {
2309    pub min: Vec3,
2310    pub max: Vec3,
2311}
2312
2313impl VegetationAabb {
2314    pub fn from_instances(instances: &[VegetationInstance]) -> Self {
2315        let mut mn = Vec3::splat(f32::INFINITY);
2316        let mut mx = Vec3::splat(f32::NEG_INFINITY);
2317        for inst in instances {
2318            mn = mn.min(inst.position);
2319            mx = mx.max(inst.position);
2320        }
2321        Self { min: mn, max: mx }
2322    }
2323
2324    pub fn contains(&self, p: Vec3) -> bool {
2325        p.x >= self.min.x && p.x <= self.max.x
2326            && p.y >= self.min.y && p.y <= self.max.y
2327            && p.z >= self.min.z && p.z <= self.max.z
2328    }
2329
2330    pub fn intersects(&self, other: &VegetationAabb) -> bool {
2331        self.min.x <= other.max.x && self.max.x >= other.min.x
2332            && self.min.y <= other.max.y && self.max.y >= other.min.y
2333            && self.min.z <= other.max.z && self.max.z >= other.min.z
2334    }
2335
2336    pub fn center(&self) -> Vec3 {
2337        (self.min + self.max) * 0.5
2338    }
2339
2340    pub fn half_extents(&self) -> Vec3 {
2341        (self.max - self.min) * 0.5
2342    }
2343}
2344
2345/// Spatial grid for O(1) region queries of vegetation instances.
2346pub struct VegetationGrid {
2347    pub cell_size: f32,
2348    pub width_cells: usize,
2349    pub height_cells: usize,
2350    cells: Vec<Vec<usize>>,  // cell → list of instance indices
2351}
2352
2353impl VegetationGrid {
2354    pub fn build(system: &VegetationSystem, cell_size: f32, world_width: f32, world_height: f32) -> Self {
2355        let wc = ((world_width / cell_size).ceil() as usize).max(1);
2356        let hc = ((world_height / cell_size).ceil() as usize).max(1);
2357        let mut cells = vec![Vec::new(); wc * hc];
2358        for (i, inst) in system.instances.iter().enumerate() {
2359            let cx = ((inst.position.x / cell_size) as usize).min(wc - 1);
2360            let cz = ((inst.position.z / cell_size) as usize).min(hc - 1);
2361            cells[cz * wc + cx].push(i);
2362        }
2363        Self { cell_size, width_cells: wc, height_cells: hc, cells }
2364    }
2365
2366    /// Returns indices of all instances in cells overlapping `aabb`.
2367    pub fn query_aabb<'a>(&'a self, aabb: &VegetationAabb, system: &'a VegetationSystem) -> Vec<&'a VegetationInstance> {
2368        let x0 = ((aabb.min.x / self.cell_size) as usize).min(self.width_cells.saturating_sub(1));
2369        let x1 = ((aabb.max.x / self.cell_size) as usize).min(self.width_cells.saturating_sub(1));
2370        let z0 = ((aabb.min.z / self.cell_size) as usize).min(self.height_cells.saturating_sub(1));
2371        let z1 = ((aabb.max.z / self.cell_size) as usize).min(self.height_cells.saturating_sub(1));
2372        let mut result = Vec::new();
2373        for cz in z0..=z1 {
2374            for cx in x0..=x1 {
2375                for &idx in &self.cells[cz * self.width_cells + cx] {
2376                    result.push(&system.instances[idx]);
2377                }
2378            }
2379        }
2380        result
2381    }
2382
2383    pub fn cell_count(&self) -> usize {
2384        self.width_cells * self.height_cells
2385    }
2386}
2387
2388// ─────────────────────────────────────────────────────────────────────────────
2389// Extended tests
2390// ─────────────────────────────────────────────────────────────────────────────
2391
2392#[cfg(test)]
2393mod extended_veg_tests {
2394    use super::*;
2395
2396    #[test]
2397    fn test_leaf_particle_lifecycle() {
2398        let mut p = LeafParticle::new(Vec3::ZERO, Vec3::new(0.8, 0.4, 0.1), 0.05, 2.0);
2399        assert!(p.is_alive());
2400        p.update(1.5, Vec3::new(0.1, 0.0, 0.05));
2401        assert!(p.is_alive());
2402        p.update(1.0, Vec3::ZERO);
2403        assert!(!p.is_alive());
2404    }
2405
2406    #[test]
2407    fn test_leaf_particle_system_emitter() {
2408        let mut sys = LeafParticleSystem::new(1000);
2409        let emitter = LeafParticleEmitter::new(
2410            Vec3::new(10.0, 15.0, 10.0),
2411            3.0, 50.0, 3.0,
2412            Vec3::new(1.0, 0.5, 0.0),
2413        );
2414        sys.add_emitter(emitter);
2415        sys.update(0.5, Vec3::new(0.2, 0.0, 0.1));
2416        // 50 particles/sec * 0.5s = 25 particles expected
2417        assert!(sys.live_count() > 0);
2418    }
2419
2420    #[test]
2421    fn test_placement_constraint_oak() {
2422        let c = PlacementConstraint::for_tree(TreeType::Oak);
2423        assert!(c.check(0.3, 20.0));
2424        assert!(!c.check(0.05, 20.0));  // too low
2425        assert!(!c.check(0.3, 40.0));   // too steep
2426    }
2427
2428    #[test]
2429    fn test_terrain_aware_placement_filter() {
2430        let positions = vec![(10.0f32, 10.0f32), (20.0, 20.0), (30.0, 30.0)];
2431        let heights   = vec![0.30f32, 0.05f32, 0.45f32];
2432        let slopes    = vec![15.0f32, 10.0f32, 50.0f32];
2433        let c = PlacementConstraint::for_tree(TreeType::Oak);
2434        let accepted = TerrainAwarePlacement::filter(&positions, &heights, &slopes, &c);
2435        // Only first passes (h=0.3, s=15); second fails altitude; third fails slope
2436        assert_eq!(accepted.len(), 1);
2437        assert!((accepted[0].0 - 10.0).abs() < 1e-5);
2438    }
2439
2440    #[test]
2441    fn test_vegetation_heat_map() {
2442        let mut sys = VegetationSystem::new();
2443        for i in 0..20u32 {
2444            sys.instances.push(VegetationInstance {
2445                position: Vec3::new(i as f32 * 5.0, 0.0, 0.0),
2446                rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true,
2447                kind: VegetationKind::Tree(TreeType::Oak),
2448            });
2449        }
2450        let hm = VegetationHeatMap::build_from(&sys, 10, 10, 10.0);
2451        assert!(hm.total_instances() > 0);
2452        assert!(hm.max_count() >= 1);
2453    }
2454
2455    #[test]
2456    fn test_vegetation_obj_exporter() {
2457        let mut sys = VegetationSystem::new();
2458        sys.instances.push(VegetationInstance {
2459            position: Vec3::new(1.0, 0.5, 2.0),
2460            rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true,
2461            kind: VegetationKind::Tree(TreeType::Pine),
2462        });
2463        let obj = VegetationObjExporter::export(&sys);
2464        assert!(obj.contains("v "));
2465        assert!(obj.contains("Pine"));
2466    }
2467
2468    #[test]
2469    fn test_vegetation_csv_exporter() {
2470        let mut sys = VegetationSystem::new();
2471        sys.instances.push(VegetationInstance {
2472            position: Vec3::new(3.0, 1.0, 4.0),
2473            rotation: 0.7, scale: Vec3::ONE, lod_level: 0, visible: true,
2474            kind: VegetationKind::Grass,
2475        });
2476        let csv = VegetationCsvExporter::export(&sys);
2477        assert!(csv.starts_with("x,y,z"));
2478        assert!(csv.contains("Grass"));
2479    }
2480
2481    #[test]
2482    fn test_vegetation_aabb() {
2483        let instances = vec![
2484            VegetationInstance { position: Vec3::new(0.0, 0.0, 0.0), rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true, kind: VegetationKind::Grass },
2485            VegetationInstance { position: Vec3::new(10.0, 5.0, 10.0), rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true, kind: VegetationKind::Grass },
2486        ];
2487        let aabb = VegetationAabb::from_instances(&instances);
2488        assert!(aabb.contains(Vec3::new(5.0, 2.5, 5.0)));
2489        assert!(!aabb.contains(Vec3::new(20.0, 0.0, 0.0)));
2490        let center = aabb.center();
2491        assert!((center.x - 5.0).abs() < 1e-5);
2492    }
2493
2494    #[test]
2495    fn test_vegetation_grid_query() {
2496        let mut sys = VegetationSystem::new();
2497        for i in 0..10u32 {
2498            sys.instances.push(VegetationInstance {
2499                position: Vec3::new(i as f32 * 10.0, 0.0, 5.0),
2500                rotation: 0.0, scale: Vec3::ONE, lod_level: 0, visible: true,
2501                kind: VegetationKind::Tree(TreeType::Oak),
2502            });
2503        }
2504        let grid = VegetationGrid::build(&sys, 20.0, 100.0, 100.0);
2505        let query_aabb = VegetationAabb { min: Vec3::new(0.0, -1.0, 0.0), max: Vec3::new(25.0, 1.0, 10.0) };
2506        let found = grid.query_aabb(&query_aabb, &sys);
2507        assert!(!found.is_empty());
2508    }
2509}