nightshade-editor 0.29.0

Interactive map editor for the Nightshade game engine
use nightshade::prelude::*;
use nightshade::shell::{Command, ShellState, shell_retained_ui};

/// Authoring intents the cutscene shell commands queue for the editor to apply
/// (the command callback only sees `World`, not `EditorWorld`).
pub enum CutsceneCommand {
    AddPoint,
    Clear,
    Save(String),
    Load(String),
    Preview,
    Stop,
}

#[derive(Default)]
pub struct EditorShellContext {
    pub cutscene_commands: Vec<CutsceneCommand>,
}

pub fn new_shell() -> ShellState<EditorShellContext> {
    let mut shell = ShellState::new(EditorShellContext::default());
    shell.register_builtin_commands();
    shell.registry.register(Command {
        name: "cutscene",
        description: "Author a camera-path cutscene with draggable waypoints",
        usage: "cutscene <point|clear|save <name>|load <name>|preview|stop>",
        execute: cutscene_command,
    });
    shell.registry.register(Command {
        name: "teleport",
        description: "Teleport the play-mode player to the first entity with a tag",
        usage: "teleport <tag>",
        execute: teleport_command,
    });
    shell
}

fn teleport_command(args: &[&str], world: &mut World, _context: &mut EditorShellContext) -> String {
    let Some(tag) = args.first() else {
        return "Usage: teleport <tag>".to_string();
    };
    let target = world
        .resources
        .entities
        .tags
        .iter()
        .find(|(_, tags)| tags.iter().any(|entry| entry == tag))
        .map(|(entity, _)| *entity)
        .and_then(|entity| world.core.get_global_transform(entity))
        .map(|transform| transform.translation());
    let Some(position) = target else {
        return format!("No entity tagged '{tag}'");
    };
    let Some(player) = world
        .core
        .query_entities(nightshade::ecs::world::CHARACTER_CONTROLLER)
        .next()
    else {
        return "No player in the level (enter play mode first)".to_string();
    };
    if let Some(transform) = world.core.get_local_transform_mut(player) {
        transform.translation = position + Vec3::new(0.0, 0.6, 0.0);
    }
    mark_local_transform_dirty(world, player);
    nightshade::ecs::physics::commands::reset_physics_interpolation(world, player);
    if let Some(controller) = world.core.get_character_controller_mut(player) {
        controller.velocity = Vec3::zeros();
    }
    format!("Teleported player to '{tag}'")
}

fn cutscene_command(args: &[&str], _world: &mut World, context: &mut EditorShellContext) -> String {
    match args.first().copied() {
        Some("point") | Some("add") => {
            context.cutscene_commands.push(CutsceneCommand::AddPoint);
            "Added a waypoint from the current view".to_string()
        }
        Some("clear") => {
            context.cutscene_commands.push(CutsceneCommand::Clear);
            "Cleared the camera path".to_string()
        }
        Some("save") => match args.get(1) {
            Some(name) => {
                context
                    .cutscene_commands
                    .push(CutsceneCommand::Save((*name).to_string()));
                format!("Saving cutscene '{name}'")
            }
            None => "Usage: cutscene save <name>".to_string(),
        },
        Some("load") => match args.get(1) {
            Some(name) => {
                context
                    .cutscene_commands
                    .push(CutsceneCommand::Load((*name).to_string()));
                format!("Loading cutscene '{name}'")
            }
            None => "Usage: cutscene load <name>".to_string(),
        },
        Some("preview") | Some("play") => {
            context.cutscene_commands.push(CutsceneCommand::Preview);
            "Previewing the cutscene".to_string()
        }
        Some("stop") => {
            context.cutscene_commands.push(CutsceneCommand::Stop);
            "Stopped the preview".to_string()
        }
        _ => "Usage: cutscene <point|clear|save <name>|load <name>|preview|stop>".to_string(),
    }
}

pub fn run(shell: &mut ShellState<EditorShellContext>, world: &mut World) {
    if shell.visible {
        let alt_held = world
            .resources
            .input
            .keyboard
            .is_key_pressed(KeyCode::AltLeft)
            || world
                .resources
                .input
                .keyboard
                .is_key_pressed(KeyCode::AltRight);
        if !alt_held {
            for character in world.resources.input.keyboard.frame_chars.clone() {
                if !character.is_control() {
                    shell.input_buffer.push(character);
                }
            }
        }
    }
    let delta_time = world.resources.window.timing.delta_time;
    shell.update_animation(delta_time);
    shell_retained_ui(shell, world);
}

pub fn handle_key(
    shell: &mut ShellState<EditorShellContext>,
    world: &mut World,
    key_code: KeyCode,
    key_state: ElementState,
) {
    let pressed = key_state == ElementState::Pressed;
    if pressed {
        let alt_pressed = world
            .resources
            .input
            .keyboard
            .is_key_pressed(KeyCode::AltLeft)
            || world
                .resources
                .input
                .keyboard
                .is_key_pressed(KeyCode::AltRight);
        if key_code == KeyCode::KeyC && alt_pressed {
            shell.toggle();
            return;
        }
    }
    if shell.visible {
        shell.handle_key(key_code, pressed);
    }
}

pub fn is_capturing_input(shell: &ShellState<EditorShellContext>) -> bool {
    shell.visible || shell.dragging_resize || shell.dragging_scrollbar
}