use minifb::{Key, Window, WindowOptions};
use neurodoom::engine::ClassicEngine;
use neurodoom::math::{SCREENHEIGHT, SCREENWIDTH};
use neurodoom::render::SemanticClass;
use neurodoom::rules::PlayerAction;
use neurodoom::types::Button;
use neurodoom::world::{LevelExit, PeerId};
#[path = "common.rs"]
mod common;
const SCALE: usize = 3;
fn main() {
let (wad_data, wad_path) = common::load_wad()
.unwrap_or_else(|e| panic!("failed to read WAD at {wad_path:?}: {e}",
wad_path = std::env::var_os("DOOM_WAD").unwrap_or_else(|| common::DEFAULT_WAD_PATH.into())));
let mut map_name = common::arg_or("E1M1");
eprintln!("Loaded WAD {wad_path:?}, starting on {map_name}");
let mut engine = ClassicEngine::new(&wad_data, &map_name)
.unwrap_or_else(|e| panic!("failed to init engine: {e}"));
let mut window = Window::new(
"neurodoom — WASD:move ←→:turn Space:use E:shoot Tab:view 1-7:weapon",
SCREENWIDTH * SCALE,
SCREENHEIGHT * SCALE,
WindowOptions::default(),
)
.expect("failed to create window");
window.set_target_fps(35);
arm_player(&mut engine);
eprintln!(
"Map things: {}, Entities: {}",
engine.map().things.len(),
engine.world().entity_count()
);
let mut fb = vec![0u32; SCREENWIDTH * SCREENHEIGHT];
let mut view_mode: u8 = 0;
while window.is_open() && !window.is_key_down(Key::Escape) {
let cmd = build_cmd(&window);
if window.is_key_pressed(Key::Tab, minifb::KeyRepeat::No) {
view_mode = (view_mode + 1) % 3;
}
engine.tick_single(PeerId(0), cmd);
if engine.level_complete() {
let exit = engine.level_exit();
match next_map(&map_name, exit == LevelExit::Secret) {
Some(next) => {
eprintln!("{map_name} cleared ({exit:?}); loading {next}");
map_name = next;
if let Err(e) = engine.load_map(&wad_data, &map_name) {
eprintln!("could not load {map_name}: {e}");
break;
}
engine.spawn_map_things(PeerId(0));
arm_player(&mut engine);
}
None => {
eprintln!("{map_name} cleared ({exit:?}); end of episode");
break;
}
}
}
match view_mode {
1 => {
for (i, &class) in engine.semantic_buffer().iter().enumerate() {
fb[i] = SemanticClass::from_u8(class).to_rgb();
}
}
2 => {
for (i, &d) in engine.depth_buffer().iter().enumerate() {
fb[i] = common::depth_to_rgb(d);
}
}
_ => {
let rgba = engine.framebuffer();
for (i, pixel) in fb.iter_mut().enumerate() {
let base = i * 4;
let r = rgba[base] as u32;
let g = rgba[base + 1] as u32;
let b = rgba[base + 2] as u32;
*pixel = (r << 16) | (g << 8) | b;
}
}
}
window
.update_with_buffer(&fb, SCREENWIDTH, SCREENHEIGHT)
.unwrap();
}
}
fn arm_player(engine: &mut ClassicEngine) {
if let Some(pid) = engine.world().controlled_by(PeerId(0))
&& let Some(ps) = engine.world_mut().player_state_mut(pid)
{
ps.weapon_owned = [true, true, true, true, true, false, false, true, false];
ps.ammo = [999, 99, 999, 99];
}
}
fn next_map(current: &str, secret: bool) -> Option<String> {
let b = current.as_bytes();
if b.len() != 4 || b[0] != b'E' || b[2] != b'M' {
return None;
}
let ep = b[1].checked_sub(b'0')?;
let map = b[3].checked_sub(b'0')?;
if !(1..=3).contains(&ep) || !(1..=9).contains(&map) {
return None;
}
let next_map = match (map, secret) {
(3, true) => 9, (9, _) => 4, (8, _) => return None, (m, _) => m + 1,
};
Some(format!("E{ep}M{next_map}"))
}
fn build_cmd(window: &Window) -> PlayerAction {
let mut cmd = PlayerAction::default();
if window.is_key_down(Key::W) || window.is_key_down(Key::Up) {
cmd.forward_move = 25;
}
if window.is_key_down(Key::S) || window.is_key_down(Key::Down) {
cmd.forward_move = -25;
}
if window.is_key_down(Key::A) {
cmd.side_move = -20;
}
if window.is_key_down(Key::D) {
cmd.side_move = 20;
}
if window.is_key_down(Key::Left) {
cmd.angle_turn = 512;
}
if window.is_key_down(Key::Right) {
cmd.angle_turn = -512;
}
if window.is_key_down(Key::Space) {
cmd.buttons |= Button::Use;
}
if window.is_key_down(Key::E) || window.is_key_down(Key::LeftCtrl) {
cmd.buttons |= Button::Attack;
}
for (key, slot) in [
(Key::Key1, 1), (Key::Key2, 2), (Key::Key3, 3), (Key::Key4, 4),
(Key::Key5, 5), (Key::Key6, 8),
] {
if window.is_key_down(key) {
cmd.weapon_select = slot;
break;
}
}
cmd
}