nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use crate::tui::ecs::events::Message;
use crate::tui::ecs::resources::MouseButton;
use crate::tui::ecs::world::{self, LABEL, POSITION, SPRITE, TILEMAP, World};
use crate::tui::key_code::KeyCode;
use crate::tui::run::State;
use crate::tui::terminal::input;
use crate::tui::terminal::render::{Cell, Renderer};
use crossterm::{cursor, execute, terminal};
use std::io::{self, Stdout};
use std::time::{Duration, Instant};

struct TerminalGuard {
    stdout: Stdout,
}

impl TerminalGuard {
    fn new() -> io::Result<Self> {
        let mut stdout = io::stdout();
        terminal::enable_raw_mode()?;
        execute!(
            stdout,
            terminal::EnterAlternateScreen,
            cursor::Hide,
            terminal::Clear(terminal::ClearType::All),
            crossterm::event::EnableMouseCapture
        )?;
        Ok(Self { stdout })
    }

    fn stdout_mut(&mut self) -> &mut Stdout {
        &mut self.stdout
    }
}

impl Drop for TerminalGuard {
    fn drop(&mut self) {
        let _ = execute!(
            self.stdout,
            crossterm::event::DisableMouseCapture,
            cursor::Show,
            terminal::LeaveAlternateScreen
        );
        let _ = terminal::disable_raw_mode();
    }
}

pub fn launch(mut state: Box<dyn State>) -> Result<(), Box<dyn std::error::Error>> {
    let mut world = World::default();

    let (columns, rows) = terminal::size()?;
    world.resources.terminal_size.columns = columns;
    world.resources.terminal_size.rows = rows;

    let mut renderer = Renderer::new(columns, rows);
    let mut guard = TerminalGuard::new()?;

    let title = state.title().to_string();
    if !title.is_empty() {
        execute!(guard.stdout_mut(), terminal::SetTitle(&title))?;
    }

    state.initialize(&mut world);

    let mut last_frame_time = Instant::now();
    let mut prev_mouse_column: u16 = 0;
    let mut prev_mouse_row: u16 = 0;

    loop {
        let now = Instant::now();
        let delta = now.duration_since(last_frame_time);
        last_frame_time = now;

        world.resources.timing.delta_seconds = delta.as_secs_f64();
        world.resources.timing.elapsed += delta.as_secs_f64();
        world.resources.timing.frame_count += 1;

        input::poll_input(&mut world);

        let just_pressed: Vec<KeyCode> = world
            .resources
            .keyboard
            .just_pressed
            .iter()
            .copied()
            .collect();
        let just_released: Vec<KeyCode> = world
            .resources
            .keyboard
            .just_released
            .iter()
            .copied()
            .collect();

        for key in &just_pressed {
            state.on_keyboard_input(&mut world, *key, true);
        }
        for key in &just_released {
            state.on_keyboard_input(&mut world, *key, false);
        }

        if world.resources.mouse.left_just_pressed {
            let column = world.resources.mouse.column;
            let row = world.resources.mouse.row;
            state.on_mouse_input(&mut world, MouseButton::Left, column, row, true);
        }
        if world.resources.mouse.right_just_pressed {
            let column = world.resources.mouse.column;
            let row = world.resources.mouse.row;
            state.on_mouse_input(&mut world, MouseButton::Right, column, row, true);
        }
        if world.resources.mouse.middle_just_pressed {
            let column = world.resources.mouse.column;
            let row = world.resources.mouse.row;
            state.on_mouse_input(&mut world, MouseButton::Middle, column, row, true);
        }
        if world.resources.mouse.left_just_released {
            let column = world.resources.mouse.column;
            let row = world.resources.mouse.row;
            state.on_mouse_input(&mut world, MouseButton::Left, column, row, false);
        }
        if world.resources.mouse.right_just_released {
            let column = world.resources.mouse.column;
            let row = world.resources.mouse.row;
            state.on_mouse_input(&mut world, MouseButton::Right, column, row, false);
        }
        if world.resources.mouse.middle_just_released {
            let column = world.resources.mouse.column;
            let row = world.resources.mouse.row;
            state.on_mouse_input(&mut world, MouseButton::Middle, column, row, false);
        }

        let current_mouse_column = world.resources.mouse.column;
        let current_mouse_row = world.resources.mouse.row;
        if current_mouse_column != prev_mouse_column || current_mouse_row != prev_mouse_row {
            prev_mouse_column = current_mouse_column;
            prev_mouse_row = current_mouse_row;
            state.on_mouse_move(&mut world, current_mouse_column, current_mouse_row);
        }

        state.run_systems(&mut world);

        let messages: Vec<Message> = world.resources.event_bus.messages.drain(..).collect();
        for message in &messages {
            state.handle_event(&mut world, message);
        }

        world::apply_commands(&mut world);

        if let Some(next) = state.next_state(&mut world) {
            state = next;
            state.initialize(&mut world);
            continue;
        }

        let current_columns = world.resources.terminal_size.columns;
        let current_rows = world.resources.terminal_size.rows;
        if renderer.width != current_columns || renderer.height != current_rows {
            renderer.resize(current_columns, current_rows);
        }

        render_frame(&world, &mut renderer);
        renderer.present(guard.stdout_mut())?;

        if world.resources.should_exit {
            break;
        }

        let target_fps = world.resources.timing.target_fps;
        if target_fps > 0 {
            let frame_duration = Duration::from_secs_f64(1.0 / target_fps as f64);
            let elapsed_this_frame = Instant::now().duration_since(now);
            if elapsed_this_frame < frame_duration {
                std::thread::sleep(frame_duration - elapsed_this_frame);
            }
        }
    }

    Ok(())
}

enum DrawableKind {
    Sprite,
    Label,
    Tilemap,
}

fn render_frame(world: &World, renderer: &mut Renderer) {
    renderer.clear();

    let camera = world.resources.camera;

    let mut drawables: Vec<(i32, freecs::Entity, DrawableKind)> = Vec::new();

    for entity in world.query_entities(POSITION | SPRITE) {
        let has_label = world.get_label(entity).is_some();
        let has_tilemap = world.get_tilemap(entity).is_some();
        if has_label || has_tilemap {
            continue;
        }
        let visible = world
            .get_visibility(entity)
            .is_none_or(|visibility| visibility.visible);
        if !visible {
            continue;
        }
        let z = world.get_z_index(entity).map_or(0, |z_index| z_index.0);
        drawables.push((z, entity, DrawableKind::Sprite));
    }

    for entity in world.query_entities(POSITION | LABEL) {
        let visible = world
            .get_visibility(entity)
            .is_none_or(|visibility| visibility.visible);
        if !visible {
            continue;
        }
        let z = world.get_z_index(entity).map_or(0, |z_index| z_index.0);
        drawables.push((z, entity, DrawableKind::Label));
    }

    for entity in world.query_entities(POSITION | TILEMAP) {
        let visible = world
            .get_visibility(entity)
            .is_none_or(|visibility| visibility.visible);
        if !visible {
            continue;
        }
        let z = world.get_z_index(entity).map_or(0, |z_index| z_index.0);
        drawables.push((z, entity, DrawableKind::Tilemap));
    }

    drawables.sort_by_key(|(z, _, _)| *z);

    for (_, entity, kind) in &drawables {
        let Some(position) = world.get_position(*entity) else {
            continue;
        };

        let screen_column = (position.column - camera.offset_column).round() as i32;
        let screen_row = (position.row - camera.offset_row).round() as i32;

        match kind {
            DrawableKind::Sprite => {
                let Some(sprite) = world.get_sprite(*entity) else {
                    continue;
                };
                if screen_column >= 0
                    && screen_column < renderer.width as i32
                    && screen_row >= 0
                    && screen_row < renderer.height as i32
                {
                    renderer.set_cell(
                        screen_column as u16,
                        screen_row as u16,
                        Cell {
                            character: sprite.character,
                            foreground: sprite.foreground.to_crossterm_color(),
                            background: sprite.background.to_crossterm_color(),
                        },
                    );
                }
            }
            DrawableKind::Label => {
                let Some(label) = world.get_label(*entity) else {
                    continue;
                };
                let foreground = label.foreground.to_crossterm_color();
                let background = label.background.to_crossterm_color();
                for (char_index, character) in label.text.chars().enumerate() {
                    let col = screen_column + char_index as i32;
                    if col >= 0
                        && col < renderer.width as i32
                        && screen_row >= 0
                        && screen_row < renderer.height as i32
                    {
                        renderer.set_cell(
                            col as u16,
                            screen_row as u16,
                            Cell {
                                character,
                                foreground,
                                background,
                            },
                        );
                    }
                }
            }
            DrawableKind::Tilemap => {
                let Some(tilemap) = world.get_tilemap(*entity) else {
                    continue;
                };
                for tile_row in 0..tilemap.height {
                    for tile_column in 0..tilemap.width {
                        let cell = &tilemap.cells[tile_row * tilemap.width + tile_column];
                        if cell.character == '\0' {
                            continue;
                        }
                        let col = screen_column + tile_column as i32;
                        let row = screen_row + tile_row as i32;
                        if col >= 0
                            && col < renderer.width as i32
                            && row >= 0
                            && row < renderer.height as i32
                        {
                            renderer.set_cell(
                                col as u16,
                                row as u16,
                                Cell {
                                    character: cell.character,
                                    foreground: cell.foreground.to_crossterm_color(),
                                    background: cell.background.to_crossterm_color(),
                                },
                            );
                        }
                    }
                }
            }
        }
    }
}