diff --git a/Cargo.lock b/Cargo.lock index 8fb0c16..a331113 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,7 +663,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pacman" -version = "0.78.4" +version = "0.78.5" dependencies = [ "anyhow", "bevy_ecs", diff --git a/Cargo.toml b/Cargo.toml index bb6ee62..8553a4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pacman" -version = "0.78.4" +version = "0.78.5" authors = ["Xevion"] edition = "2021" rust-version = "1.86.0" diff --git a/src/asset.rs b/src/asset.rs index 2328559..34a639e 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,4 +1,3 @@ -#![allow(dead_code)] //! Cross-platform asset loading abstraction. //! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem. @@ -62,7 +61,7 @@ mod imp { /// Returns `AssetError::NotFound` if the asset file cannot be located (Emscripten only), /// or `AssetError::Io` for filesystem I/O failures. pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { - trace!(asset = ?asset, path = asset.path(), "Loading game asset"); + trace!(asset = ?asset, "Loading game asset"); let result = platform::get_asset_bytes(asset); match &result { Ok(bytes) => trace!(asset = ?asset, size_bytes = bytes.len(), "Asset loaded successfully"), diff --git a/src/audio.rs b/src/audio.rs index 3b2aa18..8425f9b 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -12,7 +12,6 @@ const SOUND_ASSETS: [Asset; 4] = [Asset::Wav1, Asset::Wav2, Asset::Wav3, Asset:: /// This struct is responsible for initializing the audio device, loading sounds, /// and playing them. If audio fails to initialize, it will be disabled and all /// functions will silently do nothing. -#[allow(dead_code)] pub struct Audio { _mixer_context: Option, sounds: Vec, @@ -144,7 +143,6 @@ impl Audio { /// Automatically rotates through the four eating sound assets. The sound plays on channel 0 and the internal sound index /// advances to the next variant. Silently returns if audio is disabled, muted, /// or no sounds were loaded successfully. - #[allow(dead_code)] pub fn eat(&mut self) { if self.disabled || self.muted || self.sounds.is_empty() { return; @@ -211,7 +209,6 @@ impl Audio { /// Audio can be disabled due to SDL2_mixer initialization failures, missing /// audio device, or failure to load any sound assets. When disabled, all /// audio operations become no-ops. - #[allow(dead_code)] pub fn is_disabled(&self) -> bool { self.disabled } diff --git a/src/error.rs b/src/error.rs index fd9bf80..184216b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -46,6 +46,7 @@ pub enum AssetError { #[error("IO error: {0}")] Io(#[from] io::Error), + // This error is only possible on Emscripten, as the assets are loaded from a 'filesystem' of sorts (while on Desktop, they are included in the binary at compile time) #[allow(dead_code)] #[error("Asset not found: {0}")] NotFound(String), @@ -53,12 +54,9 @@ pub enum AssetError { /// Platform-specific errors. #[derive(thiserror::Error, Debug)] -#[allow(dead_code)] pub enum PlatformError { #[error("Console initialization failed: {0}")] ConsoleInit(String), - #[error("Platform-specific error: {0}")] - Other(String), } /// Error type for map parsing operations. @@ -110,55 +108,3 @@ pub enum MapError { /// Result type for game operations. pub type GameResult = Result; - -/// Helper trait for converting other error types to GameError. -pub trait IntoGameError { - #[allow(dead_code)] - fn into_game_error(self) -> GameResult; -} - -impl IntoGameError for Result -where - E: std::error::Error + Send + Sync + 'static, -{ - fn into_game_error(self) -> GameResult { - self.map_err(|e| GameError::InvalidState(e.to_string())) - } -} - -/// Helper trait for converting Option to GameResult with a custom error. -pub trait OptionExt { - #[allow(dead_code)] - fn ok_or_game_error(self, f: F) -> GameResult - where - F: FnOnce() -> GameError; -} - -impl OptionExt for Option { - fn ok_or_game_error(self, f: F) -> GameResult - where - F: FnOnce() -> GameError, - { - self.ok_or_else(f) - } -} - -/// Helper trait for converting Result to GameResult with context. -pub trait ResultExt { - #[allow(dead_code)] - fn with_context(self, f: F) -> GameResult - where - F: FnOnce(&E) -> GameError; -} - -impl ResultExt for Result -where - E: std::error::Error + Send + Sync + 'static, -{ - fn with_context(self, f: F) -> GameResult - where - F: FnOnce(&E) -> GameError, - { - self.map_err(|e| f(&e)) - } -} diff --git a/src/formatter.rs b/src/formatter.rs index 3878c6a..680efeb 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -150,11 +150,3 @@ pub fn increment_tick() { pub fn get_tick_count() -> u64 { TICK_COUNTER.load(Ordering::Relaxed) } - -/// Reset the tick counter to 0 -/// -/// This can be used for testing or when restarting the game -#[allow(dead_code)] -pub fn reset_tick_counter() { - TICK_COUNTER.store(0, Ordering::Relaxed); -} diff --git a/src/main.rs b/src/main.rs index 1a9153f..9ebcf51 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ // Note: This disables the console window on Windows. We manually re-attach to the parent terminal or process later on. #![windows_subsystem = "windows"] #![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![cfg_attr(coverage_nightly, coverage(off))] use crate::{app::App, constants::LOOP_TIME}; use tracing::info; +// These modules are excluded from coverage. #[cfg_attr(coverage_nightly, coverage(off))] mod app; #[cfg_attr(coverage_nightly, coverage(off))] @@ -29,7 +31,6 @@ mod texture; /// /// This function initializes SDL, the window, the game state, and then enters /// the main game loop. -#[cfg_attr(coverage_nightly, coverage(off))] pub fn main() { // On Windows, this connects output streams to the console dynamically // On Emscripten, this connects the subscriber to the browser console diff --git a/src/platform/emscripten.rs b/src/platform/emscripten.rs index 0be3be9..d4c4574 100644 --- a/src/platform/emscripten.rs +++ b/src/platform/emscripten.rs @@ -11,11 +11,8 @@ use std::io::{self, Read, Write}; use std::time::Duration; // Emscripten FFI functions -#[allow(dead_code)] extern "C" { fn emscripten_sleep(ms: u32); - fn emscripten_get_element_css_size(target: *const u8, width: *mut f64, height: *mut f64) -> i32; - // Standard C functions that Emscripten redirects to console fn printf(format: *const u8, ...) -> i32; } @@ -65,20 +62,6 @@ impl Write for EmscriptenConsoleWriter { } } -#[allow(dead_code)] -pub fn get_canvas_size() -> Option<(u32, u32)> { - let mut width = 0.0; - let mut height = 0.0; - - unsafe { - emscripten_get_element_css_size(c"canvas".as_ptr().cast(), &mut width, &mut height); - if width == 0.0 || height == 0.0 { - return None; - } - } - Some((width as u32, height as u32)) -} - pub fn get_asset_bytes(asset: Asset) -> Result, AssetError> { let path = format!("assets/game/{}", asset.path()); let mut rwops = RWops::from_file(&path, "rb").map_err(|_| AssetError::NotFound(asset.path().to_string()))?; diff --git a/src/platform/tracing_buffer.rs b/src/platform/tracing_buffer.rs index 649625f..e955bc1 100644 --- a/src/platform/tracing_buffer.rs +++ b/src/platform/tracing_buffer.rs @@ -1,4 +1,3 @@ -#![allow(dead_code)] //! Buffered tracing setup for handling logs before console attachment. use crate::formatter::CustomFormatter; diff --git a/src/systems/common/components.rs b/src/systems/common/components.rs index 45b1f25..16fc1bf 100644 --- a/src/systems/common/components.rs +++ b/src/systems/common/components.rs @@ -1,6 +1,6 @@ use bevy_ecs::{component::Component, resource::Resource}; -use crate::map::graph::TraversalFlags; +use crate::{map::graph::TraversalFlags, systems::FruitType}; /// A tag component denoting the type of entity. #[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -9,7 +9,8 @@ pub enum EntityType { Ghost, Pellet, PowerPellet, - Fruit(crate::texture::sprites::FruitSprite), + Fruit(FruitType), + Effect, } impl EntityType { diff --git a/src/systems/debug.rs b/src/systems/debug.rs index 01109ff..a1122ae 100644 --- a/src/systems/debug.rs +++ b/src/systems/debug.rs @@ -1,5 +1,4 @@ //! Debug rendering system -#[cfg_attr(coverage_nightly, feature(coverage_attribute))] use crate::constants::{self, BOARD_PIXEL_OFFSET}; use crate::map::builder::Map; use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings}; diff --git a/src/systems/ghost.rs b/src/systems/ghost.rs index fe6725a..594fced 100644 --- a/src/systems/ghost.rs +++ b/src/systems/ghost.rs @@ -59,17 +59,6 @@ impl Ghost { Ghost::Clyde => 0.85, } } - - /// Returns the ghost's color for debug rendering. - #[allow(dead_code)] - pub fn debug_color(&self) -> sdl2::pixels::Color { - match self { - Ghost::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red - Ghost::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink - Ghost::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan - Ghost::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange - } - } } #[derive(Component, Debug, Clone, Copy)] diff --git a/src/systems/item.rs b/src/systems/item.rs index 47a00f7..d8bc9d0 100644 --- a/src/systems/item.rs +++ b/src/systems/item.rs @@ -5,69 +5,71 @@ use bevy_ecs::{ query::With, system::{Commands, NonSendMut, Query, Res, ResMut, Single}, }; +use strum_macros::IntoStaticStr; use tracing::{debug, trace}; use crate::{ - constants::collider::FRUIT_SIZE, + constants, map::builder::Map, - systems::{common::bundles::ItemBundle, Collider, Position, Renderable}, - texture::{sprite::SpriteAtlas, sprites::GameSprite}, + systems::{common::bundles::ItemBundle, Collider, Position, Renderable, TimeToLive}, + texture::{ + sprite::SpriteAtlas, + sprites::{EffectSprite, GameSprite}, + }, }; use crate::{ constants::animation::FRIGHTENED_FLASH_START_TICKS, events::GameEvent, systems::common::components::EntityType, - systems::lifetime::TimeToLive, - systems::{AudioEvent, GhostCollider, GhostState, ItemCollider, LinearAnimation, PacmanCollider, ScoreResource}, - texture::animated::TileSequence, + systems::{AudioEvent, GhostCollider, GhostState, ItemCollider, PacmanCollider, ScoreResource}, }; /// Tracks the number of pellets consumed by the player for fruit spawning mechanics. #[derive(bevy_ecs::resource::Resource, Debug, Default)] pub struct PelletCount(pub u32); -/// Maps fruit score values to bonus sprite indices for displaying bonus points -fn fruit_score_to_sprite_index(score: u32) -> u8 { - match score { - 100 => 0, // Cherry - 300 => 2, // Strawberry - 500 => 3, // Orange - 700 => 4, // Apple - 1000 => 6, // Melon - 2000 => 8, // Galaxian - 3000 => 9, // Bell - 5000 => 10, // Key - _ => 0, // Default to 100 points sprite - } +/// Represents the different fruit sprites that can appear as bonus items. +#[derive(IntoStaticStr, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[strum(serialize_all = "snake_case")] +pub enum FruitType { + Cherry, + Strawberry, + Orange, + Apple, + Melon, + Galaxian, + Bell, + Key, } -/// Maps sprite index to the corresponding effect sprite path (same as in state.rs) -fn sprite_index_to_path(index: u8) -> &'static str { - match index { - 0 => "effects/100.png", - 1 => "effects/200.png", - 2 => "effects/300.png", - 3 => "effects/400.png", - 4 => "effects/700.png", - 5 => "effects/800.png", - 6 => "effects/1000.png", - 7 => "effects/1600.png", - 8 => "effects/2000.png", - 9 => "effects/3000.png", - 10 => "effects/5000.png", - _ => "effects/100.png", // fallback to index 0 +impl FruitType { + /// Returns the score value for this fruit type. + pub fn score_value(self) -> u32 { + match self { + FruitType::Cherry => 100, + FruitType::Strawberry => 300, + FruitType::Orange => 500, + FruitType::Apple => 700, + FruitType::Melon => 1000, + FruitType::Galaxian => 2000, + FruitType::Bell => 3000, + FruitType::Key => 5000, + } } -} -/// Determines if a collision between two entity types should be handled by the item system. -/// -/// Returns `true` if one entity is a player and the other is a collectible item. -#[allow(dead_code)] -pub fn is_valid_item_collision(entity1: EntityType, entity2: EntityType) -> bool { - match (entity1, entity2) { - (EntityType::Player, entity) | (entity, EntityType::Player) => entity.is_collectible(), - _ => false, + pub fn from_index(index: u8) -> Self { + match index { + 0 => FruitType::Cherry, + 1 => FruitType::Strawberry, + 2 => FruitType::Orange, + 3 => FruitType::Apple, + 4 => FruitType::Melon, + 5 => FruitType::Galaxian, + 6 => FruitType::Bell, + 7 => FruitType::Key, + _ => panic!("Invalid fruit index: {}", index), + } } } @@ -81,7 +83,6 @@ pub fn item_system( item_query: Query<(Entity, &EntityType, &Position), With>, mut ghost_query: Query<&mut GhostState, With>, mut events: EventWriter, - atlas: NonSendMut, ) { for event in collision_events.read() { if let GameEvent::Collision(entity1, entity2) = event { @@ -95,37 +96,11 @@ pub fn item_system( }; // Get the item type and update score - if let Ok((item_ent, entity_type, item_position)) = item_query.get(item_entity) { + if let Ok((item_ent, entity_type, position)) = item_query.get(item_entity) { if let Some(score_value) = entity_type.score_value() { trace!(item_entity = ?item_ent, item_type = ?entity_type, score_value, new_score = score.0 + score_value, "Item collected by player"); score.0 += score_value; - // Spawn bonus sprite for fruits at the fruit's position (similar to ghost eating bonus) - if matches!(entity_type, EntityType::Fruit(_)) { - let sprite_index = fruit_score_to_sprite_index(score_value); - let sprite_path = sprite_index_to_path(sprite_index); - - if let Ok(sprite_tile) = SpriteAtlas::get_tile(&atlas, sprite_path) { - let tile_sequence = TileSequence::single(sprite_tile); - let animation = LinearAnimation::new(tile_sequence, 1); - - commands.spawn(( - *item_position, - Renderable { - sprite: sprite_tile, - layer: 2, // Above other entities - }, - animation, - TimeToLive::new(120), // 2 seconds at 60 FPS - )); - - debug!( - fruit_score = score_value, - sprite_index, "Fruit bonus sprite spawned at fruit position" - ); - } - } - // Remove the collected item commands.entity(item_ent).despawn(); @@ -135,12 +110,21 @@ pub fn item_system( trace!(pellet_count = pellet_count.0, "Pellet consumed"); // Check if we should spawn a fruit - if pellet_count.0 == 70 || pellet_count.0 == 170 { + if pellet_count.0 == 5 || pellet_count.0 == 170 { debug!(pellet_count = pellet_count.0, "Fruit spawn milestone reached"); - commands.trigger(SpawnFruitTrigger); + commands.trigger(SpawnTrigger::Fruit); } } + // Trigger bonus points effect if a fruit is collected + if matches!(*entity_type, EntityType::Fruit(_)) { + commands.trigger(SpawnTrigger::Bonus { + position: *position, + value: entity_type.score_value().unwrap(), + ttl: 60 * 2, + }); + } + // Trigger audio if appropriate if entity_type.is_collectible() { events.write(AudioEvent::PlayEat); @@ -169,30 +153,57 @@ pub fn item_system( } /// Trigger to spawn a fruit -#[derive(Event, Clone, Copy, Debug, PartialEq, Eq)] -pub struct SpawnFruitTrigger; +#[derive(Event, Clone, Copy, Debug)] +pub enum SpawnTrigger { + Fruit, + Bonus { position: Position, value: u32, ttl: u32 }, +} pub fn spawn_fruit_observer( - _: Trigger, + trigger: Trigger, mut commands: Commands, atlas: NonSendMut, map: Res, ) { - // Use cherry sprite as the default fruit (first fruit in original Pac-Man) - let fruit_sprite = &atlas - .get_tile(&GameSprite::Fruit(crate::texture::sprites::FruitSprite::Cherry).to_path()) - .unwrap(); + let entity = match *trigger { + SpawnTrigger::Fruit => { + // Use cherry sprite as the default fruit (first fruit in original Pac-Man) + let sprite = &atlas + .get_tile(&GameSprite::Fruit(FruitType::from_index(0)).to_path()) + .unwrap(); + let bundle = ItemBundle { + position: map.start_positions.fruit_spawn, + sprite: Renderable { + sprite: *sprite, + layer: 1, + }, + entity_type: EntityType::Fruit(FruitType::Cherry), + collider: Collider { + size: constants::collider::FRUIT_SIZE, + }, + item_collider: ItemCollider, + }; - let fruit_entity = commands.spawn(ItemBundle { - position: map.start_positions.fruit_spawn, - sprite: Renderable { - sprite: *fruit_sprite, - layer: 1, - }, - entity_type: EntityType::Fruit(crate::texture::sprites::FruitSprite::Cherry), - collider: Collider { size: FRUIT_SIZE }, - item_collider: ItemCollider, - }); + commands.spawn(bundle) + } + SpawnTrigger::Bonus { position, value, ttl } => { + let sprite = &atlas + .get_tile(&GameSprite::Effect(EffectSprite::Bonus(value)).to_path()) + .unwrap(); - debug!(fruit_entity = ?fruit_entity.id(), fruit_spawn_node = ?map.start_positions.fruit_spawn, "Fruit spawned"); + let bundle = ( + position, + TimeToLive::new(ttl), + Renderable { + sprite: *sprite, + layer: 1, + }, + EntityType::Effect, + ); + + commands.spawn(bundle) + } + }; + + debug!(entity = ?entity.id(), "Entity spawned via trigger"); } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index 22d9cb9..d9774de 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -1,5 +1,6 @@ //! This module contains all the systems in the game. +// These modules are excluded from coverage. #[cfg_attr(coverage_nightly, coverage(off))] pub mod audio; #[cfg_attr(coverage_nightly, coverage(off))] diff --git a/src/systems/state.rs b/src/systems/state.rs index 8dfd848..bfb6074 100644 --- a/src/systems/state.rs +++ b/src/systems/state.rs @@ -2,20 +2,20 @@ use std::mem::discriminant; use tracing::{debug, info, warn}; use crate::events::StageTransition; +use crate::systems::SpawnTrigger; use crate::{ map::builder::Map, systems::{ AudioEvent, Blinking, DirectionalAnimation, Dying, Eaten, Frozen, Ghost, GhostCollider, GhostState, Hidden, - LinearAnimation, Looping, NodeId, PlayerControlled, Position, Renderable, TimeToLive, + LinearAnimation, Looping, NodeId, PlayerControlled, Position, }, - texture::{animated::TileSequence, sprite::SpriteAtlas}, }; use bevy_ecs::{ entity::Entity, event::{EventReader, EventWriter}, query::{With, Without}, resource::Resource, - system::{Commands, NonSendMut, Query, Res, ResMut, Single}, + system::{Commands, Query, Res, ResMut, Single}, }; #[derive(Resource, Clone)] @@ -92,24 +92,6 @@ impl Default for PlayerLives { } /// Handles startup sequence transitions and component management -/// Maps sprite index to the corresponding effect sprite path -fn sprite_index_to_path(index: u8) -> &'static str { - match index { - 0 => "effects/100.png", - 1 => "effects/200.png", - 2 => "effects/300.png", - 3 => "effects/400.png", - 4 => "effects/700.png", - 5 => "effects/800.png", - 6 => "effects/1000.png", - 7 => "effects/1600.png", - 8 => "effects/2000.png", - 9 => "effects/3000.png", - 10 => "effects/5000.png", - _ => "effects/200.png", // fallback to index 1 - } -} - #[allow(clippy::too_many_arguments)] #[allow(clippy::type_complexity)] pub fn stage_system( @@ -124,7 +106,6 @@ pub fn stage_system( mut blinking_query: Query>, player: Single<(Entity, &mut Position), With>, mut ghost_query: Query<(Entity, &Ghost, &mut Position), (With, Without)>, - atlas: NonSendMut, ) { let old_state = *game_state; let mut new_state: Option = None; @@ -246,23 +227,12 @@ pub fn stage_system( commands.entity(ghost_entity).insert(Hidden); // Spawn bonus points entity at Pac-Man's position - let sprite_index = 1; // Index 1 = 200 points (default for ghost eating) - let sprite_path = sprite_index_to_path(sprite_index); - - if let Ok(sprite_tile) = SpriteAtlas::get_tile(&atlas, sprite_path) { - let tile_sequence = TileSequence::single(sprite_tile); - let animation = LinearAnimation::new(tile_sequence, 1); - - commands.spawn(( - Position::Stopped { node }, - Renderable { - sprite: sprite_tile, - layer: 2, // Above other entities - }, - animation, - TimeToLive::new(30), - )); - } + commands.trigger(SpawnTrigger::Bonus { + position: Position::Stopped { node }, + // TODO: Doubling score value for each consecutive ghost eaten + value: 200, + ttl: 30, + }); } (GameStage::GhostEatenPause { ghost_entity, .. }, GameStage::Playing) => { // Unfreeze and reveal the player & all ghosts diff --git a/src/texture/animated.rs b/src/texture/animated.rs index befc288..50ba15f 100644 --- a/src/texture/animated.rs +++ b/src/texture/animated.rs @@ -14,11 +14,6 @@ impl TileSequence { Self { tiles: tiles.to_vec() } } - /// Creates a tile sequence with a single tile. - pub fn single(tile: AtlasTile) -> Self { - Self { tiles: vec![tile] } - } - /// Returns the tile at the given frame index, wrapping if necessary pub fn get_tile(&self, frame: usize) -> AtlasTile { if self.tiles.is_empty() { diff --git a/src/texture/sprite.rs b/src/texture/sprite.rs index 8761b11..e73f881 100644 --- a/src/texture/sprite.rs +++ b/src/texture/sprite.rs @@ -58,19 +58,6 @@ impl AtlasTile { canvas.copy(&atlas.texture, src, dest).map_err(TextureError::RenderFailed)?; Ok(()) } - - /// Creates a new atlas tile. - #[allow(dead_code)] - pub fn new(pos: U16Vec2, size: U16Vec2, color: Option) -> Self { - Self { pos, size, color } - } - - /// Sets the color of the tile. - #[allow(dead_code)] - pub fn with_color(mut self, color: Color) -> Self { - self.color = Some(color); - self - } } /// High-performance sprite atlas providing fast texture region lookups and rendering. @@ -120,32 +107,4 @@ impl SpriteAtlas { color: self.default_color, }) } - - #[allow(dead_code)] - pub fn set_color(&mut self, color: Color) { - self.default_color = Some(color); - } - - #[allow(dead_code)] - pub fn texture(&self) -> &Texture { - &self.texture - } - - /// Returns the number of tiles in the atlas. - #[allow(dead_code)] - pub fn tiles_count(&self) -> usize { - self.tiles.len() - } - - /// Returns true if the atlas has a tile with the given name. - #[allow(dead_code)] - pub fn has_tile(&self, name: &str) -> bool { - self.tiles.contains_key(name) - } - - /// Returns the default color of the atlas. - #[allow(dead_code)] - pub fn default_color(&self) -> Option { - self.default_color - } } diff --git a/src/texture/sprites.rs b/src/texture/sprites.rs index 8b34a3a..ed9d589 100644 --- a/src/texture/sprites.rs +++ b/src/texture/sprites.rs @@ -5,7 +5,10 @@ //! The `GameSprite` enum is the main entry point, and its `to_path` method //! generates the correct path for a given sprite in the texture atlas. -use crate::{map::direction::Direction, systems::Ghost}; +use crate::{ + map::direction::Direction, + systems::{FruitType, Ghost}, +}; /// Represents the different sprites for Pac-Man. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -47,34 +50,10 @@ pub enum MazeSprite { Energizer, } -/// Represents the different fruit sprites that can appear as bonus items. +/// Represents the different effect sprites that can appear as bonus items. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[allow(dead_code)] -pub enum FruitSprite { - Cherry, - Strawberry, - Orange, - Apple, - Melon, - Galaxian, - Bell, - Key, -} - -impl FruitSprite { - /// Returns the score value for this fruit type. - pub fn score_value(self) -> u32 { - match self { - FruitSprite::Cherry => 100, - FruitSprite::Strawberry => 300, - FruitSprite::Orange => 500, - FruitSprite::Apple => 700, - FruitSprite::Melon => 1000, - FruitSprite::Galaxian => 2000, - FruitSprite::Bell => 3000, - FruitSprite::Key => 5000, - } - } +pub enum EffectSprite { + Bonus(u32), } /// A top-level enum that encompasses all game sprites. @@ -83,7 +62,8 @@ pub enum GameSprite { Pacman(PacmanSprite), Ghost(GhostSprite), Maze(MazeSprite), - Fruit(FruitSprite), + Fruit(FruitType), + Effect(EffectSprite), } impl GameSprite { @@ -138,14 +118,16 @@ impl GameSprite { GameSprite::Maze(MazeSprite::Energizer) => "maze/energizer.png".to_string(), // Fruit sprites - GameSprite::Fruit(FruitSprite::Cherry) => "edible/cherry.png".to_string(), - GameSprite::Fruit(FruitSprite::Strawberry) => "edible/strawberry.png".to_string(), - GameSprite::Fruit(FruitSprite::Orange) => "edible/orange.png".to_string(), - GameSprite::Fruit(FruitSprite::Apple) => "edible/apple.png".to_string(), - GameSprite::Fruit(FruitSprite::Melon) => "edible/melon.png".to_string(), - GameSprite::Fruit(FruitSprite::Galaxian) => "edible/galaxian.png".to_string(), - GameSprite::Fruit(FruitSprite::Bell) => "edible/bell.png".to_string(), - GameSprite::Fruit(FruitSprite::Key) => "edible/key.png".to_string(), + GameSprite::Fruit(fruit) => format!("edible/{}.png", Into::<&'static str>::into(fruit)), + + // Effect sprites + GameSprite::Effect(EffectSprite::Bonus(value)) => match value { + 100 | 200 | 300 | 400 | 700 | 800 | 1000 | 2000 | 3000 | 5000 => format!("effects/{}.png", value), + _ => { + tracing::warn!("Invalid bonus value: {}", value); + "effects/100.png".to_string() + } + }, } } } diff --git a/src/texture/text.rs b/src/texture/text.rs index fb039da..98445cc 100644 --- a/src/texture/text.rs +++ b/src/texture/text.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - //! This module provides text rendering using the texture atlas. //! //! The TextTexture system renders text from the atlas using character mapping. @@ -109,6 +107,7 @@ impl TextTexture { } } + #[allow(dead_code)] pub fn get_char_map(&self) -> &HashMap { &self.char_map } @@ -167,26 +166,6 @@ impl TextTexture { Ok(()) } - /// Sets the default color for text rendering. - pub fn set_color(&mut self, color: Color) { - self.default_color = Some(color); - } - - /// Gets the current default color. - pub fn color(&self) -> Option { - self.default_color - } - - /// Sets the scale for text rendering. - pub fn set_scale(&mut self, scale: f32) { - self.scale = scale; - } - - /// Gets the current scale. - pub fn scale(&self) -> f32 { - self.scale - } - /// Calculates the width of a string in pixels at the current scale. pub fn text_width(&self, text: &str) -> u32 { let char_width = (8.0 * self.scale) as u32; diff --git a/tests/common/mod.rs b/tests/common.rs similarity index 97% rename from tests/common/mod.rs rename to tests/common.rs index 54d0ccd..5458dd2 100644 --- a/tests/common/mod.rs +++ b/tests/common.rs @@ -14,7 +14,8 @@ use pacman::{ }, systems::{ AudioEvent, AudioState, BufferedDirection, Collider, DebugState, DeltaTime, EntityType, Ghost, GhostCollider, GhostState, - GlobalState, ItemCollider, MovementModifiers, PacmanCollider, PlayerControlled, Position, ScoreResource, Velocity, + GlobalState, ItemCollider, MovementModifiers, PacmanCollider, PelletCount, PlayerControlled, Position, ScoreResource, + Velocity, }, texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas}, }; @@ -85,6 +86,7 @@ pub fn create_test_world() -> World { world.insert_resource(AudioState::default()); world.insert_resource(GlobalState { exit: false }); world.insert_resource(DebugState::default()); + world.insert_resource(PelletCount(0)); world.insert_resource(DeltaTime { seconds: 1.0 / 60.0, ticks: 1, diff --git a/tests/error.rs b/tests/error.rs deleted file mode 100644 index 41ee35b..0000000 --- a/tests/error.rs +++ /dev/null @@ -1,66 +0,0 @@ -use pacman::error::{GameError, GameResult, IntoGameError, OptionExt, ResultExt}; -use speculoos::prelude::*; -use std::io; - -#[test] -fn test_into_game_error_trait() { - let result: Result = Err(io::Error::new(io::ErrorKind::Other, "test error")); - let game_result: GameResult = result.into_game_error(); - - assert_that(&game_result.is_err()).is_true(); - if let Err(GameError::InvalidState(msg)) = game_result { - assert_that(&msg.contains("test error")).is_true(); - } else { - panic!("Expected InvalidState error"); - } -} - -#[test] -fn test_into_game_error_trait_success() { - let result: Result = Ok(42); - let game_result: GameResult = result.into_game_error(); - - assert_that(&game_result.unwrap()).is_equal_to(42); -} - -#[test] -fn test_option_ext_some() { - let option: Option = Some(42); - let result: GameResult = option.ok_or_game_error(|| GameError::InvalidState("Not found".to_string())); - - assert_that(&result.unwrap()).is_equal_to(42); -} - -#[test] -fn test_option_ext_none() { - let option: Option = None; - let result: GameResult = option.ok_or_game_error(|| GameError::InvalidState("Not found".to_string())); - - assert_that(&result.is_err()).is_true(); - if let Err(GameError::InvalidState(msg)) = result { - assert_that(&msg).is_equal_to("Not found".to_string()); - } else { - panic!("Expected InvalidState error"); - } -} - -#[test] -fn test_result_ext_success() { - let result: Result = Ok(42); - let game_result: GameResult = result.with_context(|_| GameError::InvalidState("Context".to_string())); - - assert_that(&game_result.unwrap()).is_equal_to(42); -} - -#[test] -fn test_result_ext_error() { - let result: Result = Err(io::Error::new(io::ErrorKind::Other, "original error")); - let game_result: GameResult = result.with_context(|_| GameError::InvalidState("Context error".to_string())); - - assert_that(&game_result.is_err()).is_true(); - if let Err(GameError::InvalidState(msg)) = game_result { - assert_that(&msg).is_equal_to("Context error".to_string()); - } else { - panic!("Expected InvalidState error"); - } -} diff --git a/tests/item.rs b/tests/item.rs index bc850c0..cc69af5 100644 --- a/tests/item.rs +++ b/tests/item.rs @@ -1,5 +1,5 @@ use bevy_ecs::{entity::Entity, system::RunSystemOnce}; -use pacman::systems::{is_valid_item_collision, item_system, EntityType, GhostState, Position, ScoreResource}; +use pacman::systems::{item_system, EntityType, GhostState, Position, ScoreResource}; use speculoos::prelude::*; mod common; @@ -24,21 +24,6 @@ fn test_is_collectible_item() { assert_that(&EntityType::Ghost.is_collectible()).is_false(); } -#[test] -fn test_is_valid_item_collision() { - // Player-item collisions should be valid - assert_that(&is_valid_item_collision(EntityType::Player, EntityType::Pellet)).is_true(); - assert_that(&is_valid_item_collision(EntityType::Player, EntityType::PowerPellet)).is_true(); - assert_that(&is_valid_item_collision(EntityType::Pellet, EntityType::Player)).is_true(); - assert_that(&is_valid_item_collision(EntityType::PowerPellet, EntityType::Player)).is_true(); - - // Non-player-item collisions should be invalid - assert_that(&is_valid_item_collision(EntityType::Player, EntityType::Ghost)).is_false(); - assert_that(&is_valid_item_collision(EntityType::Ghost, EntityType::Pellet)).is_false(); - assert_that(&is_valid_item_collision(EntityType::Pellet, EntityType::PowerPellet)).is_false(); - assert_that(&is_valid_item_collision(EntityType::Player, EntityType::Player)).is_false(); -} - #[test] fn test_item_system_pellet_collection() { let mut world = common::create_test_world(); diff --git a/tests/sprite.rs b/tests/sprite.rs deleted file mode 100644 index 4c50c29..0000000 --- a/tests/sprite.rs +++ /dev/null @@ -1,70 +0,0 @@ -use glam::U16Vec2; -use pacman::texture::sprite::{AtlasMapper, AtlasTile, MapperFrame}; -use sdl2::pixels::Color; -use speculoos::prelude::*; -use std::collections::HashMap; - -mod common; - -#[test] -fn test_atlas_mapper_frame_lookup() { - let mut frames = HashMap::new(); - frames.insert( - "test".to_string(), - MapperFrame { - pos: U16Vec2::new(10, 20), - size: U16Vec2::new(32, 64), - }, - ); - - let mapper = AtlasMapper { frames }; - - // Test direct frame lookup - let frame = mapper.frames.get("test"); - assert_that(&frame.is_some()).is_true(); - let frame = frame.unwrap(); - assert_that(&frame.pos).is_equal_to(U16Vec2::new(10, 20)); - assert_that(&frame.size).is_equal_to(U16Vec2::new(32, 64)); -} - -#[test] -fn test_atlas_mapper_multiple_frames() { - let mut frames = HashMap::new(); - frames.insert( - "tile1".to_string(), - MapperFrame { - pos: U16Vec2::new(0, 0), - size: U16Vec2::new(32, 32), - }, - ); - frames.insert( - "tile2".to_string(), - MapperFrame { - pos: U16Vec2::new(32, 0), - size: U16Vec2::new(64, 64), - }, - ); - - let mapper = AtlasMapper { frames }; - - assert_that(&mapper.frames.len()).is_equal_to(2); - assert_that(&mapper.frames.contains_key("tile1")).is_true(); - assert_that(&mapper.frames.contains_key("tile2")).is_true(); - assert_that(&mapper.frames.contains_key("tile3")).is_false(); - assert_that(&mapper.frames.contains_key("nonexistent")).is_false(); -} - -#[test] -fn test_atlas_tile_new_and_with_color() { - let pos = U16Vec2::new(10, 20); - let size = U16Vec2::new(30, 40); - let color = Color::RGB(100, 150, 200); - - let tile = AtlasTile::new(pos, size, None); - assert_that(&tile.pos).is_equal_to(pos); - assert_that(&tile.size).is_equal_to(size); - assert_that(&tile.color).is_equal_to(None); - - let tile_with_color = tile.with_color(color); - assert_that(&tile_with_color.color).is_equal_to(Some(color)); -} diff --git a/tests/text.rs b/tests/text.rs index b0a9ed2..196121d 100644 --- a/tests/text.rs +++ b/tests/text.rs @@ -81,44 +81,20 @@ fn test_text_scale() -> Result<(), String> { let string = "ABCDEFG !-/\""; let base_width = (string.len() * 8) as u32; - let mut text_texture = TextTexture::new(0.5); - - assert_that(&text_texture.scale()).is_equal_to(0.5); + let text_texture = TextTexture::new(0.5); assert_that(&text_texture.text_height()).is_equal_to(4); assert_that(&text_texture.text_width("")).is_equal_to(0); assert_that(&text_texture.text_width(string)).is_equal_to(base_width / 2); - text_texture.set_scale(2.0); - assert_that(&text_texture.scale()).is_equal_to(2.0); + let text_texture = TextTexture::new(2.0); assert_that(&text_texture.text_height()).is_equal_to(16); assert_that(&text_texture.text_width(string)).is_equal_to(base_width * 2); assert_that(&text_texture.text_width("")).is_equal_to(0); - text_texture.set_scale(1.0); - assert_that(&text_texture.scale()).is_equal_to(1.0); + let text_texture = TextTexture::new(1.0); assert_that(&text_texture.text_height()).is_equal_to(8); assert_that(&text_texture.text_width(string)).is_equal_to(base_width); assert_that(&text_texture.text_width("")).is_equal_to(0); Ok(()) } - -#[test] -fn test_text_color() -> Result<(), String> { - let mut text_texture = TextTexture::new(1.0); - - // Test default color (should be None initially) - assert_that(&text_texture.color()).is_equal_to(None); - - // Test setting color - let test_color = sdl2::pixels::Color::YELLOW; - text_texture.set_color(test_color); - assert_that(&text_texture.color()).is_equal_to(Some(test_color)); - - // Test changing color - let new_color = sdl2::pixels::Color::RED; - text_texture.set_color(new_color); - assert_that(&text_texture.color()).is_equal_to(Some(new_color)); - - Ok(()) -}