nightshade-api 0.47.0

Procedural high level API for the nightshade game engine
Documentation
//! Camera presets. Each call replaces the previous api camera, makes the new
//! one active, and the right controller runs automatically every frame.

#[cfg(feature = "physics")]
use crate::runner::{CAMERA_FIRST_PERSON, PLAYER_NAME, lookup_named, register_named};
use crate::runner::{CAMERA_FIXED, CAMERA_FLY, CAMERA_NAME_PREFIX, CAMERA_ORBIT};
use nightshade::ecs::camera::commands::spawn_third_person_camera;
#[cfg(feature = "physics")]
use nightshade::ecs::physics::commands::spawn_first_person_player;
use nightshade::ecs::world::VIEWPORT_SHADING;
use nightshade::prelude::*;

/// An orbit camera looking at `focus` from `radius` away. Drag to orbit,
/// scroll to zoom. This is the default camera every program starts with.
pub fn orbit_camera(world: &mut World, focus: Vec3, radius: f32) -> Entity {
    despawn_api_camera(world);
    let camera = spawn_pan_orbit_camera(world, focus, radius, 0.6, 0.4, CAMERA_ORBIT.to_string());
    activate_camera(world, camera);
    camera
}

/// A free fly camera at `position`. WASD to move, right drag to look.
pub fn fly_camera(world: &mut World, position: Vec3) -> Entity {
    despawn_api_camera(world);
    let camera = spawn_camera(world, position, CAMERA_FLY.to_string());
    activate_camera(world, camera);
    camera
}

/// A walking first person player at `position` with mouse look, WASD, sprint,
/// and jump. The cursor locks to the window. Returns the player entity.
/// `position` is the center of the player capsule, which is two units tall,
/// so spawn a little above the ground and let gravity settle it. Requires the
/// `physics` feature and a floor to stand on.
#[cfg(feature = "physics")]
pub fn first_person(world: &mut World, position: Vec3) -> Entity {
    despawn_api_camera(world);
    let (player, camera) = spawn_first_person_player(world, position);
    world.core.set_name(player, Name(PLAYER_NAME.to_string()));
    register_named(world, PLAYER_NAME, player);
    world
        .core
        .set_name(camera, Name(CAMERA_FIRST_PERSON.to_string()));
    activate_camera(world, camera);
    set_cursor_locked(world, true);
    set_cursor_visible(world, false);
    player
}

/// A third person camera that orbits and follows `target` from `distance` away,
/// the over-the-shoulder view for a character. Drag to orbit, scroll to zoom,
/// and the follow plus collision smoothing run every frame. Returns the camera.
pub fn third_person_camera(world: &mut World, target: Entity, distance: f32) -> Entity {
    despawn_api_camera(world);
    let camera = spawn_third_person_camera(
        world,
        target,
        distance,
        0.0,
        0.4,
        format!("{CAMERA_NAME_PREFIX}third_person"),
    );
    activate_camera(world, camera);
    camera
}

/// Sets the active camera's shading mode: wireframe, flat, solid, or the full
/// rendered look. A quick way to inspect geometry or get a stylized pass.
pub fn set_shading_mode(world: &mut World, mode: ShadingMode) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    world.core.add_components(camera, VIEWPORT_SHADING);
    world.core.set_viewport_shading(
        camera,
        ViewportShading {
            mode,
            show_overlays: true,
        },
    );
}

/// A stationary camera at `eye` looking at `target`. No controller runs, so
/// the view only changes when you move it yourself with [`look_at`].
pub fn fixed_camera(world: &mut World, eye: Vec3, target: Vec3) -> Entity {
    despawn_api_camera(world);
    let camera = spawn_camera(world, eye, CAMERA_FIXED.to_string());
    if let Some(transform) = mutate_local_transform(world, camera) {
        transform.rotation = camera_look_rotation(eye, target, 0.0);
    }
    activate_camera(world, camera);
    camera
}

/// Sets the first person player's walking speed in units per second. Sprint
/// scales from this.
#[cfg(feature = "physics")]
#[inline]
pub fn set_player_speed(world: &mut World, player: Entity, speed: f32) {
    if let Some(controller) = world.core.get_character_controller_mut(player) {
        controller.max_speed = speed;
    }
}

/// Sets the upward impulse of the first person player's jump.
#[cfg(feature = "physics")]
#[inline]
pub fn set_player_jump(world: &mut World, player: Entity, impulse: f32) {
    if let Some(controller) = world.core.get_character_controller_mut(player) {
        controller.jump_impulse = impulse;
    }
}

/// Moves the active camera to `eye` and points it at `target`.
pub fn look_at(world: &mut World, eye: Vec3, target: Vec3) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    if let Some(transform) = mutate_local_transform(world, camera) {
        transform.translation = eye;
        transform.rotation = camera_look_rotation(eye, target, 0.0);
    }
}

/// Glides the orbit camera's focus toward `focus`. Use it as a follow camera
/// by calling it every frame with a moving target.
pub fn set_orbit_focus(world: &mut World, focus: Vec3) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    if let Some(orbit) = world.core.get_pan_orbit_camera_mut(camera) {
        orbit.target_focus = focus;
    }
}

/// Sets the orbit camera's full framing at once: where it looks (`focus`), how
/// far back it sits (`radius`), and the `yaw` and `pitch` angles in radians.
/// Snaps there immediately, so it doubles as a camera reset. Pair it with
/// [`orbit_camera`], which creates the controller this drives.
pub fn set_orbit_view(world: &mut World, focus: Vec3, radius: f32, yaw: f32, pitch: f32) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    if let Some(orbit) = world.core.get_pan_orbit_camera_mut(camera) {
        orbit.focus = focus;
        orbit.target_focus = focus;
        orbit.radius = radius;
        orbit.target_radius = radius;
        orbit.yaw = yaw;
        orbit.target_yaw = yaw;
        orbit.pitch = pitch;
        orbit.target_pitch = pitch;
    }
}

/// Enables or disables scroll-wheel zoom on the active orbit camera. Disable it
/// to free the wheel for game use, like cycling a selection, while drag-to-orbit
/// keeps working.
pub fn set_orbit_zoom(world: &mut World, enabled: bool) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    if let Some(orbit) = world.core.get_pan_orbit_camera_mut(camera) {
        orbit.sensitivity.zoom = if enabled { 1.0 } else { 0.0 };
    }
}

/// Sets the modifier key the active orbit camera requires before drag orbits.
/// Pass "alt", "shift", or "control" to gate orbit behind that key, or "none"
/// to clear it so plain drag orbits again. A script that wants the unmodified
/// drag for itself sets a modifier so the camera does not move while it drags.
pub fn set_orbit_modifier(world: &mut World, modifier: &str) {
    use nightshade::ecs::camera::components::PanOrbitModifier;
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    let modifier = match modifier.to_ascii_lowercase().as_str() {
        "alt" => Some(PanOrbitModifier::Alt),
        "shift" => Some(PanOrbitModifier::Shift),
        "control" | "ctrl" => Some(PanOrbitModifier::Control),
        _ => None,
    };
    if let Some(orbit) = world.core.get_pan_orbit_camera_mut(camera) {
        orbit.bindings.orbit_modifier = modifier;
    }
}

/// The active camera's position in world space.
pub fn camera_position(world: &World) -> Vec3 {
    world
        .resources
        .active_camera
        .map(|camera| crate::placement::position(world, camera))
        .unwrap_or_else(Vec3::zeros)
}

/// The direction the active camera is looking, normalized.
pub fn camera_forward(world: &World) -> Vec3 {
    let Some(camera) = world.resources.active_camera else {
        return Vec3::new(0.0, 0.0, -1.0);
    };
    let matrix = crate::placement::world_matrix(world, camera);
    nalgebra_glm::vec3(-matrix[(0, 2)], -matrix[(1, 2)], -matrix[(2, 2)]).normalize()
}

/// Sets the active camera's vertical field of view in degrees. Wider sees more
/// with more distortion, narrower zooms in. Typical values are 45 to 75. Only
/// affects a perspective camera; on an orthographic one use [`set_orthographic`].
pub fn set_field_of_view(world: &mut World, degrees: f32) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    if let Some(component) = world.core.get_camera_mut(camera)
        && let Projection::Perspective(perspective) = &mut component.projection
    {
        perspective.y_fov_rad = degrees.to_radians();
    }
}

/// Switches the active camera to an orthographic projection: flat, with no
/// perspective foreshortening, for top-down, isometric, and 2d-ish looks.
/// `half_height` is half the visible height in world units; the width follows
/// the window aspect.
pub fn set_orthographic(world: &mut World, half_height: f32) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    let aspect = world
        .resources
        .window
        .cached_viewport_size
        .map(|(width, height)| width as f32 / height.max(1) as f32)
        .unwrap_or(16.0 / 9.0);
    if let Some(component) = world.core.get_camera_mut(camera) {
        component.projection = Projection::Orthographic(OrthographicCamera {
            x_mag: half_height * aspect,
            y_mag: half_height,
            z_near: 0.01,
            z_far: 1000.0,
        });
    }
}

/// Switches the active camera back to a perspective projection with the given
/// vertical field of view in degrees, undoing [`set_orthographic`].
pub fn set_perspective(world: &mut World, degrees: f32) {
    let Some(camera) = world.resources.active_camera else {
        return;
    };
    if let Some(component) = world.core.get_camera_mut(camera) {
        component.projection = Projection::Perspective(PerspectiveCamera {
            aspect_ratio: None,
            y_fov_rad: degrees.to_radians(),
            z_near: 0.1,
            z_far: Some(1000.0),
        });
    }
}

fn despawn_api_camera(world: &mut World) {
    #[cfg(feature = "physics")]
    if let Some(player) = lookup_named(world, PLAYER_NAME) {
        despawn_recursive_immediate(world, player);
        world.resources.entities.names.remove(PLAYER_NAME);
        set_cursor_locked(world, false);
        set_cursor_visible(world, true);
    }
    let api_camera = world.resources.active_camera.filter(|&camera| {
        world
            .core
            .get_name(camera)
            .is_some_and(|name| name.0.starts_with(CAMERA_NAME_PREFIX))
    });
    if let Some(camera) = api_camera {
        despawn_recursive_immediate(world, camera);
    }
    world.resources.active_camera = None;
}

fn activate_camera(world: &mut World, camera: Entity) {
    world.resources.active_camera = Some(camera);
    #[cfg(feature = "audio")]
    {
        world.core.add_components(camera, AUDIO_LISTENER);
        world.core.set_audio_listener(camera, AudioListener);
    }
}