nightshade-api 0.40.0

Procedural high level API for the nightshade game engine
Documentation
//! Immediate mode drawing. Everything drawn with `draw_` is visible for
//! exactly one frame, so redraw it every frame you want it on screen. For
//! geometry that should persist, use the `spawn_` functions instead.
//!
//! Backed by pooled instanced meshes and a shared lines entity, so heavy use
//! costs one instance buffer refill per frame, not entity churn.

use crate::runner::{
    DRAW_CUBE_POOL, DRAW_LINES_POOL, DRAW_MATERIAL, DRAW_SPHERE_POOL, lookup_named, register_named,
};
use nightshade::prelude::nalgebra_glm::Quat;
use nightshade::prelude::*;

/// Draws a cube at `position` with the given size and color for one frame.
pub fn draw_cube(world: &mut World, position: Vec3, scale: Vec3, color: [f32; 4]) {
    push_instance(
        world,
        DRAW_CUBE_POOL,
        InstanceTransform::new(position, Quat::identity(), scale),
        color,
    );
}

/// Draws a sphere at `position` with the given radius and color for one frame.
pub fn draw_sphere(world: &mut World, position: Vec3, radius: f32, color: [f32; 4]) {
    push_instance(
        world,
        DRAW_SPHERE_POOL,
        InstanceTransform::new(
            position,
            Quat::identity(),
            Vec3::new(radius, radius, radius),
        ),
        color,
    );
}

/// Draws a line from `start` to `end` for one frame.
pub fn draw_line(world: &mut World, start: Vec3, end: Vec3, color: [f32; 4]) {
    let Some(entity) = lookup_named(world, DRAW_LINES_POOL) else {
        return;
    };
    if let Some(lines) = world.core.get_lines_mut(entity) {
        lines.push(Line {
            start,
            end,
            color: Vec4::new(color[0], color[1], color[2], color[3]),
        });
    }
}

/// Draws billboard text at a 3d position for one frame. Damage numbers,
/// debug readouts over entities, anything that should track live data.
/// Up to 64 labels per frame, drawn from a reused pool so a label with
/// stable content costs nothing to redraw.
pub fn draw_text_3d(world: &mut World, text: &str, position: Vec3) {
    for (index, name) in TEXT_POOL_NAMES.iter().enumerate() {
        match lookup_named(world, name) {
            Some(entity) => {
                let parked = world
                    .core
                    .get_local_transform(entity)
                    .is_some_and(|transform| transform.translation.y < PARKED_THRESHOLD);
                if parked || index == TEXT_POOL_NAMES.len() - 1 {
                    crate::text::set_text(world, entity, text);
                    crate::placement::set_position(world, entity, position);
                    return;
                }
            }
            None => {
                let entity = crate::text::spawn_label(world, text, position);
                world.core.add_components(entity, NAME);
                world.core.set_name(entity, Name(name.to_string()));
                register_named(world, name, entity);
                return;
            }
        }
    }
}

/// Spawns a retained set of lines that stays until you
/// [`despawn`](crate::prelude::despawn) it.
pub fn spawn_lines(world: &mut World, lines: Vec<Line>) -> Entity {
    let entity = spawn_entities(world, LINES, 1)[0];
    world.core.set_lines(entity, Lines::new(lines));
    entity
}

fn push_instance(
    world: &mut World,
    pool_name: &str,
    transform: InstanceTransform,
    color: [f32; 4],
) {
    let Some(entity) = lookup_named(world, pool_name) else {
        return;
    };
    if let Some(instanced) = world.core.get_instanced_mesh_mut(entity) {
        instanced.add_instance(transform);
        let index = instanced.instance_count() - 1;
        instanced.set_instance_tint(index, color);
    }
}

pub(crate) fn initialize_draw_pools(world: &mut World) {
    crate::scene::ensure_primitive_mesh(world, "Cube");
    crate::scene::ensure_primitive_mesh(world, "Sphere");

    material_registry_insert(
        &mut world.resources.assets.material_registry,
        DRAW_MATERIAL.to_string(),
        Material::default(),
    );
    if let Some((index, _)) = registry_lookup_index(
        &world.resources.assets.material_registry.registry,
        DRAW_MATERIAL,
    ) {
        registry_add_reference(
            &mut world.resources.assets.material_registry.registry,
            index,
        );
    }

    let cube_pool = spawn_instanced_mesh_with_material(world, "Cube", Vec::new(), DRAW_MATERIAL);
    world
        .core
        .set_name(cube_pool, Name(DRAW_CUBE_POOL.to_string()));
    register_named(world, DRAW_CUBE_POOL, cube_pool);
    let sphere_pool =
        spawn_instanced_mesh_with_material(world, "Sphere", Vec::new(), DRAW_MATERIAL);
    world
        .core
        .set_name(sphere_pool, Name(DRAW_SPHERE_POOL.to_string()));
    register_named(world, DRAW_SPHERE_POOL, sphere_pool);

    let lines_pool = spawn_entities(world, LINES | NAME, 1)[0];
    world
        .core
        .set_name(lines_pool, Name(DRAW_LINES_POOL.to_string()));
    world.core.set_lines(lines_pool, Lines::new(Vec::new()));
    register_named(world, DRAW_LINES_POOL, lines_pool);
}

pub(crate) fn clear_draw_pools(world: &mut World) {
    for pool_name in [DRAW_CUBE_POOL, DRAW_SPHERE_POOL] {
        if let Some(entity) = lookup_named(world, pool_name)
            && let Some(instanced) = world.core.get_instanced_mesh_mut(entity)
            && instanced.instance_count() > 0
        {
            instanced.clear_instances();
        }
    }
    if let Some(entity) = lookup_named(world, DRAW_LINES_POOL)
        && let Some(lines) = world.core.get_lines_mut(entity)
        && !lines.lines.is_empty()
    {
        lines.clear();
    }
    for name in TEXT_POOL_NAMES.iter() {
        let Some(entity) = lookup_named(world, name) else {
            break;
        };
        let used = world
            .core
            .get_local_transform(entity)
            .is_some_and(|transform| transform.translation.y > PARKED_THRESHOLD);
        if used {
            crate::placement::set_position(world, entity, Vec3::new(0.0, -100000.0, 0.0));
        }
    }
}

const PARKED_THRESHOLD: f32 = -90000.0;

const TEXT_POOL_NAMES: [&str; 64] = [
    "api::draw::text::0",
    "api::draw::text::1",
    "api::draw::text::2",
    "api::draw::text::3",
    "api::draw::text::4",
    "api::draw::text::5",
    "api::draw::text::6",
    "api::draw::text::7",
    "api::draw::text::8",
    "api::draw::text::9",
    "api::draw::text::10",
    "api::draw::text::11",
    "api::draw::text::12",
    "api::draw::text::13",
    "api::draw::text::14",
    "api::draw::text::15",
    "api::draw::text::16",
    "api::draw::text::17",
    "api::draw::text::18",
    "api::draw::text::19",
    "api::draw::text::20",
    "api::draw::text::21",
    "api::draw::text::22",
    "api::draw::text::23",
    "api::draw::text::24",
    "api::draw::text::25",
    "api::draw::text::26",
    "api::draw::text::27",
    "api::draw::text::28",
    "api::draw::text::29",
    "api::draw::text::30",
    "api::draw::text::31",
    "api::draw::text::32",
    "api::draw::text::33",
    "api::draw::text::34",
    "api::draw::text::35",
    "api::draw::text::36",
    "api::draw::text::37",
    "api::draw::text::38",
    "api::draw::text::39",
    "api::draw::text::40",
    "api::draw::text::41",
    "api::draw::text::42",
    "api::draw::text::43",
    "api::draw::text::44",
    "api::draw::text::45",
    "api::draw::text::46",
    "api::draw::text::47",
    "api::draw::text::48",
    "api::draw::text::49",
    "api::draw::text::50",
    "api::draw::text::51",
    "api::draw::text::52",
    "api::draw::text::53",
    "api::draw::text::54",
    "api::draw::text::55",
    "api::draw::text::56",
    "api::draw::text::57",
    "api::draw::text::58",
    "api::draw::text::59",
    "api::draw::text::60",
    "api::draw::text::61",
    "api::draw::text::62",
    "api::draw::text::63",
];