Compare commits

...

6 Commits

21 changed files with 365 additions and 137 deletions

68
Cargo.lock generated
View File

@@ -194,7 +194,8 @@ dependencies = [
"libc",
"once_cell",
"pathfinding",
"rand",
"phf",
"rand 0.9.2",
"sdl2",
"serde",
"serde_json",
@@ -223,6 +224,48 @@ dependencies = [
"thiserror",
]
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand 0.8.5",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.13"
@@ -253,15 +296,30 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_core",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "rand_core"
version = "0.9.3"
@@ -399,6 +457,12 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "smallvec"
version = "1.15.1"

View File

@@ -23,6 +23,7 @@ serde_json = "1.0.142"
smallvec = "1.15.1"
strum = "0.27.2"
strum_macros = "0.27.2"
phf = { version = "0.11", features = ["macros"] }
[profile.release]
lto = true
@@ -57,3 +58,8 @@ aarch64-apple-darwin = { triplet = "arm64-osx" }
[target.'cfg(target_os = "emscripten")'.dependencies]
libc = "0.2.175"
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
phf = { version = "0.11", features = ["macros"] }

View File

@@ -1,6 +1,6 @@
# Pac-Man
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg

50
build.rs Normal file
View File

@@ -0,0 +1,50 @@
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AtlasMapper {
frames: HashMap<String, MapperFrame>,
}
#[derive(Copy, Clone, Debug, Deserialize)]
struct MapperFrame {
x: u16,
y: u16,
width: u16,
height: u16,
}
fn main() {
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("atlas_data.rs");
let mut file = BufWriter::new(File::create(&path).unwrap());
let atlas_json = include_str!("./assets/game/atlas.json");
let atlas_mapper: AtlasMapper = serde_json::from_str(atlas_json).unwrap();
writeln!(&mut file, "use phf::phf_map;").unwrap();
writeln!(&mut file, "use crate::texture::sprite::MapperFrame;").unwrap();
writeln!(
&mut file,
"pub static ATLAS_FRAMES: phf::Map<&'static str, MapperFrame> = phf_map! {{"
)
.unwrap();
for (name, frame) in atlas_mapper.frames {
writeln!(
&mut file,
" \"{}\" => MapperFrame {{ x: {}, y: {}, width: {}, height: {} }},",
name, frame.x, frame.y, frame.width, frame.height
)
.unwrap();
}
writeln!(&mut file, "}};").unwrap();
println!("cargo:rerun-if-changed=assets/game/atlas.json");
}

View File

@@ -2,25 +2,28 @@ 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};
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
use tracing::{error, event};
use tracing::{debug, error, info, warn};
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<Window>,
event_pump: &'static mut EventPump,
backbuffer: Texture<'static>,
paused: bool,
focused: bool,
last_tick: Instant,
cursor_pos: Vec2,
}
@@ -51,7 +54,13 @@ impl App {
.build()
.map_err(|e| GameError::Sdl(e.to_string()))?;
let mut canvas = window.into_canvas().build().map_err(|e| GameError::Sdl(e.to_string()))?;
let mut canvas = window
.into_canvas()
.accelerated()
.present_vsync()
.build()
.map_err(|e| GameError::Sdl(e.to_string()))?;
canvas
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
@@ -72,12 +81,14 @@ 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,
paused: false,
focused: true,
last_tick: Instant::now(),
cursor_pos: Vec2::ZERO,
})
@@ -90,46 +101,42 @@ impl App {
for event in self.event_pump.poll_iter() {
match event {
Event::Window { win_event, .. } => match win_event {
WindowEvent::Hidden => {
event!(tracing::Level::DEBUG, "Window hidden");
WindowEvent::FocusGained => {
self.focused = true;
}
WindowEvent::Shown => {
event!(tracing::Level::DEBUG, "Window shown");
WindowEvent::FocusLost => {
debug!("Window focus lost");
self.focused = false;
}
_ => {}
},
// 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!(tracing::Level::INFO, "Exit requested. Exiting...");
Event::Quit { .. } => {
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 => {
info!("Exit requested. Exiting...");
return false;
}
GameCommand::TogglePause => {
self.paused = !self.paused;
info!("{}", if self.paused { "Paused" } else { "Unpaused" });
}
_ => self.game.post_event(command.into()),
}
}
}
let dt = self.last_tick.elapsed().as_secs_f32();
@@ -151,14 +158,10 @@ impl App {
if start.elapsed() < LOOP_TIME {
let time = LOOP_TIME.saturating_sub(start.elapsed());
if time != Duration::ZERO {
get_platform().sleep(time);
get_platform().sleep(time, self.focused);
}
} else {
event!(
tracing::Level::WARN,
"Game loop behind schedule by: {:?}",
start.elapsed() - LOOP_TIME
);
warn!("Game loop behind schedule by: {:?}", start.elapsed() - LOOP_TIME);
}
true

View File

@@ -12,8 +12,6 @@ pub enum Asset {
Wav3,
Wav4,
Atlas,
AtlasJson,
// Add more as needed
}
impl Asset {
@@ -26,7 +24,6 @@ impl Asset {
Wav3 => "sound/waka/3.ogg",
Wav4 => "sound/waka/4.ogg",
Atlas => "atlas.png",
AtlasJson => "atlas.json",
}
}
}
@@ -36,6 +33,7 @@ mod imp {
use crate::error::AssetError;
use crate::platform::get_platform;
/// Returns the raw bytes of the given asset.
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
get_platform().get_asset_bytes(asset)
}

View File

@@ -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,

View File

@@ -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 {

12
src/game/events.rs Normal file
View File

@@ -0,0 +1,12 @@
use crate::input::commands::GameCommand;
#[derive(Debug, Clone, Copy)]
pub enum GameEvent {
InputCommand(GameCommand),
}
impl From<GameCommand> for GameEvent {
fn from(command: GameCommand) -> Self {
GameEvent::InputCommand(command)
}
}

View File

@@ -1,32 +1,34 @@
//! 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::error::{EntityError, GameError, GameResult};
use crate::entity::r#trait::Entity;
use crate::error::GameResult;
use crate::entity::{
collision::{Collidable, CollisionSystem, EntityId},
ghost::{Ghost, GhostType},
pacman::Pacman,
r#trait::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 {
@@ -36,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),
}
}
}
@@ -91,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
@@ -167,14 +192,37 @@ impl Game {
self.state.ghost_ids.iter().position(|&id| id == entity_id)
}
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
pub fn draw<T: sdl2::render::RenderTarget>(&mut self, canvas: &mut Canvas<T>, 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| crate::error::GameError::Sdl(e.to_string()))?;
canvas
.with_texture_canvas(&mut map_texture, |map_canvas| {
let mut map_tiles = Vec::with_capacity(35);
for i in 0..35 {
let tile_name = format!("maze/tiles/{}.png", i);
let tile = SpriteAtlas::get_tile(&self.state.atlas, &tile_name).unwrap();
map_tiles.push(tile);
}
MapRenderer::render_map(map_canvas, &mut self.state.atlas, &mut map_tiles);
})
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
self.state.map_texture = Some(map_texture);
self.state.map_rendered = true;
}
canvas
.with_texture_canvas(backbuffer, |canvas| {
canvas.set_draw_color(Color::BLACK);
canvas.clear();
self.state
.map
.render(canvas, &mut self.state.atlas, &mut self.state.map_tiles);
if let Some(ref map_texture) = self.state.map_texture {
canvas.copy(map_texture, None, None).unwrap();
}
// Render all items
for item in &self.state.items {
@@ -194,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<T: RenderTarget>(
pub fn present_backbuffer<T: sdl2::render::RenderTarget>(
&mut self,
canvas: &mut Canvas<T>,
backbuffer: &Texture,
@@ -207,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
@@ -227,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<T: RenderTarget>(&self, canvas: &mut Canvas<T>) -> GameResult<()> {
fn render_pathfinding_debug<T: sdl2::render::RenderTarget>(&self, canvas: &mut Canvas<T>) -> GameResult<()> {
let pacman_node = self.state.pacman.current_node_id();
for ghost in self.state.ghosts.iter() {
@@ -248,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
@@ -262,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);
}
@@ -278,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()))?;
}
}
}
@@ -287,7 +335,7 @@ impl Game {
Ok(())
}
fn draw_hud<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> GameResult<()> {
fn draw_hud<T: sdl2::render::RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> GameResult<()> {
let lives = 3;
let score_text = format!("{:02}", self.state.score);
let x_offset = 4;
@@ -299,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);
}
@@ -307,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);
}

View File

@@ -1,4 +1,10 @@
use sdl2::{image::LoadTexture, render::TextureCreator, video::WindowContext};
use std::collections::VecDeque;
use sdl2::{
image::LoadTexture,
render::{Texture, TextureCreator},
video::WindowContext,
};
use smallvec::SmallVec;
use crate::{
@@ -6,20 +12,22 @@ 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, AtlasTile, SpriteAtlas},
sprite::{AtlasMapper, SpriteAtlas},
text::TextTexture,
},
};
include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
/// The `GameState` struct holds all the essential data for the game.
///
/// This includes the score, map, entities (Pac-Man, ghosts, items),
@@ -29,7 +37,6 @@ use crate::{
pub struct GameState {
pub score: u32,
pub map: Map,
pub map_tiles: Vec<AtlasTile>,
pub pacman: Pacman,
pub pacman_id: EntityId,
pub ghosts: SmallVec<[Ghost; 4]>,
@@ -37,6 +44,7 @@ pub struct GameState {
pub items: Vec<Item>,
pub item_ids: Vec<EntityId>,
pub debug_mode: bool,
pub event_queue: VecDeque<GameEvent>,
// Collision system
pub(crate) collision_system: CollisionSystem,
@@ -47,6 +55,11 @@ pub struct GameState {
// Audio
pub audio: Audio,
// Map texture pre-rendering
pub(crate) map_texture: Option<Texture<'static>>,
pub(crate) map_rendered: bool,
pub(crate) texture_creator: &'static TextureCreator<WindowContext>,
}
impl GameState {
@@ -58,7 +71,7 @@ impl GameState {
pub fn new(texture_creator: &'static TextureCreator<WindowContext>) -> GameResult<Self> {
let map = Map::new(RAW_BOARD)?;
let pacman_start_node = map.start_positions.pacman;
let start_node = map.start_positions.pacman;
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
@@ -68,21 +81,15 @@ impl GameState {
GameError::Texture(TextureError::LoadFailed(e.to_string()))
}
})?;
let atlas_json = get_asset_bytes(Asset::AtlasJson)?;
let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json)?;
let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper);
let mut map_tiles = Vec::with_capacity(35);
for i in 0..35 {
let tile_name = format!("maze/tiles/{}.png", i);
let tile = SpriteAtlas::get_tile(&atlas, &tile_name)
.ok_or(GameError::Texture(TextureError::AtlasTileNotFound(tile_name)))?;
map_tiles.push(tile);
}
let atlas_mapper = AtlasMapper {
frames: ATLAS_FRAMES.into_iter().map(|(k, v)| (k.to_string(), *v)).collect(),
};
let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper);
let text_texture = TextTexture::new(1.0);
let audio = Audio::new();
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?;
let pacman = Pacman::new(&map.graph, start_node, &atlas)?;
// Generate items (pellets and energizers)
let items = map.generate_items(&atlas)?;
@@ -123,7 +130,6 @@ impl GameState {
Ok(Self {
map,
atlas,
map_tiles,
pacman,
pacman_id,
ghosts,
@@ -135,6 +141,10 @@ impl GameState {
score: 0,
debug_mode: false,
collision_system,
map_texture: None,
map_rendered: false,
texture_creator,
event_queue: VecDeque::new(),
})
}
}

11
src/input/commands.rs Normal file
View File

@@ -0,0 +1,11 @@
use crate::entity::direction::Direction;
#[derive(Debug, Clone, Copy)]
pub enum GameCommand {
MovePlayer(Direction),
TogglePause,
ToggleDebug,
MuteAudio,
ResetLevel,
Exit,
}

48
src/input/mod.rs Normal file
View File

@@ -0,0 +1,48 @@
use std::collections::HashMap;
use sdl2::{event::Event, keyboard::Keycode};
use crate::{entity::direction::Direction, input::commands::GameCommand};
pub mod commands;
#[derive(Debug, Clone, Default)]
pub struct InputSystem {
key_bindings: HashMap<Keycode, GameCommand>,
}
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<GameCommand> {
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
}
}

View File

@@ -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;

View File

@@ -14,6 +14,7 @@ mod entity;
mod error;
mod game;
mod helpers;
mod input;
mod map;
mod platform;
mod texture;

View File

@@ -6,7 +6,7 @@ use crate::entity::graph::{EdgePermissions, Graph, Node, NodeId};
use crate::entity::item::{Item, ItemType};
use crate::map::parser::MapTileParser;
use crate::map::render::MapRenderer;
use crate::texture::sprite::{AtlasTile, Sprite, SpriteAtlas};
use crate::texture::sprite::{Sprite, SpriteAtlas};
use glam::{IVec2, Vec2};
use sdl2::render::{Canvas, RenderTarget};
use std::collections::{HashMap, VecDeque};
@@ -154,14 +154,6 @@ impl Map {
})
}
/// Renders the map to the given canvas.
///
/// This function draws the static map texture to the screen at the correct
/// position and scale.
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, map_tiles: &mut [AtlasTile]) {
MapRenderer::render_map(canvas, atlas, map_tiles);
}
/// Generates Item entities for pellets and energizers from the parsed map.
pub fn generate_items(&self, atlas: &SpriteAtlas) -> GameResult<Vec<Item>> {
// Pre-load sprites to avoid repeated texture lookups

View File

@@ -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;

View File

@@ -11,8 +11,12 @@ use crate::platform::Platform;
pub struct DesktopPlatform;
impl Platform for DesktopPlatform {
fn sleep(&self, duration: Duration) {
spin_sleep::sleep(duration);
fn sleep(&self, duration: Duration, focused: bool) {
if focused {
spin_sleep::sleep(duration);
} else {
std::thread::sleep(duration);
}
}
fn get_time(&self) -> f64 {
@@ -72,7 +76,6 @@ impl Platform for DesktopPlatform {
Asset::Wav3 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/3.ogg"))),
Asset::Wav4 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/4.ogg"))),
Asset::Atlas => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))),
Asset::AtlasJson => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.json"))),
}
}
}

View File

@@ -11,7 +11,7 @@ use crate::platform::Platform;
pub struct EmscriptenPlatform;
impl Platform for EmscriptenPlatform {
fn sleep(&self, duration: Duration) {
fn sleep(&self, duration: Duration, _focused: bool) {
unsafe {
emscripten_sleep(duration.as_millis() as u32);
}

View File

@@ -11,7 +11,7 @@ pub mod emscripten;
/// Platform abstraction trait that defines cross-platform functionality.
pub trait Platform {
/// Sleep for the specified duration using platform-appropriate method.
fn sleep(&self, duration: Duration);
fn sleep(&self, duration: Duration, focused: bool);
/// Get the current time in seconds since some reference point.
/// This is available for future use in timing and performance monitoring.

View File

@@ -2,7 +2,8 @@
use pacman::{
asset::{get_asset_bytes, Asset},
texture::sprite::SpriteAtlas,
game::state::ATLAS_FRAMES,
texture::sprite::{AtlasMapper, SpriteAtlas},
};
use sdl2::{
image::LoadTexture,
@@ -28,12 +29,13 @@ pub fn setup_sdl() -> Result<(Canvas<Window>, TextureCreator<WindowContext>, Sdl
pub fn create_atlas(canvas: &mut sdl2::render::Canvas<sdl2::video::Window>) -> SpriteAtlas {
let texture_creator = canvas.texture_creator();
let atlas_bytes = get_asset_bytes(Asset::Atlas).unwrap();
let atlas_json = get_asset_bytes(Asset::AtlasJson).unwrap();
let texture = texture_creator.load_texture_bytes(&atlas_bytes).unwrap();
let texture: Texture<'static> = unsafe { std::mem::transmute(texture) };
let mapper: pacman::texture::sprite::AtlasMapper = serde_json::from_slice(&atlas_json).unwrap();
let atlas_mapper = AtlasMapper {
frames: ATLAS_FRAMES.into_iter().map(|(k, v)| (k.to_string(), *v)).collect(),
};
SpriteAtlas::new(texture, mapper)
SpriteAtlas::new(texture, atlas_mapper)
}