nightshade-api 0.38.0

Procedural high level API for the nightshade game engine
Documentation
//! Retained scene content: primitives, models, hierarchy, and the [`Object`]
//! descriptor for spawning shape, color, and physics in one call.

use crate::palette::WHITE;
use crate::runner::MATERIAL_PREFIX;
use nightshade::prelude::nalgebra_glm::Mat4;
use nightshade::prelude::*;

pub use nightshade::prelude::despawn_recursive_immediate as despawn;
pub use nightshade::prelude::spawn_cone_at as spawn_cone;
pub use nightshade::prelude::spawn_cube_at as spawn_cube;
pub use nightshade::prelude::spawn_cylinder_at as spawn_cylinder;
pub use nightshade::prelude::spawn_plane_at as spawn_plane;
pub use nightshade::prelude::spawn_sphere_at as spawn_sphere;
pub use nightshade::prelude::spawn_torus_at as spawn_torus;

/// The primitive shapes [`spawn_object`] can produce.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Shape {
    #[default]
    Cube,
    Sphere,
    Cylinder,
    Cone,
    Torus,
    Plane,
}

/// Physics participation for [`spawn_object`]. Requires the `physics`
/// feature, which is on by default.
///
/// Dynamic cubes, spheres, cylinders, and cones get exact colliders. A
/// dynamic torus gets a convex hull of its mesh, which fills the hole. Static
/// toruses and planes collide against the exact triangle mesh.
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum Body {
    #[default]
    None,
    Static,
    Dynamic {
        mass: f32,
    },
}

/// Everything about one object in a single struct literal.
///
/// ```ignore
/// let ball = spawn_object(world, Object {
///     shape: Shape::Sphere,
///     position: vec3(0.0, 4.0, 0.0),
///     color: RED,
///     body: Body::Dynamic { mass: 2.0 },
///     ..Object::default()
/// });
/// ```
pub struct Object {
    pub shape: Shape,
    pub position: Vec3,
    pub scale: Vec3,
    pub color: [f32; 4],
    pub body: Body,
}

impl Default for Object {
    fn default() -> Self {
        Self {
            shape: Shape::Cube,
            position: Vec3::zeros(),
            scale: Vec3::new(1.0, 1.0, 1.0),
            color: WHITE,
            body: Body::None,
        }
    }
}

/// Spawns an [`Object`]: mesh, color, and optional physics body in one call.
pub fn spawn_object(world: &mut World, object: Object) -> Entity {
    let entity = spawn_mesh_at(
        world,
        mesh_name(object.shape),
        object.position,
        object.scale,
    );
    crate::appearance::set_color(world, entity, object.color);
    match object.body {
        Body::None => {}
        #[cfg(feature = "physics")]
        Body::Static => {
            let collider = static_collider(world, object.shape, object.scale)
                .with_friction(0.8)
                .with_restitution(0.1);
            attach_body(
                world,
                entity,
                RigidBodyComponent::new_static().with_translation(
                    object.position.x,
                    object.position.y,
                    object.position.z,
                ),
                collider,
                false,
            );
        }
        #[cfg(feature = "physics")]
        Body::Dynamic { mass } => {
            let collider = dynamic_collider(world, object.shape, object.scale)
                .with_friction(0.7)
                .with_restitution(0.2);
            attach_body(
                world,
                entity,
                RigidBodyComponent::new_dynamic()
                    .with_translation(object.position.x, object.position.y, object.position.z)
                    .with_mass(mass),
                collider,
                true,
            );
        }
        #[cfg(not(feature = "physics"))]
        Body::Static | Body::Dynamic { .. } => {}
    }
    entity
}

/// Spawns a flat ground plane reaching `half_extent` in each direction. With
/// the `physics` feature it carries a static collider so dynamic objects land
/// on it.
pub fn spawn_floor(world: &mut World, half_extent: f32) -> Entity {
    let entity = spawn_mesh_at(
        world,
        "Plane",
        Vec3::zeros(),
        Vec3::new(half_extent, 1.0, half_extent),
    );
    #[cfg(feature = "physics")]
    attach_body(
        world,
        entity,
        RigidBodyComponent::new_static().with_translation(0.0, -0.05, 0.0),
        ColliderComponent::new_cuboid(half_extent, 0.05, half_extent)
            .with_friction(0.8)
            .with_restitution(0.1),
        false,
    );
    entity
}

/// Spawns a glb model with its textures, materials, skins, and animations.
/// Panics with the import error when the bytes are not a valid glb.
pub fn spawn_model(world: &mut World, glb_bytes: &[u8], position: Vec3) -> Entity {
    let mut result =
        import_gltf_from_bytes(glb_bytes).expect("failed to import the glb model bytes");
    nightshade::ecs::loading::queue_gltf_load(world, &mut result);
    let prefab = &result.prefabs[0];
    nightshade::ecs::prefab::commands::spawn_prefab_with_skins(
        world,
        prefab,
        &result.animations,
        &result.skins,
        position,
    )
}

/// Starts playing the model's animation clip at `clip_index`.
pub fn play_animation(world: &mut World, entity: Entity, clip_index: usize) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        player.play(clip_index);
    }
}

/// Sets whether the entity's current animation repeats.
pub fn set_animation_looping(world: &mut World, entity: Entity, looping: bool) {
    if let Some(player) = world.core.get_animation_player_mut(entity) {
        player.looping = looping;
    }
}

/// Parents `child` to `parent`, or unparents it with `None`, keeping the
/// child exactly where it is in world space.
pub fn set_parent(world: &mut World, child: Entity, parent: Option<Entity>) {
    let child_world = crate::placement::world_matrix(world, child);
    let parent_world = parent
        .map(|parent_entity| crate::placement::world_matrix(world, parent_entity))
        .unwrap_or_else(Mat4::identity);
    let local = nalgebra_glm::inverse(&parent_world) * child_world;

    let translation = nalgebra_glm::vec3(local[(0, 3)], local[(1, 3)], local[(2, 3)]);
    let basis_x = nalgebra_glm::vec3(local[(0, 0)], local[(1, 0)], local[(2, 0)]);
    let basis_y = nalgebra_glm::vec3(local[(0, 1)], local[(1, 1)], local[(2, 1)]);
    let basis_z = nalgebra_glm::vec3(local[(0, 2)], local[(1, 2)], local[(2, 2)]);
    let scale = nalgebra_glm::vec3(
        basis_x.magnitude(),
        basis_y.magnitude(),
        basis_z.magnitude(),
    );
    let rotation_matrix = nalgebra_glm::Mat3::from_columns(&[
        basis_x / scale.x.max(f32::EPSILON),
        basis_y / scale.y.max(f32::EPSILON),
        basis_z / scale.z.max(f32::EPSILON),
    ]);
    let rotation = nalgebra_glm::mat3_to_quat(&rotation_matrix);

    if parent.is_some() {
        world.core.add_components(child, PARENT);
    }
    update_parent(
        world,
        child,
        parent.map(|parent_entity| Parent(Some(parent_entity))),
    );
    assign_local_transform(
        world,
        child,
        LocalTransform {
            translation,
            rotation,
            scale,
        },
    );
}

#[cfg(feature = "picking")]
pub(crate) fn is_reserved(world: &World, entity: Entity) -> bool {
    world
        .core
        .get_name(entity)
        .is_some_and(|name| name.0.starts_with(crate::runner::RESERVED_PREFIX))
}

pub(crate) fn api_material_name(entity: Entity) -> String {
    format!("{MATERIAL_PREFIX}{}", entity.id)
}

fn mesh_name(shape: Shape) -> &'static str {
    match shape {
        Shape::Cube => "Cube",
        Shape::Sphere => "Sphere",
        Shape::Cylinder => "Cylinder",
        Shape::Cone => "Cone",
        Shape::Torus => "Torus",
        Shape::Plane => "Plane",
    }
}

#[cfg(feature = "physics")]
fn dynamic_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
    match shape {
        Shape::Cube => ColliderComponent::new_cuboid(scale.x * 0.5, scale.y * 0.5, scale.z * 0.5),
        Shape::Sphere => ColliderComponent::new_ball(scale.x),
        Shape::Cylinder => ColliderComponent::new_cylinder(scale.y * 0.5, scale.x * 0.5),
        Shape::Cone => ColliderComponent::new_cone(scale.y * 0.5, scale.x * 0.5),
        Shape::Torus => ColliderComponent {
            shape: ColliderShape::ConvexMesh {
                vertices: scaled_mesh_points(world, "Torus", scale),
            },
            ..Default::default()
        },
        Shape::Plane => ColliderComponent::new_cuboid(scale.x, 0.05, scale.z),
    }
}

#[cfg(feature = "physics")]
fn static_collider(world: &World, shape: Shape, scale: Vec3) -> ColliderComponent {
    match shape {
        Shape::Torus | Shape::Plane => {
            let shape_mesh_name = mesh_name(shape);
            ColliderComponent {
                shape: ColliderShape::TriMesh {
                    vertices: scaled_mesh_points(world, shape_mesh_name, scale),
                    indices: mesh_triangles(world, shape_mesh_name),
                },
                ..Default::default()
            }
        }
        _ => dynamic_collider(world, shape, scale),
    }
}

#[cfg(feature = "physics")]
fn scaled_mesh_points(world: &World, mesh_name: &str, scale: Vec3) -> Vec<[f32; 3]> {
    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
        .map(|mesh| {
            mesh.vertices
                .iter()
                .map(|vertex| {
                    [
                        vertex.position[0] * scale.x,
                        vertex.position[1] * scale.y,
                        vertex.position[2] * scale.z,
                    ]
                })
                .collect()
        })
        .unwrap_or_default()
}

#[cfg(feature = "physics")]
fn mesh_triangles(world: &World, mesh_name: &str) -> Vec<[u32; 3]> {
    registry_entry_by_name(&world.resources.assets.mesh_cache.registry, mesh_name)
        .map(|mesh| {
            mesh.indices
                .chunks_exact(3)
                .map(|triangle| [triangle[0], triangle[1], triangle[2]])
                .collect()
        })
        .unwrap_or_default()
}

#[cfg(feature = "physics")]
fn attach_body(
    world: &mut World,
    entity: Entity,
    body: RigidBodyComponent,
    collider: ColliderComponent,
    dynamic: bool,
) {
    let mut flags = RIGID_BODY | COLLIDER;
    if dynamic {
        flags |= COLLISION_LISTENER | PHYSICS_INTERPOLATION;
    }
    world.core.add_components(entity, flags);
    world.core.set_rigid_body(entity, body);
    world.core.set_collider(entity, collider);
    if dynamic {
        reset_physics_interpolation(world, entity);
        if let Some(interpolation) = world.core.get_physics_interpolation_mut(entity) {
            interpolation.enabled = true;
        }
    }
}