nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use nalgebra_glm::Vec3;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrassSpecies {
    pub name: String,
    pub blade_width: f32,
    pub blade_height_min: f32,
    pub blade_height_max: f32,
    pub blade_curvature: f32,
    pub base_color: [f32; 4],
    pub tip_color: [f32; 4],
    pub sss_color: [f32; 4],
    pub sss_intensity: f32,
    pub specular_power: f32,
    pub specular_strength: f32,
    pub density_scale: f32,
}

impl Default for GrassSpecies {
    fn default() -> Self {
        Self {
            name: "default".to_string(),
            blade_width: 0.05,
            blade_height_min: 0.3,
            blade_height_max: 0.6,
            blade_curvature: 0.3,
            base_color: [0.05, 0.15, 0.02, 1.0],
            tip_color: [0.2, 0.4, 0.1, 1.0],
            sss_color: [0.4, 0.7, 0.2, 1.0],
            sss_intensity: 0.5,
            specular_power: 64.0,
            specular_strength: 0.3,
            density_scale: 1.0,
        }
    }
}

impl GrassSpecies {
    pub fn meadow() -> Self {
        Self {
            name: "meadow".to_string(),
            blade_width: 0.04,
            blade_height_min: 0.2,
            blade_height_max: 0.5,
            blade_curvature: 0.25,
            base_color: [0.04, 0.12, 0.02, 1.0],
            tip_color: [0.15, 0.35, 0.08, 1.0],
            sss_color: [0.35, 0.65, 0.18, 1.0],
            sss_intensity: 0.6,
            specular_power: 48.0,
            specular_strength: 0.25,
            density_scale: 1.2,
        }
    }

    pub fn tall() -> Self {
        Self {
            name: "tall".to_string(),
            blade_width: 0.06,
            blade_height_min: 0.6,
            blade_height_max: 1.2,
            blade_curvature: 0.4,
            base_color: [0.03, 0.1, 0.02, 1.0],
            tip_color: [0.18, 0.38, 0.1, 1.0],
            sss_color: [0.4, 0.7, 0.2, 1.0],
            sss_intensity: 0.5,
            specular_power: 56.0,
            specular_strength: 0.3,
            density_scale: 0.6,
        }
    }

    pub fn short() -> Self {
        Self {
            name: "short".to_string(),
            blade_width: 0.03,
            blade_height_min: 0.1,
            blade_height_max: 0.25,
            blade_curvature: 0.15,
            base_color: [0.06, 0.16, 0.03, 1.0],
            tip_color: [0.22, 0.42, 0.12, 1.0],
            sss_color: [0.38, 0.68, 0.22, 1.0],
            sss_intensity: 0.55,
            specular_power: 40.0,
            specular_strength: 0.2,
            density_scale: 1.5,
        }
    }

    pub fn flowers() -> Self {
        Self {
            name: "flowers".to_string(),
            blade_width: 0.08,
            blade_height_min: 0.25,
            blade_height_max: 0.45,
            blade_curvature: 0.2,
            base_color: [0.04, 0.12, 0.03, 1.0],
            tip_color: [0.8, 0.3, 0.5, 1.0],
            sss_color: [0.9, 0.5, 0.6, 1.0],
            sss_intensity: 0.7,
            specular_power: 32.0,
            specular_strength: 0.4,
            density_scale: 0.4,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrassConfig {
    pub blades_per_patch: u32,
    pub patch_size: f32,
    pub stream_radius: f32,
    pub unload_radius: f32,
    pub max_loaded_patches: usize,
    pub wind_strength: f32,
    pub wind_frequency: f32,
    pub wind_direction: [f32; 2],
    pub interaction_radius: f32,
    pub interaction_strength: f32,
    pub interactors_enabled: bool,
    pub cast_shadows: bool,
    pub receive_shadows: bool,
    pub lod_distances: [f32; 4],
    pub lod_density_scales: [f32; 4],
}

impl Default for GrassConfig {
    fn default() -> Self {
        Self {
            blades_per_patch: 64,
            patch_size: 8.0,
            stream_radius: 200.0,
            unload_radius: 220.0,
            max_loaded_patches: 4096,
            wind_strength: 1.0,
            wind_frequency: 1.0,
            wind_direction: [1.0, 0.0],
            interaction_radius: 1.0,
            interaction_strength: 1.0,
            interactors_enabled: true,
            cast_shadows: true,
            receive_shadows: true,
            lod_distances: [20.0, 50.0, 100.0, 200.0],
            lod_density_scales: [1.0, 0.6, 0.3, 0.1],
        }
    }
}

impl GrassConfig {
    pub fn with_density(mut self, blades_per_patch: u32) -> Self {
        self.blades_per_patch = blades_per_patch;
        self
    }

    pub fn with_wind(mut self, strength: f32, frequency: f32) -> Self {
        self.wind_strength = strength;
        self.wind_frequency = frequency;
        self
    }

    pub fn with_wind_direction(mut self, x: f32, z: f32) -> Self {
        let len = (x * x + z * z).sqrt();
        if len > 0.0001 {
            self.wind_direction = [x / len, z / len];
        }
        self
    }

    pub fn with_shadows(mut self, cast: bool, receive: bool) -> Self {
        self.cast_shadows = cast;
        self.receive_shadows = receive;
        self
    }

    pub fn with_stream_radius(mut self, radius: f32) -> Self {
        self.stream_radius = radius;
        self.unload_radius = radius + 20.0;
        self
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrassRegion {
    pub config: GrassConfig,
    pub species: Vec<GrassSpecies>,
    pub species_weights: Vec<f32>,
    pub enabled: bool,
    pub bounds_min: Vec3,
    pub bounds_max: Vec3,
    pub player_position: Vec3,
    #[cfg(feature = "terrain")]
    pub terrain_config: Option<crate::ecs::terrain::TerrainConfig>,
}

impl Default for GrassRegion {
    fn default() -> Self {
        Self {
            config: GrassConfig::default(),
            species: vec![GrassSpecies::default()],
            species_weights: vec![1.0],
            enabled: true,
            bounds_min: Vec3::new(-1000.0, -10.0, -1000.0),
            bounds_max: Vec3::new(1000.0, 100.0, 1000.0),
            player_position: Vec3::zeros(),
            #[cfg(feature = "terrain")]
            terrain_config: None,
        }
    }
}

impl GrassRegion {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_config(mut self, config: GrassConfig) -> Self {
        self.config = config;
        self
    }

    pub fn with_species(mut self, species: GrassSpecies, weight: f32) -> Self {
        self.species.push(species);
        self.species_weights.push(weight);
        self
    }

    pub fn with_bounds(mut self, min: Vec3, max: Vec3) -> Self {
        self.bounds_min = min;
        self.bounds_max = max;
        self
    }

    pub fn set_player_position(&mut self, position: Vec3) {
        self.player_position = position;
    }

    #[cfg(feature = "terrain")]
    pub fn with_terrain(mut self, terrain_config: crate::ecs::terrain::TerrainConfig) -> Self {
        self.terrain_config = Some(terrain_config);
        self
    }

    #[cfg(feature = "terrain")]
    pub fn set_terrain(&mut self, terrain_config: crate::ecs::terrain::TerrainConfig) {
        self.terrain_config = Some(terrain_config);
    }

    pub fn clear_species(&mut self) {
        self.species.clear();
        self.species_weights.clear();
    }

    pub fn add_species(&mut self, species: GrassSpecies, weight: f32) {
        self.species.push(species);
        self.species_weights.push(weight);
    }

    pub fn normalized_weights(&self) -> Vec<f32> {
        let total: f32 = self.species_weights.iter().sum();
        if total > 0.0001 {
            self.species_weights.iter().map(|w| w / total).collect()
        } else {
            vec![1.0 / self.species.len() as f32; self.species.len()]
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GrassInteractor {
    pub radius: f32,
    pub strength: f32,
}

impl GrassInteractor {
    pub fn new(radius: f32, strength: f32) -> Self {
        Self { radius, strength }
    }
}