nightshade 0.14.1

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
    }
}

/// Precomputed heightmap that grass uses to follow a terrain surface.
/// The heightmap is centered at the world origin and spans `width` × `depth`
/// in world units. `heights` is row-major with `resolution_x * resolution_z`
/// entries.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TerrainHeightmap {
    pub width: f32,
    pub depth: f32,
    pub resolution_x: u32,
    pub resolution_z: u32,
    pub heights: Vec<f32>,
}

impl TerrainHeightmap {
    pub fn new(width: f32, depth: f32, resolution_x: u32, resolution_z: u32) -> Self {
        Self {
            width,
            depth,
            resolution_x,
            resolution_z,
            heights: vec![0.0; (resolution_x * resolution_z) as usize],
        }
    }

    /// Sample height at world position `(world_x, world_z)` via bilinear
    /// interpolation. Out-of-bounds samples are clamped to the edge.
    pub fn sample(&self, world_x: f32, world_z: f32) -> f32 {
        if self.resolution_x < 2 || self.resolution_z < 2 || self.heights.is_empty() {
            return 0.0;
        }

        let half_width = self.width * 0.5;
        let half_depth = self.depth * 0.5;
        let u = ((world_x + half_width) / self.width).clamp(0.0, 1.0);
        let v = ((world_z + half_depth) / self.depth).clamp(0.0, 1.0);

        let fx = u * (self.resolution_x - 1) as f32;
        let fz = v * (self.resolution_z - 1) as f32;
        let x0 = fx.floor() as u32;
        let z0 = fz.floor() as u32;
        let x1 = (x0 + 1).min(self.resolution_x - 1);
        let z1 = (z0 + 1).min(self.resolution_z - 1);
        let tx = fx - x0 as f32;
        let tz = fz - z0 as f32;

        let stride = self.resolution_x as usize;
        let h00 = self.heights[z0 as usize * stride + x0 as usize];
        let h10 = self.heights[z0 as usize * stride + x1 as usize];
        let h01 = self.heights[z1 as usize * stride + x0 as usize];
        let h11 = self.heights[z1 as usize * stride + x1 as usize];

        let top = h00 + (h10 - h00) * tx;
        let bottom = h01 + (h11 - h01) * tx;
        top + (bottom - top) * tz
    }
}

#[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,
    pub heightmap: Option<TerrainHeightmap>,
}

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(),
            heightmap: 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;
    }

    pub fn with_heightmap(mut self, heightmap: TerrainHeightmap) -> Self {
        self.heightmap = Some(heightmap);
        self
    }

    pub fn set_heightmap(&mut self, heightmap: TerrainHeightmap) {
        self.heightmap = Some(heightmap);
    }

    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 }
    }
}