diff --git a/Cargo.toml b/Cargo.toml index b45a62d..20377ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,10 @@ crossterm = "0.28" clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -minifb = "0.27" fontdue = "0.9" +winit = "0.30" +softbuffer = "0.4" +log = "0.4" [profile.release] opt-level = 3 diff --git a/src/main.rs b/src/main.rs index ea94195..271d624 100644 --- a/src/main.rs +++ b/src/main.rs @@ -202,6 +202,28 @@ fn player_info(game: &Game) -> String { } fn run_window_mode() { + use winit::event::{Event, WindowEvent}; + use winit::event_loop::{EventLoop, ControlFlow}; + use winit::window::Window; + + use std::time::{Duration, Instant}; + + use std::sync::Arc; + + let event_loop = EventLoop::new().expect("Failed to create event loop"); + let window = event_loop.create_window( + Window::default_attributes() + .with_title("Verbatim") + .with_inner_size(winit::dpi::LogicalSize::new(160 * 8, 50 * 16)) + ).expect("Failed to create window"); + let window = Arc::new(window); + + let display_handle = event_loop.owned_display_handle(); + let context = softbuffer::Context::new(display_handle) + .expect("Failed to create softbuffer context"); + let mut surface = softbuffer::Surface::new(&context, Arc::clone(&window)) + .expect("Failed to create surface"); + let mut renderer = WindowRenderer::new(); let mut game = Game::new(); game.init_world(); @@ -210,82 +232,116 @@ fn run_window_mode() { let vw = renderer.width(); let vh = renderer.height(); - use std::time::{Duration, Instant}; let fixed_dt = Duration::from_millis(16); let mut last_time = Instant::now(); let mut accumulator = Duration::ZERO; - while renderer.is_open() && game.running { - let now = Instant::now(); - let frame_time = now.duration_since(last_time); - last_time = now; - accumulator += frame_time; + let mut running = true; - while accumulator >= fixed_dt { - game.fixed_update(); - accumulator -= fixed_dt; - } + event_loop.run(|event, ctrl| { + ctrl.set_control_flow(ControlFlow::Poll); - let keys = renderer.get_keys_down(); - input.update(&keys); + match event { + Event::WindowEvent { event, .. } => { + match event { + WindowEvent::CloseRequested => { + running = false; + ctrl.exit(); + } + WindowEvent::KeyboardInput { event: key_event, .. } => { + input.on_key_event(key_event.physical_key, key_event.state); + } + _ => {} + } + } + Event::AboutToWait => { + if !running { + ctrl.exit(); + return; + } - if input.quit { - break; - } + let now = Instant::now(); + let frame_time = now.duration_since(last_time); + last_time = now; + accumulator += frame_time; - if input.jump { - let on_ground = game.check_on_ground(); - game.player.jump(&mut game.entities, on_ground); - } + let mut steps = 0; + while accumulator >= fixed_dt && steps < 5 { + game.fixed_update(); + accumulator -= fixed_dt; + steps += 1; + } - if input.left { - game.player.move_left(&mut game.entities); - } else if input.right { - game.player.move_right(&mut game.entities); - } else { - game.player.stop_horizontal(&mut game.entities); - } + input.update(); - if input.cam_left { game.cam_x -= 3; } - if input.cam_right { game.cam_x += 3; } - if input.cam_up { game.cam_y -= 3; } - if input.cam_down { game.cam_y += 3; } + if input.quit { + running = false; + ctrl.exit(); + return; + } - if let Some(brush_id) = input.paint { - let mat = match brush_id { - 1 => MaterialId::Sand, - 2 => MaterialId::Water, - 3 => MaterialId::Stone, - 4 => MaterialId::Lava, - 5 => MaterialId::Wood, - 6 => MaterialId::Acid, - 7 => MaterialId::Grass, - 8 => MaterialId::Dirt, - 9 => MaterialId::Fire, - 0 => MaterialId::Flesh, - 99 => MaterialId::Empty, - _ => continue, - }; - let cx = game.cam_x + (vw as i32 / 2); - let cy = game.cam_y + (vh as i32 / 2); - let r = 2; - for dy in -r..=r { - for dx in -r..=r { - if dx * dx + dy * dy <= r * r + 1 { - if mat == MaterialId::Empty { - game.grid.set(cx + dx, cy + dy, verbatim::world::cell::Cell::empty()); - } else { - game.grid.set_material(cx + dx, cy + dy, mat); + if input.jump { + let on_ground = game.check_on_ground(); + game.player.jump(&mut game.entities, on_ground); + } + + if input.left { + game.player.move_left(&mut game.entities); + } else if input.right { + game.player.move_right(&mut game.entities); + } else { + game.player.stop_horizontal(&mut game.entities); + } + + if input.cam_left { game.cam_x -= 3; } + if input.cam_right { game.cam_x += 3; } + if input.cam_up { game.cam_y -= 3; } + if input.cam_down { game.cam_y += 3; } + + if let Some(brush_id) = input.paint { + let mat = match brush_id { + 1 => MaterialId::Sand, + 2 => MaterialId::Water, + 3 => MaterialId::Stone, + 4 => MaterialId::Lava, + 5 => MaterialId::Wood, + 6 => MaterialId::Acid, + 7 => MaterialId::Grass, + 8 => MaterialId::Dirt, + 9 => MaterialId::Fire, + 0 => MaterialId::Flesh, + 99 => MaterialId::Empty, + _ => MaterialId::Empty, + }; + let cx = game.cam_x + (vw as i32 / 2); + let cy = game.cam_y + (vh as i32 / 2); + let r = 2; + for dy in -r..=r { + for dx in -r..=r { + if dx * dx + dy * dy <= r * r + 1 { + if mat == MaterialId::Empty { + game.grid.set(cx + dx, cy + dy, verbatim::world::cell::Cell::empty()); + } else { + game.grid.set_material(cx + dx, cy + dy, mat); + } + } } } } + + let (px, py) = game.player.center(&game.entities); + game.cam_x = px as i32 - (vw as i32 / 2); + game.cam_y = py as i32 - (vh as i32 / 2); + + renderer.render_to_buffer(&game.grid, &game.entities, game.cam_x, game.cam_y); + + let mut buffer = surface.buffer_mut().expect("buffer"); + let pixels = renderer.pixels(); + let len = buffer.len().min(pixels.len()); + buffer[..len].copy_from_slice(&pixels[..len]); + buffer.present().expect("present"); } + _ => {} } - - let (px, py) = game.player.center(&game.entities); - game.cam_x = px as i32 - (vw as i32 / 2); - game.cam_y = py as i32 - (vh as i32 / 2); - - renderer.render(&game.grid, &game.entities, game.cam_x, game.cam_y); - } + }).expect("event loop error"); } diff --git a/src/render/window.rs b/src/render/window.rs index 9caed1a..7d5644e 100644 --- a/src/render/window.rs +++ b/src/render/window.rs @@ -1,4 +1,3 @@ -use minifb::{Key, Window, WindowOptions}; use fontdue::{Font, FontSettings}; use crate::entity::{EntityManager, EntityKind}; use crate::world::cell::MaterialId; @@ -13,12 +12,12 @@ const ATLAS_W: usize = ATLAS_COLS * CHAR_W; const ATLAS_H: usize = ATLAS_ROWS * CHAR_H; pub struct WindowRenderer { - window: Window, width: usize, height: usize, pixels: Vec, atlas: Vec, atlas_map: std::collections::HashMap, + font: Font, } impl WindowRenderer { @@ -33,21 +32,10 @@ impl WindowRenderer { let width = 160; let height = 50; - let mut window = Window::new( - "Verbatim", - width * CHAR_W, - height * CHAR_H, - WindowOptions { - resize: true, - ..WindowOptions::default() - }, - ).expect("Failed to create window"); - window.set_target_fps(60); - let mut atlas = vec![0u8; ATLAS_W * ATLAS_H]; let mut atlas_map = std::collections::HashMap::new(); - let chars: Vec = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".chars().collect(); + let chars: Vec = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~?".chars().collect(); for (i, &ch) in chars.iter().enumerate() { let col = i % ATLAS_COLS; @@ -75,71 +63,41 @@ impl WindowRenderer { } Self { - window, width, height, - pixels: vec![0u32; width * CHAR_W * height * CHAR_H], + pixels: vec![0x000A0A0F; width * CHAR_W * height * CHAR_H], atlas, atlas_map, + font, } } - #[inline] - fn blend(fg: [u8; 3], bg: [u8; 3], alpha: u8) -> u32 { - if alpha == 0 { - return ((bg[0] as u32) << 16) | ((bg[1] as u32) << 8) | (bg[2] as u32); - } + #[inline(always)] + fn blend_fast(fg: [u8; 3], bg: u32, alpha: u8) -> u32 { + if alpha == 0 { return bg; } if alpha == 255 { return ((fg[0] as u32) << 16) | ((fg[1] as u32) << 8) | (fg[2] as u32); } let a = alpha as u32; let inv = 255 - a; - let r = (fg[0] as u32 * a + bg[0] as u32 * inv) / 255; - let g = (fg[1] as u32 * a + bg[1] as u32 * inv) / 255; - let b = (fg[2] as u32 * a + bg[2] as u32 * inv) / 255; + let br = (bg >> 16) & 0xFF; + let bg_ = (bg >> 8) & 0xFF; + let bb = bg & 0xFF; + let r = (fg[0] as u32 * a + br * inv) >> 8; + let g = (fg[1] as u32 * a + bg_ * inv) >> 8; + let b = (fg[2] as u32 * a + bb * inv) >> 8; (r << 16) | (g << 8) | b } - #[inline] - fn draw_cell(&mut self, col: usize, row: usize, ch: char, fg: [u8; 3], bg: [u8; 3]) { - let (ac, ar) = match self.atlas_map.get(&ch) { - Some(&(c, r)) => (c, r), - None => return, - }; - - let base_x = col * CHAR_W; - let base_y = row * CHAR_H; - let screen_w = self.width * CHAR_W; - - for y in 0..CHAR_H { - let ay = ar * CHAR_H + y; - let py = base_y + y; - if py >= self.height * CHAR_H { break; } - - let atlas_row = &self.atlas[ay * ATLAS_W + ac * CHAR_W..ay * ATLAS_W + ac * CHAR_W + CHAR_W]; - let pixel_row = &mut self.pixels[py * screen_w + base_x..py * screen_w + base_x + CHAR_W.min(screen_w - base_x)]; - - for x in 0..pixel_row.len() { - pixel_row[x] = Self::blend(fg, bg, atlas_row[x]); - } - } - } - - pub fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) { + pub fn render_to_buffer(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) { let reg = MaterialRegistry::instance(); let screen_w = self.width * CHAR_W; - let screen_h = self.height * CHAR_H; + let bg_default = 0x000A0A0F; - for py in (0..screen_h).step_by(CHAR_H) { - for px in (0..screen_w).step_by(CHAR_W) { - let base = py * screen_w + px; - let end = base + CHAR_W.min(screen_w - px); - for p in base..end { - self.pixels[p] = 0x000A0A0F; - } - } - } + // Fast clear: fill with background + self.pixels.fill(bg_default); + // Build entity overlay let mut entity_map: std::collections::HashMap<(i32, i32), (char, [u8; 3])> = std::collections::HashMap::new(); for e in entities.all() { for b in &e.bodies { @@ -168,49 +126,69 @@ impl WindowRenderer { } } + // Draw cells for dy in 0..self.height { + let wy = cam_y + dy as i32; for dx in 0..self.width { let wx = cam_x + dx as i32; - let wy = cam_y + dy as i32; - if let Some(&(ch, fg)) = entity_map.get(&(dx as i32, dy as i32)) { - self.draw_cell(dx, dy, ch, fg, [10, 10, 15]); - continue; - } - - if !grid.in_bounds(wx, wy) { - self.draw_cell(dx, dy, '?', [80, 80, 80], [10, 10, 15]); - continue; - } - - let cell = grid.get(wx, wy); - let mat = reg.get(cell.material); - if cell.is_empty() { - self.draw_cell(dx, dy, ' ', [10, 10, 15], [10, 10, 15]); + let (ch, fg, bg) = if let Some(&(ec, ef)) = entity_map.get(&(dx as i32, dy as i32)) { + (ec, ef, bg_default) + } else if !grid.in_bounds(wx, wy) { + ('?', [80, 80, 80], bg_default) } else { - let fg = if cell.material == MaterialId::Lava { - let r = 200u8.saturating_add(cell.variant / 2); - [r, 60, 20] + let cell = grid.get(wx, wy); + let mat = reg.get(cell.material); + if cell.is_empty() { + (' ', [10, 10, 15], bg_default) } else { - [mat.color_fg.0, mat.color_fg.1, mat.color_fg.2] - }; - let bg = [mat.color_bg.0, mat.color_bg.1, mat.color_bg.2]; - self.draw_cell(dx, dy, mat.display_char, fg, bg); + let fg = if cell.material == MaterialId::Lava { + let r = 200u8.saturating_add(cell.variant / 2); + [r, 60, 20] + } else { + [mat.color_fg.0, mat.color_fg.1, mat.color_fg.2] + }; + let bg = ((mat.color_bg.0 as u32) << 16) | ((mat.color_bg.1 as u32) << 8) | (mat.color_bg.2 as u32); + (mat.display_char, fg, bg) + } + }; + + // Skip drawing if it's a space on default background + if ch == ' ' && bg == bg_default { + continue; + } + + let (ac, ar) = match self.atlas_map.get(&ch) { + Some(&(c, r)) => (c, r), + None => continue, + }; + + let base_x = dx * CHAR_W; + let base_y = dy * CHAR_H; + + for y in 0..CHAR_H { + let ay = ar * CHAR_H + y; + let py = base_y + y; + let atlas_off = ay * ATLAS_W + ac * CHAR_W; + let pix_off = py * screen_w + base_x; + + for x in 0..CHAR_W { + let alpha = self.atlas[atlas_off + x]; + if alpha > 0 { + self.pixels[pix_off + x] = Self::blend_fast(fg, bg, alpha); + } else { + self.pixels[pix_off + x] = bg; + } + } } } } - - let _ = self.window.update_with_buffer(&self.pixels, screen_w, screen_h); - } - - pub fn is_open(&self) -> bool { - self.window.is_open() - } - - pub fn get_keys_down(&self) -> Vec { - self.window.get_keys() } pub fn width(&self) -> usize { self.width } pub fn height(&self) -> usize { self.height } + pub fn pixels(&self) -> &[u32] { &self.pixels } + pub fn pixel_w(&self) -> usize { self.width * CHAR_W } + pub fn pixel_h(&self) -> usize { self.height * CHAR_H } + pub fn font(&self) -> &Font { &self.font } } diff --git a/src/render/window_input.rs b/src/render/window_input.rs index da9f5ea..bb43a8a 100644 --- a/src/render/window_input.rs +++ b/src/render/window_input.rs @@ -1,4 +1,5 @@ -use minifb::Key; +use std::collections::HashSet; +use winit::keyboard::{KeyCode, PhysicalKey}; pub struct WindowInput { pub left: bool, @@ -11,6 +12,7 @@ pub struct WindowInput { pub quit: bool, pub paint: Option, jump_was_down: bool, + down_keys: HashSet, } impl WindowInput { @@ -20,13 +22,32 @@ impl WindowInput { cam_left: false, cam_right: false, cam_up: false, cam_down: false, quit: false, paint: None, jump_was_down: false, + down_keys: HashSet::new(), } } - pub fn update(&mut self, keys: &[Key]) { - let now_left = keys.contains(&Key::A) || keys.contains(&Key::Left); - let now_right = keys.contains(&Key::D) || keys.contains(&Key::Right); - let now_jump = keys.contains(&Key::W) || keys.contains(&Key::Space) || keys.contains(&Key::Up); + pub fn on_key_event(&mut self, key: PhysicalKey, state: winit::event::ElementState) { + let code = match key { + PhysicalKey::Code(c) => c, + _ => return, + }; + + match state { + winit::event::ElementState::Pressed => { + self.down_keys.insert(code); + } + winit::event::ElementState::Released => { + self.down_keys.remove(&code); + } + } + } + + pub fn update(&mut self) { + let keys = &self.down_keys; + + let now_left = keys.contains(&KeyCode::KeyA) || keys.contains(&KeyCode::ArrowLeft); + let now_right = keys.contains(&KeyCode::KeyD) || keys.contains(&KeyCode::ArrowRight); + let now_jump = keys.contains(&KeyCode::KeyW) || keys.contains(&KeyCode::Space) || keys.contains(&KeyCode::ArrowUp); self.left = now_left && !now_right; self.right = now_right && !now_left; @@ -34,23 +55,30 @@ impl WindowInput { self.jump = now_jump && !self.jump_was_down; self.jump_was_down = now_jump; - self.cam_left = keys.contains(&Key::H); - self.cam_right = keys.contains(&Key::L); - self.cam_up = keys.contains(&Key::K); - self.cam_down = keys.contains(&Key::J); - self.quit = keys.contains(&Key::Q) || keys.contains(&Key::Escape); + self.cam_left = keys.contains(&KeyCode::KeyH); + self.cam_right = keys.contains(&KeyCode::KeyL); + self.cam_up = keys.contains(&KeyCode::KeyK); + self.cam_down = keys.contains(&KeyCode::KeyJ); + self.quit = keys.contains(&KeyCode::KeyQ) || keys.contains(&KeyCode::Escape); self.paint = None; - if keys.contains(&Key::Key1) { self.paint = Some(1); } - else if keys.contains(&Key::Key2) { self.paint = Some(2); } - else if keys.contains(&Key::Key3) { self.paint = Some(3); } - else if keys.contains(&Key::Key4) { self.paint = Some(4); } - else if keys.contains(&Key::Key5) { self.paint = Some(5); } - else if keys.contains(&Key::Key6) { self.paint = Some(6); } - else if keys.contains(&Key::Key7) { self.paint = Some(7); } - else if keys.contains(&Key::Key8) { self.paint = Some(8); } - else if keys.contains(&Key::Key9) { self.paint = Some(9); } - else if keys.contains(&Key::Key0) { self.paint = Some(0); } - else if keys.contains(&Key::X) { self.paint = Some(99); } + if keys.contains(&KeyCode::Digit1) { self.paint = Some(1); } + else if keys.contains(&KeyCode::Digit2) { self.paint = Some(2); } + else if keys.contains(&KeyCode::Digit3) { self.paint = Some(3); } + else if keys.contains(&KeyCode::Digit4) { self.paint = Some(4); } + else if keys.contains(&KeyCode::Digit5) { self.paint = Some(5); } + else if keys.contains(&KeyCode::Digit6) { self.paint = Some(6); } + else if keys.contains(&KeyCode::Digit7) { self.paint = Some(7); } + else if keys.contains(&KeyCode::Digit8) { self.paint = Some(8); } + else if keys.contains(&KeyCode::Digit9) { self.paint = Some(9); } + else if keys.contains(&KeyCode::Digit0) { self.paint = Some(0); } + else if keys.contains(&KeyCode::KeyX) { self.paint = Some(99); } + } + + pub fn clear(&mut self) { + self.down_keys.clear(); + self.left = false; + self.right = false; + self.jump = false; } }