nightshade 0.39.0

A cross-platform data-oriented game engine.
Documentation
//! Fire and forget interpolation of entity properties plus camera shake.
//!
//! Push a [`Tween`] onto `world.resources.tweens.active` and the
//! `update_tweens_system` entry in the default frame schedule advances it
//! every frame, applying eased values to the entity until the duration
//! elapses. Position and scale tweens write through the dirty-flagged
//! transform path. Color tweens write the entity's material base color in
//! place and mark it dirty for the renderer.
//!
//! Camera shake lives on the same resource. Set `strength` and the duration
//! fields on `world.resources.tweens.shake` and the system offsets the active
//! camera with a decaying jitter, removing the previous frame's offset first
//! so controllers that integrate translation do not drift.

use crate::ecs::generational_registry::registry_entry_by_name_mut;
use crate::ecs::primitives::EasingFunction;
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::world::{Entity, Vec3, World};

/// The property a [`Tween`] animates, with captured start and end values.
pub enum TweenTarget {
    Position { from: Vec3, to: Vec3 },
    Scale { from: Vec3, to: Vec3 },
    Color { from: [f32; 4], to: [f32; 4] },
}

/// One in-flight interpolation.
pub struct Tween {
    pub entity: Entity,
    pub target: TweenTarget,
    pub duration_seconds: f32,
    pub elapsed_seconds: f32,
    pub easing: EasingFunction,
}

/// Decaying positional jitter applied to the active camera.
#[derive(Default)]
pub struct Shake {
    pub strength: f32,
    pub duration_seconds: f32,
    pub remaining_seconds: f32,
    pub last_offset: Vec3,
}

/// All in-flight tweens and the camera shake state.
#[derive(Default)]
pub struct Tweens {
    pub active: Vec<Tween>,
    pub shake: Shake,
}

pub fn update_tweens_system(world: &mut World) {
    let delta = world.resources.window.timing.delta_time;

    let mut tweens = std::mem::take(&mut world.resources.tweens.active);
    tweens.retain_mut(|tween| {
        tween.elapsed_seconds += delta;
        let progress = if tween.duration_seconds <= f32::EPSILON {
            1.0
        } else {
            (tween.elapsed_seconds / tween.duration_seconds).min(1.0)
        };
        let eased = tween.easing.evaluate(progress);
        apply_tween(world, tween, eased);
        progress < 1.0
    });
    tweens.append(&mut world.resources.tweens.active);
    world.resources.tweens.active = tweens;

    apply_shake(world, delta);
}

fn apply_tween(world: &mut World, tween: &Tween, eased: f32) {
    match &tween.target {
        TweenTarget::Position { from, to } => {
            if let Some(transform) = world.core.get_local_transform_mut(tween.entity) {
                transform.translation = from + (to - from) * eased;
                mark_local_transform_dirty(world, tween.entity);
            }
        }
        TweenTarget::Scale { from, to } => {
            if let Some(transform) = world.core.get_local_transform_mut(tween.entity) {
                transform.scale = from + (to - from) * eased;
                mark_local_transform_dirty(world, tween.entity);
            }
        }
        TweenTarget::Color { from, to } => {
            let Some(material_name) = world
                .core
                .get_material_ref(tween.entity)
                .map(|material_ref| material_ref.name.clone())
            else {
                return;
            };
            if let Some(material) = registry_entry_by_name_mut(
                &mut world.resources.assets.material_registry.registry,
                &material_name,
            ) {
                for component in 0..4 {
                    material.base_color[component] =
                        from[component] + (to[component] - from[component]) * eased;
                }
                world
                    .resources
                    .mesh_render_state
                    .mark_material_dirty(tween.entity);
            }
        }
    }
}

fn apply_shake(world: &mut World, delta: f32) {
    let (offset, last_offset) = {
        let shake = &mut world.resources.tweens.shake;
        if shake.remaining_seconds <= 0.0 && shake.last_offset == Vec3::zeros() {
            return;
        }
        let falloff = if shake.duration_seconds <= f32::EPSILON {
            0.0
        } else {
            (shake.remaining_seconds / shake.duration_seconds).clamp(0.0, 1.0)
        };
        let time = world.resources.window.timing.uptime_milliseconds as f32 / 1000.0;
        let offset = if falloff > 0.0 {
            Vec3::new(
                (time * 35.0).sin(),
                (time * 41.0 + 1.3).sin(),
                (time * 47.0 + 2.6).sin(),
            ) * shake.strength
                * falloff
        } else {
            Vec3::zeros()
        };
        let last_offset = shake.last_offset;
        shake.remaining_seconds = (shake.remaining_seconds - delta).max(0.0);
        shake.last_offset = offset;
        (offset, last_offset)
    };

    let Some(camera) = world.resources.active_camera else {
        return;
    };
    if let Some(transform) = world.core.get_local_transform_mut(camera) {
        transform.translation += offset - last_offset;
        mark_local_transform_dirty(world, camera);
    }
}