nightshade 0.16.0

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::primitives::EasingFunction;
use nalgebra_glm::{Quat, Vec3};

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CutsceneShot {
    pub eye: Vec3,
    pub target: Vec3,
    pub field_of_view_degrees: f32,
    pub roll_degrees: f32,
}

impl CutsceneShot {
    pub fn new(eye: Vec3, target: Vec3) -> Self {
        Self {
            eye,
            target,
            field_of_view_degrees: 45.0,
            roll_degrees: 0.0,
        }
    }

    pub fn with_field_of_view(mut self, field_of_view_degrees: f32) -> Self {
        self.field_of_view_degrees = field_of_view_degrees;
        self
    }

    pub fn with_roll(mut self, roll_degrees: f32) -> Self {
        self.roll_degrees = roll_degrees;
        self
    }

    pub fn interpolate(self, other: CutsceneShot, factor: f32) -> CutsceneShot {
        CutsceneShot {
            eye: self.eye + (other.eye - self.eye) * factor,
            target: self.target + (other.target - self.target) * factor,
            field_of_view_degrees: self.field_of_view_degrees
                + (other.field_of_view_degrees - self.field_of_view_degrees) * factor,
            roll_degrees: self.roll_degrees + (other.roll_degrees - self.roll_degrees) * factor,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum CutsceneAction {
    CameraMove {
        from: CutsceneShot,
        to: CutsceneShot,
    },
    CameraFollow {
        actor: String,
        eye_offset: Vec3,
        look_height: f32,
        field_of_view_degrees: f32,
    },
    CameraShake {
        amplitude: f32,
        frequency: f32,
    },
    ActorMove {
        actor: String,
        from: Vec3,
        to: Vec3,
    },
    ActorTurn {
        actor: String,
        from_yaw_radians: f32,
        to_yaw_radians: f32,
    },
    ActorVisible {
        actor: String,
        visible: bool,
    },
    Dialogue {
        speaker: Option<String>,
        line: String,
    },
    Title {
        line: String,
    },
    Fade {
        from: f32,
        to: f32,
        color: Vec3,
    },
    Letterbox {
        from: f32,
        to: f32,
    },
    FocusPull {
        from_distance: f32,
        to_distance: f32,
        range: f32,
    },
}

#[derive(Clone, Debug, PartialEq)]
pub struct CutsceneEvent {
    pub start: f32,
    pub duration: f32,
    pub easing: EasingFunction,
    pub action: CutsceneAction,
}

impl CutsceneEvent {
    pub fn end(&self) -> f32 {
        self.start + self.duration
    }

    pub fn eased_progress(&self, time: f32) -> f32 {
        if self.duration <= 0.0 {
            return if time >= self.start { 1.0 } else { 0.0 };
        }
        let raw = ((time - self.start) / self.duration).clamp(0.0, 1.0);
        self.easing.evaluate(raw)
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct Cutscene {
    pub name: String,
    pub events: Vec<CutsceneEvent>,
}

impl Cutscene {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            events: Vec::new(),
        }
    }

    pub fn duration(&self) -> f32 {
        self.events
            .iter()
            .map(CutsceneEvent::end)
            .fold(0.0, f32::max)
    }

    fn push(
        mut self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        action: CutsceneAction,
    ) -> Self {
        self.events.push(CutsceneEvent {
            start,
            duration,
            easing,
            action,
        });
        self
    }

    pub fn camera(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from: CutsceneShot,
        to: CutsceneShot,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::CameraMove { from, to },
        )
    }

    pub fn camera_follow(
        self,
        start: f32,
        duration: f32,
        actor: impl Into<String>,
        eye_offset: Vec3,
        look_height: f32,
        field_of_view_degrees: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::CameraFollow {
                actor: actor.into(),
                eye_offset,
                look_height,
                field_of_view_degrees,
            },
        )
    }

    pub fn camera_shake(self, start: f32, duration: f32, amplitude: f32, frequency: f32) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::CameraShake {
                amplitude,
                frequency,
            },
        )
    }

    pub fn camera_cut(self, start: f32, shot: CutsceneShot) -> Self {
        self.push(
            start,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::CameraMove {
                from: shot,
                to: shot,
            },
        )
    }

    pub fn actor_move(
        self,
        actor: impl Into<String>,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from: Vec3,
        to: Vec3,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::ActorMove {
                actor: actor.into(),
                from,
                to,
            },
        )
    }

    pub fn actor_turn(
        self,
        actor: impl Into<String>,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from_yaw_radians: f32,
        to_yaw_radians: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::ActorTurn {
                actor: actor.into(),
                from_yaw_radians,
                to_yaw_radians,
            },
        )
    }

    pub fn actor_visible(self, actor: impl Into<String>, time: f32, visible: bool) -> Self {
        self.push(
            time,
            0.0,
            EasingFunction::Linear,
            CutsceneAction::ActorVisible {
                actor: actor.into(),
                visible,
            },
        )
    }

    pub fn dialogue(
        self,
        start: f32,
        duration: f32,
        speaker: Option<&str>,
        line: impl Into<String>,
    ) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::Dialogue {
                speaker: speaker.map(str::to_string),
                line: line.into(),
            },
        )
    }

    pub fn title(self, start: f32, duration: f32, line: impl Into<String>) -> Self {
        self.push(
            start,
            duration,
            EasingFunction::Linear,
            CutsceneAction::Title { line: line.into() },
        )
    }

    pub fn fade(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from: f32,
        to: f32,
        color: Vec3,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::Fade { from, to, color },
        )
    }

    pub fn fade_in(self, start: f32, duration: f32) -> Self {
        self.fade(
            start,
            duration,
            EasingFunction::QuadOut,
            1.0,
            0.0,
            Vec3::zeros(),
        )
    }

    pub fn fade_out(self, start: f32, duration: f32) -> Self {
        self.fade(
            start,
            duration,
            EasingFunction::QuadIn,
            0.0,
            1.0,
            Vec3::zeros(),
        )
    }

    pub fn letterbox(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from: f32,
        to: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::Letterbox { from, to },
        )
    }

    pub fn letterbox_in(self, start: f32, duration: f32) -> Self {
        self.letterbox(start, duration, EasingFunction::CubicOut, 0.0, 1.0)
    }

    pub fn letterbox_out(self, start: f32, duration: f32) -> Self {
        self.letterbox(start, duration, EasingFunction::CubicIn, 1.0, 0.0)
    }

    pub fn focus_pull(
        self,
        start: f32,
        duration: f32,
        easing: EasingFunction,
        from_distance: f32,
        to_distance: f32,
        range: f32,
    ) -> Self {
        self.push(
            start,
            duration,
            easing,
            CutsceneAction::FocusPull {
                from_distance,
                to_distance,
                range,
            },
        )
    }
}

pub fn camera_look_rotation(eye: Vec3, target: Vec3, roll_degrees: f32) -> Quat {
    let direction = target - eye;
    if direction.magnitude_squared() < f32::EPSILON {
        return Quat::identity();
    }
    let direction = direction.normalize();
    let yaw = (-direction.x).atan2(-direction.z);
    let pitch = (-direction.y).clamp(-1.0, 1.0).asin();
    let yaw_rotation = nalgebra_glm::quat_angle_axis(yaw, &Vec3::y());
    let pitch_rotation = nalgebra_glm::quat_angle_axis(-pitch, &Vec3::x());
    let look = yaw_rotation * pitch_rotation;
    if roll_degrees.abs() < f32::EPSILON {
        return look;
    }
    let roll = nalgebra_glm::quat_angle_axis(roll_degrees.to_radians(), &direction);
    roll * look
}