diff --git a/src/entity/item.rs b/src/entity/item.rs new file mode 100644 index 0000000..11cf450 --- /dev/null +++ b/src/entity/item.rs @@ -0,0 +1,95 @@ +use crate::{ + constants, + entity::graph::Graph, + error::EntityError, + texture::sprite::{Sprite, SpriteAtlas}, +}; +use sdl2::render::{Canvas, RenderTarget}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ItemType { + Pellet, + Energizer, + #[allow(dead_code)] + Fruit { + kind: FruitKind, + }, +} + +impl ItemType { + pub fn get_score(self) -> u32 { + match self { + ItemType::Pellet => 10, + ItemType::Energizer => 50, + ItemType::Fruit { kind } => kind.get_score(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub enum FruitKind { + Apple, + Strawberry, + Orange, + Melon, + Bell, + Key, + Galaxian, +} + +impl FruitKind { + pub fn get_score(self) -> u32 { + match self { + FruitKind::Apple => 100, + FruitKind::Strawberry => 300, + FruitKind::Orange => 500, + FruitKind::Melon => 700, + FruitKind::Bell => 1000, + FruitKind::Key => 2000, + FruitKind::Galaxian => 3000, + } + } +} + +pub struct Item { + pub node_index: usize, + pub item_type: ItemType, + pub sprite: Sprite, + pub collected: bool, +} + +impl Item { + pub fn new(node_index: usize, item_type: ItemType, sprite: Sprite) -> Self { + Self { + node_index, + item_type, + sprite, + collected: false, + } + } + + pub fn is_collected(&self) -> bool { + self.collected + } + + pub fn collect(&mut self) { + self.collected = true; + } + + pub fn get_score(&self) -> u32 { + self.item_type.get_score() + } + + pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, graph: &Graph) -> anyhow::Result<()> { + if !self.collected { + let node = graph + .get_node(self.node_index) + .ok_or(EntityError::NodeNotFound(self.node_index))?; + let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2(); + self.sprite.render(canvas, atlas, position) + } else { + Ok(()) + } + } +} diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 3539097..700ee6a 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -1,6 +1,7 @@ pub mod direction; pub mod ghost; pub mod graph; +pub mod item; pub mod pacman; pub mod r#trait; pub mod traversal; diff --git a/src/game.rs b/src/game.rs index c7307b6..81b9eac 100644 --- a/src/game.rs +++ b/src/game.rs @@ -18,6 +18,7 @@ use crate::{ constants::{CELL_SIZE, RAW_BOARD}, entity::{ ghost::{Ghost, GhostType}, + item::Item, pacman::Pacman, r#trait::Entity, }, @@ -37,6 +38,7 @@ pub struct Game { pub map: Map, pub pacman: Pacman, pub ghosts: Vec, + pub items: Vec, pub debug_mode: bool, // Rendering resources @@ -87,6 +89,9 @@ impl Game { let audio = Audio::new(); let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?; + // Generate items (pellets and energizers) + let items = map.generate_items(&atlas)?; + // Create ghosts at random positions let mut ghosts = Vec::new(); let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde]; @@ -109,6 +114,7 @@ impl Game { map, pacman, ghosts, + items, debug_mode: false, map_texture, text_texture, @@ -146,6 +152,9 @@ impl Game { self.pacman = Pacman::new(&self.map.graph, pacman_start_node, &self.atlas)?; + // Reset items + self.items = self.map.generate_items(&self.atlas)?; + // Randomize ghost positions let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde]; let mut rng = SmallRng::from_os_rng(); @@ -165,6 +174,26 @@ impl Game { for ghost in &mut self.ghosts { ghost.tick(dt, &self.map.graph); } + + // Check for item collisions + self.check_item_collisions(); + } + + fn check_item_collisions(&mut self) { + let pacman_node = self.pacman.current_node_id(); + + for item in &mut self.items { + if !item.is_collected() && item.node_index == pacman_node { + item.collect(); + self.score += item.get_score(); + + // Handle energizer effects + if matches!(item.item_type, crate::entity::item::ItemType::Energizer) { + // TODO: Make ghosts frightened + tracing::info!("Energizer collected! Ghosts should become frightened."); + } + } + } } pub fn draw(&mut self, canvas: &mut Canvas, backbuffer: &mut Texture) -> GameResult<()> { @@ -174,6 +203,13 @@ impl Game { canvas.clear(); self.map.render(canvas, &mut self.atlas, &mut self.map_texture); + // Render all items + for item in &self.items { + if let Err(e) = item.render(canvas, &mut self.atlas, &self.map.graph) { + tracing::error!("Failed to render item: {}", e); + } + } + // Render all ghosts for ghost in &self.ghosts { if let Err(e) = ghost.render(canvas, &mut self.atlas, &self.map.graph) { diff --git a/src/map/builder.rs b/src/map/builder.rs index 50ea117..cfc7040 100644 --- a/src/map/builder.rs +++ b/src/map/builder.rs @@ -1,11 +1,12 @@ //! Map construction and building functionality. -use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE}; +use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE, RAW_BOARD}; use crate::entity::direction::Direction; 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, SpriteAtlas}; +use crate::texture::sprite::{AtlasTile, Sprite, SpriteAtlas}; use glam::{IVec2, UVec2, Vec2}; use sdl2::render::{Canvas, RenderTarget}; use std::collections::{HashMap, VecDeque}; @@ -187,6 +188,44 @@ impl Map { MapRenderer::render_map(canvas, atlas, map_texture); } + /// Generates Item entities for pellets and energizers from the parsed map. + pub fn generate_items(&self, atlas: &SpriteAtlas) -> GameResult> { + // Pre-load sprites to avoid repeated texture lookups + let pellet_sprite = SpriteAtlas::get_tile(atlas, "maze/pellet.png") + .ok_or_else(|| MapError::InvalidConfig("Pellet texture not found".to_string()))?; + let energizer_sprite = SpriteAtlas::get_tile(atlas, "maze/energizer.png") + .ok_or_else(|| MapError::InvalidConfig("Energizer texture not found".to_string()))?; + + // Pre-allocate with estimated capacity (typical Pac-Man maps have ~240 pellets + 4 energizers) + let mut items = Vec::with_capacity(250); + + // Parse the raw board once + let parsed_map = MapTileParser::parse_board(RAW_BOARD)?; + let map = parsed_map.tiles; + + // Iterate through the map and collect items more efficiently + for (x, row) in map.iter().enumerate() { + for (y, tile) in row.iter().enumerate() { + match tile { + MapTile::Pellet | MapTile::PowerPellet => { + let grid_pos = IVec2::new(x as i32, y as i32); + if let Some(&node_id) = self.grid_to_node.get(&grid_pos) { + let (item_type, sprite) = match tile { + MapTile::Pellet => (ItemType::Pellet, Sprite::new(pellet_sprite)), + MapTile::PowerPellet => (ItemType::Energizer, Sprite::new(energizer_sprite)), + _ => unreachable!(), // We already filtered for these types + }; + items.push(Item::new(node_id, item_type, sprite)); + } + } + _ => {} + } + } + } + + Ok(items) + } + /// Renders a debug visualization with cursor-based highlighting. /// /// This function provides interactive debugging by highlighting the nearest node diff --git a/src/texture/sprite.rs b/src/texture/sprite.rs index 19941d4..6e8af3c 100644 --- a/src/texture/sprite.rs +++ b/src/texture/sprite.rs @@ -6,6 +6,27 @@ use sdl2::render::{Canvas, RenderTarget, Texture}; use serde::Deserialize; use std::collections::HashMap; +/// A simple sprite for stationary items like pellets and energizers. +#[derive(Clone, Debug)] +pub struct Sprite { + pub atlas_tile: AtlasTile, +} + +impl Sprite { + pub fn new(atlas_tile: AtlasTile) -> Self { + Self { atlas_tile } + } + + pub fn render(&self, canvas: &mut Canvas, atlas: &mut SpriteAtlas, position: glam::Vec2) -> Result<()> { + let dest = crate::helpers::centered_with_size( + glam::IVec2::new(position.x as i32, position.y as i32), + glam::UVec2::new(self.atlas_tile.size.x as u32, self.atlas_tile.size.y as u32), + ); + let mut tile = self.atlas_tile; + tile.render(canvas, atlas, dest) + } +} + #[derive(Clone, Debug, Deserialize)] pub struct AtlasMapper { pub frames: HashMap,