nightshade 0.43.0

A cross-platform data-oriented game engine.
Documentation
use std::collections::{HashMap, VecDeque};

use crate::ecs::cutscene::components::{Cutscene, CutsceneShot};
use crate::ecs::world::{Entity, World};

#[derive(Clone, Copy, Debug)]
pub struct CutsceneOverlay {
    pub canvas: Entity,
    pub title: Entity,
    pub speaker: Entity,
    pub dialogue: Entity,
    pub dialogue_panel: Entity,
}

#[derive(Clone, Debug)]
pub struct CutsceneDirector {
    pub active: Option<Cutscene>,
    pub time: f32,
    pub speed: f32,
    pub playing: bool,
    pub looping: bool,
    pub finished: bool,
    pub camera: Option<Entity>,
    pub bindings: HashMap<String, Entity>,
    pub overlay: Option<CutsceneOverlay>,
    pub dialogue_characters_per_second: f32,
    pub hold_on_finish: bool,
    pub holding: bool,
    pub queue: VecDeque<Cutscene>,
    /// Marker ids fired by `Trigger` actions since the host last drained them.
    /// Drain with [`take_cutscene_markers`] each frame and react in game code.
    pub fired_markers: Vec<String>,
    /// Optional table mapping dialogue and title strings to display text. A line
    /// not present in the table is shown verbatim, so this is opt-in.
    pub localization: HashMap<String, String>,
    pub music_entity: Option<Entity>,
    pub music_track: Option<String>,
    pub sound_oneshots: Vec<(Entity, f32)>,
    pub camera_smoothed: Option<CutsceneShot>,
    /// Time below which discrete events (triggers, sounds, music) have already
    /// fired this pass. Starts below zero so events at time zero still fire.
    pub marker_cursor: f32,
}

impl Default for CutsceneDirector {
    fn default() -> Self {
        Self {
            active: None,
            time: 0.0,
            speed: 1.0,
            playing: false,
            looping: false,
            finished: false,
            camera: None,
            bindings: HashMap::new(),
            overlay: None,
            dialogue_characters_per_second: 38.0,
            hold_on_finish: false,
            holding: false,
            queue: VecDeque::new(),
            fired_markers: Vec::new(),
            localization: HashMap::new(),
            music_entity: None,
            music_track: None,
            sound_oneshots: Vec::new(),
            camera_smoothed: None,
            marker_cursor: -1.0,
        }
    }
}

pub fn bind_cutscene_actor(world: &mut World, name: impl Into<String>, entity: Entity) {
    world
        .resources
        .cutscene
        .bindings
        .insert(name.into(), entity);
}

pub fn set_cutscene_camera(world: &mut World, camera: Entity) {
    world.resources.cutscene.camera = Some(camera);
}

pub fn play_cutscene(world: &mut World, cutscene: Cutscene) {
    if world.resources.cutscene.camera.is_none() {
        world.resources.cutscene.camera = world.resources.active_camera;
    }
    let director = &mut world.resources.cutscene;
    director.active = Some(cutscene);
    director.time = 0.0;
    director.playing = true;
    director.finished = false;
    director.holding = false;
    director.queue.clear();
    director.marker_cursor = -1.0;
    director.camera_smoothed = None;
    director.fired_markers.clear();
    if director.speed <= 0.0 {
        director.speed = 1.0;
    }
}

pub fn play_cutscene_reel(world: &mut World, cutscenes: impl IntoIterator<Item = Cutscene>) {
    let mut iterator = cutscenes.into_iter();
    let Some(first) = iterator.next() else {
        return;
    };
    play_cutscene(world, first);
    world.resources.cutscene.queue.extend(iterator);
}

pub fn queue_cutscene(world: &mut World, cutscene: Cutscene) {
    world.resources.cutscene.queue.push_back(cutscene);
}

pub fn stop_cutscene(world: &mut World) {
    let director = &mut world.resources.cutscene;
    director.playing = false;
    director.holding = false;
    director.queue.clear();
}

pub fn pause_cutscene(world: &mut World) {
    let director = &mut world.resources.cutscene;
    if director.active.is_some() {
        director.playing = false;
        director.holding = true;
    }
}

pub fn resume_cutscene(world: &mut World) {
    let director = &mut world.resources.cutscene;
    if director.active.is_some() {
        director.playing = true;
        director.holding = false;
        director.finished = false;
    }
}

/// Jumps the active cutscene to `seconds`, clamped to its duration. Seeking
/// backward replays any markers, sounds, or music between the new time and
/// where playback next advances.
pub fn seek_cutscene(world: &mut World, seconds: f32) {
    let duration = world
        .resources
        .cutscene
        .active
        .as_ref()
        .map(Cutscene::duration)
        .unwrap_or(0.0);
    let clamped = seconds.clamp(0.0, duration);
    world.resources.cutscene.time = clamped;
    world.resources.cutscene.marker_cursor = clamped;
    world.resources.cutscene.camera_smoothed = None;
}

/// Removes and returns the marker ids fired since the last call. Call once per
/// frame and react to each id in game code.
pub fn take_cutscene_markers(world: &mut World) -> Vec<String> {
    std::mem::take(&mut world.resources.cutscene.fired_markers)
}

pub fn set_cutscene_localization(world: &mut World, table: HashMap<String, String>) {
    world.resources.cutscene.localization = table;
}

pub fn cutscene_playing(world: &World) -> bool {
    world.resources.cutscene.playing
}

pub fn cutscene_finished(world: &World) -> bool {
    world.resources.cutscene.finished
}

pub fn cutscene_time(world: &World) -> f32 {
    world.resources.cutscene.time
}

pub fn cutscene_progress(world: &World) -> f32 {
    let director = &world.resources.cutscene;
    match &director.active {
        Some(cutscene) => {
            let duration = cutscene.duration();
            if duration <= 0.0 {
                1.0
            } else {
                (director.time / duration).clamp(0.0, 1.0)
            }
        }
        None => 0.0,
    }
}