diff --git a/src/app.rs b/src/app.rs index 50ed463..f78777e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,7 +2,6 @@ use std::time::{Duration, Instant}; use glam::Vec2; use sdl2::event::{Event, WindowEvent}; -use sdl2::keyboard::Keycode; use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator}; use sdl2::ttf::Sdl2TtfContext; use sdl2::video::{Window, WindowContext}; @@ -13,10 +12,13 @@ use crate::error::{GameError, GameResult}; use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE}; use crate::game::Game; +use crate::input::commands::GameCommand; +use crate::input::InputSystem; use crate::platform::get_platform; pub struct App { game: Game, + input_system: InputSystem, canvas: Canvas, event_pump: &'static mut EventPump, backbuffer: Texture<'static>, @@ -78,8 +80,9 @@ impl App { game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO) .map_err(|e| GameError::Sdl(e.to_string()))?; - Ok(Self { + Ok(App { game, + input_system: InputSystem::new(), canvas, event_pump, backbuffer, @@ -106,36 +109,31 @@ impl App { }, // It doesn't really make sense to have this available in the browser #[cfg(not(target_os = "emscripten"))] - Event::Quit { .. } - | Event::KeyDown { - keycode: Some(Keycode::Escape) | Some(Keycode::Q), - .. - } => { + Event::Quit { .. } => { event!(tracing::Level::INFO, "Exit requested. Exiting..."); return false; } - Event::KeyDown { - keycode: Some(Keycode::P), - .. - } => { - self.paused = !self.paused; - event!(tracing::Level::INFO, "{}", if self.paused { "Paused" } else { "Unpaused" }); - } - Event::KeyDown { - keycode: Some(Keycode::Space), - .. - } => { - self.game.toggle_debug_mode(); - } - Event::KeyDown { keycode: Some(key), .. } => { - self.game.keyboard_event(key); - } Event::MouseMotion { x, y, .. } => { // Convert window coordinates to logical coordinates self.cursor_pos = Vec2::new(x as f32, y as f32); } _ => {} } + + let commands = self.input_system.handle_event(&event); + for command in commands { + match command { + GameCommand::Exit => { + event!(tracing::Level::INFO, "Exit requested. Exiting..."); + return false; + } + GameCommand::TogglePause => { + self.paused = !self.paused; + event!(tracing::Level::INFO, "{}", if self.paused { "Paused" } else { "Unpaused" }); + } + _ => self.game.post_event(command.into()), + } + } } let dt = self.last_tick.elapsed().as_secs_f32(); diff --git a/src/entity/direction.rs b/src/entity/direction.rs index cfb23af..b6466f9 100644 --- a/src/entity/direction.rs +++ b/src/entity/direction.rs @@ -1,7 +1,8 @@ use glam::IVec2; /// The four cardinal directions. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(usize)] pub enum Direction { Up, Down, diff --git a/src/entity/pacman.rs b/src/entity/pacman.rs index 861b192..a1b2c8e 100644 --- a/src/entity/pacman.rs +++ b/src/entity/pacman.rs @@ -14,7 +14,6 @@ use crate::entity::{ use crate::texture::animated::AnimatedTexture; use crate::texture::directional::DirectionalAnimatedTexture; use crate::texture::sprite::SpriteAtlas; -use sdl2::keyboard::Keycode; use tracing::error; use crate::error::{GameError, GameResult, TextureError}; @@ -107,24 +106,6 @@ impl Pacman { texture: DirectionalAnimatedTexture::new(textures, stopped_textures), }) } - - /// Handles keyboard input to change Pac-Man's direction. - /// - /// Maps arrow keys to directions and queues the direction change - /// for the next valid intersection. - pub fn handle_key(&mut self, keycode: Keycode) { - let direction = match keycode { - Keycode::Up => Some(Direction::Up), - Keycode::Down => Some(Direction::Down), - Keycode::Left => Some(Direction::Left), - Keycode::Right => Some(Direction::Right), - _ => None, - }; - - if let Some(direction) = direction { - self.traverser.set_next_direction(direction); - } - } } impl Collidable for Pacman { diff --git a/src/game/events.rs b/src/game/events.rs new file mode 100644 index 0000000..157e188 --- /dev/null +++ b/src/game/events.rs @@ -0,0 +1,12 @@ +use crate::input::commands::GameCommand; + +#[derive(Debug, Clone, Copy)] +pub enum GameEvent { + InputCommand(GameCommand), +} + +impl From for GameEvent { + fn from(command: GameCommand) -> Self { + GameEvent::InputCommand(command) + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 7c0c6bc..435f1f8 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,16 +1,12 @@ //! This module contains the main game logic and state. -use glam::{UVec2, Vec2}; use rand::{rngs::SmallRng, Rng, SeedableRng}; -use sdl2::{ - keyboard::Keycode, - pixels::Color, - render::{Canvas, RenderTarget, Texture, TextureCreator}, - video::WindowContext, -}; +use sdl2::pixels::Color; +use sdl2::render::{Canvas, Texture, TextureCreator}; +use sdl2::video::WindowContext; use crate::entity::r#trait::Entity; -use crate::error::{EntityError, GameError, GameResult}; +use crate::error::GameResult; use crate::entity::{ collision::{Collidable, CollisionSystem, EntityId}, @@ -21,15 +17,18 @@ use crate::entity::{ use crate::map::render::MapRenderer; use crate::{constants, texture::sprite::SpriteAtlas}; +use self::events::GameEvent; +use self::state::GameState; + +pub mod events; pub mod state; -use state::GameState; /// The `Game` struct is the main entry point for the game. /// /// It contains the game's state and logic, and is responsible for /// handling user input, updating the game state, and rendering the game. pub struct Game { - state: GameState, + state: state::GameState, } impl Game { @@ -39,16 +38,38 @@ impl Game { Ok(Game { state }) } - pub fn keyboard_event(&mut self, keycode: Keycode) { - self.state.pacman.handle_key(keycode); + pub fn post_event(&mut self, event: GameEvent) { + self.state.event_queue.push_back(event); + } - if keycode == Keycode::M { - self.state.audio.set_mute(!self.state.audio.is_muted()); + fn handle_command(&mut self, command: crate::input::commands::GameCommand) { + use crate::input::commands::GameCommand; + match command { + GameCommand::MovePlayer(direction) => { + self.state.pacman.set_next_direction(direction); + } + GameCommand::ToggleDebug => { + self.toggle_debug_mode(); + } + GameCommand::MuteAudio => { + let is_muted = self.state.audio.is_muted(); + self.state.audio.set_mute(!is_muted); + } + GameCommand::ResetLevel => { + if let Err(e) = self.reset_game_state() { + tracing::error!("Failed to reset game state: {}", e); + } + } + GameCommand::Exit | GameCommand::TogglePause => { + // These are handled in app.rs + } } + } - if keycode == Keycode::R { - if let Err(e) = self.reset_game_state() { - tracing::error!("Failed to reset game state: {}", e); + fn process_events(&mut self) { + while let Some(event) = self.state.event_queue.pop_front() { + match event { + GameEvent::InputCommand(command) => self.handle_command(command), } } } @@ -94,6 +115,7 @@ impl Game { } pub fn tick(&mut self, dt: f32) { + self.process_events(); self.state.pacman.tick(dt, &self.state.map.graph); // Update all ghosts @@ -170,14 +192,14 @@ impl Game { self.state.ghost_ids.iter().position(|&id| id == entity_id) } - pub fn draw(&mut self, canvas: &mut Canvas, backbuffer: &mut Texture) -> GameResult<()> { + pub fn draw(&mut self, canvas: &mut Canvas, backbuffer: &mut Texture) -> GameResult<()> { // Only render the map texture once and cache it if !self.state.map_rendered { let mut map_texture = self .state .texture_creator .create_texture_target(None, constants::CANVAS_SIZE.x, constants::CANVAS_SIZE.y) - .map_err(|e| GameError::Sdl(e.to_string()))?; + .map_err(|e| crate::error::GameError::Sdl(e.to_string()))?; canvas .with_texture_canvas(&mut map_texture, |map_canvas| { @@ -189,7 +211,7 @@ impl Game { } MapRenderer::render_map(map_canvas, &mut self.state.atlas, &mut map_tiles); }) - .map_err(|e| GameError::Sdl(e.to_string()))?; + .map_err(|e| crate::error::GameError::Sdl(e.to_string()))?; self.state.map_texture = Some(map_texture); self.state.map_rendered = true; } @@ -220,12 +242,12 @@ impl Game { tracing::error!("Failed to render pacman: {}", e); } }) - .map_err(|e| GameError::Sdl(e.to_string()))?; + .map_err(|e| crate::error::GameError::Sdl(e.to_string()))?; Ok(()) } - pub fn present_backbuffer( + pub fn present_backbuffer( &mut self, canvas: &mut Canvas, backbuffer: &Texture, @@ -233,7 +255,7 @@ impl Game { ) -> GameResult<()> { canvas .copy(backbuffer, None, None) - .map_err(|e| GameError::Sdl(e.to_string()))?; + .map_err(|e| crate::error::GameError::Sdl(e.to_string()))?; if self.state.debug_mode { if let Err(e) = self.state @@ -253,7 +275,7 @@ impl Game { /// /// Each ghost's path is drawn in its respective color with a small offset /// to prevent overlapping lines. - fn render_pathfinding_debug(&self, canvas: &mut Canvas) -> GameResult<()> { + fn render_pathfinding_debug(&self, canvas: &mut Canvas) -> GameResult<()> { let pacman_node = self.state.pacman.current_node_id(); for ghost in self.state.ghosts.iter() { @@ -274,10 +296,10 @@ impl Game { // Use the overall direction from start to end to determine the perpendicular offset let offset = match ghost.ghost_type { - GhostType::Blinky => Vec2::new(0.25, 0.5), - GhostType::Pinky => Vec2::new(-0.25, -0.25), - GhostType::Inky => Vec2::new(0.5, -0.5), - GhostType::Clyde => Vec2::new(-0.5, 0.25), + GhostType::Blinky => glam::Vec2::new(0.25, 0.5), + GhostType::Pinky => glam::Vec2::new(-0.25, -0.25), + GhostType::Inky => glam::Vec2::new(0.5, -0.5), + GhostType::Clyde => glam::Vec2::new(-0.5, 0.25), } * 5.0; // Calculate offset positions for all nodes using the same perpendicular direction @@ -288,7 +310,7 @@ impl Game { .map .graph .get_node(node_id) - .ok_or(GameError::Entity(EntityError::NodeNotFound(node_id)))?; + .ok_or(crate::error::EntityError::NodeNotFound(node_id))?; let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2(); offset_positions.push(pos + offset); } @@ -304,7 +326,7 @@ impl Game { // Draw the line canvas .draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32)) - .map_err(|e| GameError::Sdl(e.to_string()))?; + .map_err(|e| crate::error::GameError::Sdl(e.to_string()))?; } } } @@ -313,7 +335,7 @@ impl Game { Ok(()) } - fn draw_hud(&mut self, canvas: &mut Canvas) -> GameResult<()> { + fn draw_hud(&mut self, canvas: &mut Canvas) -> GameResult<()> { let lives = 3; let score_text = format!("{:02}", self.state.score); let x_offset = 4; @@ -325,7 +347,7 @@ impl Game { canvas, &mut self.state.atlas, &format!("{lives}UP HIGH SCORE "), - UVec2::new(8 * lives_offset as u32 + x_offset, y_offset), + glam::UVec2::new(8 * lives_offset as u32 + x_offset, y_offset), ) { tracing::error!("Failed to render HUD text: {}", e); } @@ -333,7 +355,7 @@ impl Game { canvas, &mut self.state.atlas, &score_text, - UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset), + glam::UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset), ) { tracing::error!("Failed to render score text: {}", e); } diff --git a/src/game/state.rs b/src/game/state.rs index 7174701..85947c4 100644 --- a/src/game/state.rs +++ b/src/game/state.rs @@ -1,3 +1,5 @@ +use std::collections::VecDeque; + use sdl2::{ image::LoadTexture, render::{Texture, TextureCreator}, @@ -10,14 +12,14 @@ use crate::{ audio::Audio, constants::RAW_BOARD, entity::{ - collision::{Collidable, CollisionSystem}, + collision::{Collidable, CollisionSystem, EntityId}, ghost::{Ghost, GhostType}, item::Item, pacman::Pacman, }, error::{GameError, GameResult, TextureError}, - game::EntityId, - map::Map, + game::events::GameEvent, + map::builder::Map, texture::{ sprite::{AtlasMapper, SpriteAtlas}, text::TextTexture, @@ -42,6 +44,7 @@ pub struct GameState { pub items: Vec, pub item_ids: Vec, pub debug_mode: bool, + pub event_queue: VecDeque, // Collision system pub(crate) collision_system: CollisionSystem, @@ -141,6 +144,7 @@ impl GameState { map_texture: None, map_rendered: false, texture_creator, + event_queue: VecDeque::new(), }) } } diff --git a/src/input/commands.rs b/src/input/commands.rs new file mode 100644 index 0000000..d125a0c --- /dev/null +++ b/src/input/commands.rs @@ -0,0 +1,11 @@ +use crate::entity::direction::Direction; + +#[derive(Debug, Clone, Copy)] +pub enum GameCommand { + MovePlayer(Direction), + TogglePause, + ToggleDebug, + MuteAudio, + ResetLevel, + Exit, +} diff --git a/src/input/mod.rs b/src/input/mod.rs new file mode 100644 index 0000000..1770a58 --- /dev/null +++ b/src/input/mod.rs @@ -0,0 +1,47 @@ +use std::collections::HashMap; + +use sdl2::{event::Event, keyboard::Keycode}; + +use crate::{entity::direction::Direction, input::commands::GameCommand}; + +pub mod commands; + +pub struct InputSystem { + key_bindings: HashMap, +} + +impl InputSystem { + pub fn new() -> Self { + let mut key_bindings = HashMap::new(); + + // Player movement + key_bindings.insert(Keycode::Up, GameCommand::MovePlayer(Direction::Up)); + key_bindings.insert(Keycode::W, GameCommand::MovePlayer(Direction::Up)); + key_bindings.insert(Keycode::Down, GameCommand::MovePlayer(Direction::Down)); + key_bindings.insert(Keycode::S, GameCommand::MovePlayer(Direction::Down)); + key_bindings.insert(Keycode::Left, GameCommand::MovePlayer(Direction::Left)); + key_bindings.insert(Keycode::A, GameCommand::MovePlayer(Direction::Left)); + key_bindings.insert(Keycode::Right, GameCommand::MovePlayer(Direction::Right)); + key_bindings.insert(Keycode::D, GameCommand::MovePlayer(Direction::Right)); + + // Game actions + key_bindings.insert(Keycode::P, GameCommand::TogglePause); + key_bindings.insert(Keycode::Space, GameCommand::ToggleDebug); + key_bindings.insert(Keycode::M, GameCommand::MuteAudio); + key_bindings.insert(Keycode::R, GameCommand::ResetLevel); + key_bindings.insert(Keycode::Escape, GameCommand::Exit); + key_bindings.insert(Keycode::Q, GameCommand::Exit); + + Self { key_bindings } + } + + pub fn handle_event(&self, event: &Event) -> Vec { + let mut commands = Vec::new(); + if let Event::KeyDown { keycode: Some(key), .. } = event { + if let Some(command) = self.key_bindings.get(key) { + commands.push(*command); + } + } + commands + } +} diff --git a/src/lib.rs b/src/lib.rs index 6636dbb..d9374f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod entity; pub mod error; pub mod game; pub mod helpers; +pub mod input; pub mod map; pub mod platform; pub mod texture; diff --git a/src/main.rs b/src/main.rs index 12a5f05..f293b89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod entity; mod error; mod game; mod helpers; +mod input; mod map; mod platform; mod texture; diff --git a/src/map/mod.rs b/src/map/mod.rs index 1743947..8f2e2bc 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -4,6 +4,3 @@ pub mod builder; pub mod layout; pub mod parser; pub mod render; - -// Re-export main types for convenience -pub use builder::Map;