freenukum 0.4.0

A clone of the 1991 DOS game Duke Nukem 1
Documentation
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: Wolfgang Silbermayr <wolfgang@silbermayr.at>

use anyhow::{Error, Result};
use freenukum::{
    data::original_data_dir,
    game,
    graphics::load_default_font,
    hero::{Hero, Motion},
    level::tiles::LevelTiles,
    rendering::{CanvasRenderer, Renderer},
    settings::Settings,
    tilecache::TileCache,
    DefaultSizes, HorizontalDirection, Sizes, UserEvent, BACKDROP_HEIGHT,
    BACKDROP_WIDTH, GAME_INTERVAL,
};
use sdl2::{
    event::{Event, WindowEvent},
    keyboard::Keycode,
    mouse::MouseButton,
    pixels::Color,
};
use std::collections::HashSet;

fn main() -> Result<()> {
    const VERSION: &str = env!("CARGO_PKG_VERSION");

    let settings = Settings::load_or_create();
    let sdl_context = sdl2::init().map_err(Error::msg)?;
    let video_subsystem = sdl_context.video().map_err(Error::msg)?;
    let ttf_context = sdl2::ttf::init()?;
    let event_subsystem = sdl_context.event().map_err(Error::msg)?;
    let timer_subsystem = sdl_context.timer().map_err(Error::msg)?;
    let mut event_pump = sdl_context.event_pump().map_err(Error::msg)?;

    event_subsystem
        .register_custom_event::<UserEvent>()
        .map_err(Error::msg)?;

    let sizes = DefaultSizes;

    let win_width = BACKDROP_WIDTH * sizes.width();
    let win_height = BACKDROP_HEIGHT * sizes.height();
    let window = game::create_window(
        win_width,
        win_height,
        settings.fullscreen,
        &format!("Freenukum {} hero example", VERSION),
        &video_subsystem,
    )?;

    let mut canvas = window.into_canvas().present_vsync().build()?;
    canvas.set_draw_color(Color::RGB(0, 0, 0));
    canvas.clear();
    canvas.present();
    let texture_creator = canvas.texture_creator();

    game::check_episodes(
        &mut canvas,
        &load_default_font(&ttf_context)?,
        &texture_creator,
        &mut event_pump,
    )?;
    let tilecache = TileCache::load_from_path(&original_data_dir())?;

    let mut hero = Hero::new(&sizes);

    hero.position.geometry.x =
        (win_width as i32 - hero.position.geometry.width() as i32) / 2;
    hero.position.geometry.y =
        (win_height as i32 - hero.position.geometry.height() as i32) / 2;

    let mut renderer = CanvasRenderer {
        canvas: &mut canvas,
        texture_creator: &texture_creator,
        tileprovider: &tilecache,
    };

    let tiles = LevelTiles::new_all_solid();
    hero.render(
        &mut renderer,
        &sizes,
        &tiles,
        settings.draw_collision_bounds,
    )?;
    renderer.canvas.present();

    let mut directions = HashSet::new();

    let event_sender = event_subsystem.event_sender();

    let timer = timer_subsystem.add_timer(
        GAME_INTERVAL,
        Box::new(move || {
            event_sender.push_custom_event(UserEvent::Timer).unwrap();
            GAME_INTERVAL
        }),
    );

    let mut game_commands = game::GameCommandQueue::new();

    'event_loop: loop {
        match event_pump.wait_event() {
            Event::Quit { .. }
            | Event::KeyDown {
                keycode: Some(Keycode::Escape),
                ..
            }
            | Event::KeyDown {
                keycode: Some(Keycode::Q),
                ..
            } => break 'event_loop,
            Event::KeyDown {
                keycode: Some(Keycode::Right),
                ..
            } => {
                directions.insert(HorizontalDirection::Right);
                if directions.contains(&HorizontalDirection::Left) {
                    hero.motion = Motion::NotMoving;
                } else {
                    hero.motion = Motion::Walking;
                    hero.direction = HorizontalDirection::Right;
                }
            }
            Event::KeyDown {
                keycode: Some(Keycode::Left),
                ..
            } => {
                directions.insert(HorizontalDirection::Left);
                if directions.contains(&HorizontalDirection::Right) {
                    hero.motion = Motion::NotMoving;
                } else {
                    hero.motion = Motion::Walking;
                    hero.direction = HorizontalDirection::Left;
                }
            }
            Event::KeyDown {
                keycode: Some(Keycode::LAlt),
                ..
            }
            | Event::MouseButtonDown {
                mouse_btn: MouseButton::Left,
                ..
            } => {
                hero.is_shooting = true;
            }
            Event::KeyUp {
                keycode: Some(Keycode::LAlt),
                ..
            }
            | Event::MouseButtonUp {
                mouse_btn: MouseButton::Left,
                ..
            } => {
                hero.is_shooting = false;
            }
            Event::KeyDown {
                keycode: Some(Keycode::LCtrl),
                ..
            }
            | Event::MouseButtonDown {
                mouse_btn: MouseButton::Right,
                ..
            } => {
                hero.jump(&sizes, &tiles, &mut game_commands);
            }
            Event::KeyUp {
                keycode: Some(Keycode::Right),
                ..
            } => {
                directions.remove(&HorizontalDirection::Right);
                if directions.contains(&HorizontalDirection::Left) {
                    hero.motion = Motion::Walking;
                    hero.direction = HorizontalDirection::Left;
                } else {
                    hero.motion = Motion::NotMoving;
                }
            }
            Event::KeyUp {
                keycode: Some(Keycode::Left),
                ..
            } => {
                directions.remove(&HorizontalDirection::Left);
                if directions.contains(&HorizontalDirection::Right) {
                    hero.motion = Motion::Walking;
                    hero.direction = HorizontalDirection::Right;
                } else {
                    hero.motion = Motion::NotMoving;
                }
            }
            e if e.is_user_event() => {
                if e.as_user_event_type::<UserEvent>()
                    == Some(UserEvent::Timer)
                {
                    renderer.fill(Color::RGB(0, 0, 0))?;
                    hero.act(&sizes, &tiles, &mut game_commands)?;
                    hero.render(
                        &mut renderer,
                        &sizes,
                        &tiles,
                        settings.draw_collision_bounds,
                    )?;
                    renderer.canvas.present();
                }
            }
            Event::Window {
                win_event: WindowEvent::Exposed,
                ..
            }
            | Event::Window {
                win_event: WindowEvent::Shown,
                ..
            } => {
                renderer.canvas.present();
            }
            _ => {}
        }
    }
    drop(timer);
    Ok(())
}