refactor: remove dead code, tune lints, remove useless tests

This commit is contained in:
Ryan Walters
2025-09-09 14:20:32 -05:00
parent 139afb2d40
commit ca006b5073
24 changed files with 148 additions and 518 deletions

2
Cargo.lock generated
View File

@@ -663,7 +663,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "pacman" name = "pacman"
version = "0.78.4" version = "0.78.5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bevy_ecs", "bevy_ecs",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "pacman" name = "pacman"
version = "0.78.4" version = "0.78.5"
authors = ["Xevion"] authors = ["Xevion"]
edition = "2021" edition = "2021"
rust-version = "1.86.0" rust-version = "1.86.0"

View File

@@ -1,4 +1,3 @@
#![allow(dead_code)]
//! Cross-platform asset loading abstraction. //! Cross-platform asset loading abstraction.
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem. //! 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), /// Returns `AssetError::NotFound` if the asset file cannot be located (Emscripten only),
/// or `AssetError::Io` for filesystem I/O failures. /// or `AssetError::Io` for filesystem I/O failures.
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> { pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
trace!(asset = ?asset, path = asset.path(), "Loading game asset"); trace!(asset = ?asset, "Loading game asset");
let result = platform::get_asset_bytes(asset); let result = platform::get_asset_bytes(asset);
match &result { match &result {
Ok(bytes) => trace!(asset = ?asset, size_bytes = bytes.len(), "Asset loaded successfully"), Ok(bytes) => trace!(asset = ?asset, size_bytes = bytes.len(), "Asset loaded successfully"),

View File

@@ -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, /// 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 /// and playing them. If audio fails to initialize, it will be disabled and all
/// functions will silently do nothing. /// functions will silently do nothing.
#[allow(dead_code)]
pub struct Audio { pub struct Audio {
_mixer_context: Option<mixer::Sdl2MixerContext>, _mixer_context: Option<mixer::Sdl2MixerContext>,
sounds: Vec<Chunk>, sounds: Vec<Chunk>,
@@ -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 /// 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, /// advances to the next variant. Silently returns if audio is disabled, muted,
/// or no sounds were loaded successfully. /// or no sounds were loaded successfully.
#[allow(dead_code)]
pub fn eat(&mut self) { pub fn eat(&mut self) {
if self.disabled || self.muted || self.sounds.is_empty() { if self.disabled || self.muted || self.sounds.is_empty() {
return; return;
@@ -211,7 +209,6 @@ impl Audio {
/// Audio can be disabled due to SDL2_mixer initialization failures, missing /// Audio can be disabled due to SDL2_mixer initialization failures, missing
/// audio device, or failure to load any sound assets. When disabled, all /// audio device, or failure to load any sound assets. When disabled, all
/// audio operations become no-ops. /// audio operations become no-ops.
#[allow(dead_code)]
pub fn is_disabled(&self) -> bool { pub fn is_disabled(&self) -> bool {
self.disabled self.disabled
} }

View File

@@ -46,6 +46,7 @@ pub enum AssetError {
#[error("IO error: {0}")] #[error("IO error: {0}")]
Io(#[from] io::Error), 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)] #[allow(dead_code)]
#[error("Asset not found: {0}")] #[error("Asset not found: {0}")]
NotFound(String), NotFound(String),
@@ -53,12 +54,9 @@ pub enum AssetError {
/// Platform-specific errors. /// Platform-specific errors.
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
#[allow(dead_code)]
pub enum PlatformError { pub enum PlatformError {
#[error("Console initialization failed: {0}")] #[error("Console initialization failed: {0}")]
ConsoleInit(String), ConsoleInit(String),
#[error("Platform-specific error: {0}")]
Other(String),
} }
/// Error type for map parsing operations. /// Error type for map parsing operations.
@@ -110,55 +108,3 @@ pub enum MapError {
/// Result type for game operations. /// Result type for game operations.
pub type GameResult<T> = Result<T, GameError>; pub type GameResult<T> = Result<T, GameError>;
/// Helper trait for converting other error types to GameError.
pub trait IntoGameError<T> {
#[allow(dead_code)]
fn into_game_error(self) -> GameResult<T>;
}
impl<T, E> IntoGameError<T> for Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn into_game_error(self) -> GameResult<T> {
self.map_err(|e| GameError::InvalidState(e.to_string()))
}
}
/// Helper trait for converting Option to GameResult with a custom error.
pub trait OptionExt<T> {
#[allow(dead_code)]
fn ok_or_game_error<F>(self, f: F) -> GameResult<T>
where
F: FnOnce() -> GameError;
}
impl<T> OptionExt<T> for Option<T> {
fn ok_or_game_error<F>(self, f: F) -> GameResult<T>
where
F: FnOnce() -> GameError,
{
self.ok_or_else(f)
}
}
/// Helper trait for converting Result to GameResult with context.
pub trait ResultExt<T, E> {
#[allow(dead_code)]
fn with_context<F>(self, f: F) -> GameResult<T>
where
F: FnOnce(&E) -> GameError;
}
impl<T, E> ResultExt<T, E> for Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn with_context<F>(self, f: F) -> GameResult<T>
where
F: FnOnce(&E) -> GameError,
{
self.map_err(|e| f(&e))
}
}

View File

@@ -150,11 +150,3 @@ pub fn increment_tick() {
pub fn get_tick_count() -> u64 { pub fn get_tick_count() -> u64 {
TICK_COUNTER.load(Ordering::Relaxed) 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);
}

View File

@@ -1,10 +1,12 @@
// Note: This disables the console window on Windows. We manually re-attach to the parent terminal or process later on. // Note: This disables the console window on Windows. We manually re-attach to the parent terminal or process later on.
#![windows_subsystem = "windows"] #![windows_subsystem = "windows"]
#![cfg_attr(coverage_nightly, feature(coverage_attribute))] #![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![cfg_attr(coverage_nightly, coverage(off))]
use crate::{app::App, constants::LOOP_TIME}; use crate::{app::App, constants::LOOP_TIME};
use tracing::info; use tracing::info;
// These modules are excluded from coverage.
#[cfg_attr(coverage_nightly, coverage(off))] #[cfg_attr(coverage_nightly, coverage(off))]
mod app; mod app;
#[cfg_attr(coverage_nightly, coverage(off))] #[cfg_attr(coverage_nightly, coverage(off))]
@@ -29,7 +31,6 @@ mod texture;
/// ///
/// This function initializes SDL, the window, the game state, and then enters /// This function initializes SDL, the window, the game state, and then enters
/// the main game loop. /// the main game loop.
#[cfg_attr(coverage_nightly, coverage(off))]
pub fn main() { pub fn main() {
// On Windows, this connects output streams to the console dynamically // On Windows, this connects output streams to the console dynamically
// On Emscripten, this connects the subscriber to the browser console // On Emscripten, this connects the subscriber to the browser console

View File

@@ -11,11 +11,8 @@ use std::io::{self, Read, Write};
use std::time::Duration; use std::time::Duration;
// Emscripten FFI functions // Emscripten FFI functions
#[allow(dead_code)]
extern "C" { extern "C" {
fn emscripten_sleep(ms: u32); 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; 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<Cow<'static, [u8]>, AssetError> { pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
let path = format!("assets/game/{}", asset.path()); let path = format!("assets/game/{}", asset.path());
let mut rwops = RWops::from_file(&path, "rb").map_err(|_| AssetError::NotFound(asset.path().to_string()))?; let mut rwops = RWops::from_file(&path, "rb").map_err(|_| AssetError::NotFound(asset.path().to_string()))?;

View File

@@ -1,4 +1,3 @@
#![allow(dead_code)]
//! Buffered tracing setup for handling logs before console attachment. //! Buffered tracing setup for handling logs before console attachment.
use crate::formatter::CustomFormatter; use crate::formatter::CustomFormatter;

View File

@@ -1,6 +1,6 @@
use bevy_ecs::{component::Component, resource::Resource}; 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. /// A tag component denoting the type of entity.
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -9,7 +9,8 @@ pub enum EntityType {
Ghost, Ghost,
Pellet, Pellet,
PowerPellet, PowerPellet,
Fruit(crate::texture::sprites::FruitSprite), Fruit(FruitType),
Effect,
} }
impl EntityType { impl EntityType {

View File

@@ -1,5 +1,4 @@
//! Debug rendering system //! Debug rendering system
#[cfg_attr(coverage_nightly, feature(coverage_attribute))]
use crate::constants::{self, BOARD_PIXEL_OFFSET}; use crate::constants::{self, BOARD_PIXEL_OFFSET};
use crate::map::builder::Map; use crate::map::builder::Map;
use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings}; use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings};

View File

@@ -59,17 +59,6 @@ impl Ghost {
Ghost::Clyde => 0.85, 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)] #[derive(Component, Debug, Clone, Copy)]

View File

@@ -5,69 +5,71 @@ use bevy_ecs::{
query::With, query::With,
system::{Commands, NonSendMut, Query, Res, ResMut, Single}, system::{Commands, NonSendMut, Query, Res, ResMut, Single},
}; };
use strum_macros::IntoStaticStr;
use tracing::{debug, trace}; use tracing::{debug, trace};
use crate::{ use crate::{
constants::collider::FRUIT_SIZE, constants,
map::builder::Map, map::builder::Map,
systems::{common::bundles::ItemBundle, Collider, Position, Renderable}, systems::{common::bundles::ItemBundle, Collider, Position, Renderable, TimeToLive},
texture::{sprite::SpriteAtlas, sprites::GameSprite}, texture::{
sprite::SpriteAtlas,
sprites::{EffectSprite, GameSprite},
},
}; };
use crate::{ use crate::{
constants::animation::FRIGHTENED_FLASH_START_TICKS, constants::animation::FRIGHTENED_FLASH_START_TICKS,
events::GameEvent, events::GameEvent,
systems::common::components::EntityType, systems::common::components::EntityType,
systems::lifetime::TimeToLive, systems::{AudioEvent, GhostCollider, GhostState, ItemCollider, PacmanCollider, ScoreResource},
systems::{AudioEvent, GhostCollider, GhostState, ItemCollider, LinearAnimation, PacmanCollider, ScoreResource},
texture::animated::TileSequence,
}; };
/// Tracks the number of pellets consumed by the player for fruit spawning mechanics. /// Tracks the number of pellets consumed by the player for fruit spawning mechanics.
#[derive(bevy_ecs::resource::Resource, Debug, Default)] #[derive(bevy_ecs::resource::Resource, Debug, Default)]
pub struct PelletCount(pub u32); pub struct PelletCount(pub u32);
/// Maps fruit score values to bonus sprite indices for displaying bonus points /// Represents the different fruit sprites that can appear as bonus items.
fn fruit_score_to_sprite_index(score: u32) -> u8 { #[derive(IntoStaticStr, Debug, Clone, Copy, PartialEq, Eq, Hash)]
match score { #[strum(serialize_all = "snake_case")]
100 => 0, // Cherry pub enum FruitType {
300 => 2, // Strawberry Cherry,
500 => 3, // Orange Strawberry,
700 => 4, // Apple Orange,
1000 => 6, // Melon Apple,
2000 => 8, // Galaxian Melon,
3000 => 9, // Bell Galaxian,
5000 => 10, // Key Bell,
_ => 0, // Default to 100 points sprite Key,
}
} }
/// Maps sprite index to the corresponding effect sprite path (same as in state.rs) impl FruitType {
fn sprite_index_to_path(index: u8) -> &'static str { /// Returns the score value for this fruit type.
match index { pub fn score_value(self) -> u32 {
0 => "effects/100.png", match self {
1 => "effects/200.png", FruitType::Cherry => 100,
2 => "effects/300.png", FruitType::Strawberry => 300,
3 => "effects/400.png", FruitType::Orange => 500,
4 => "effects/700.png", FruitType::Apple => 700,
5 => "effects/800.png", FruitType::Melon => 1000,
6 => "effects/1000.png", FruitType::Galaxian => 2000,
7 => "effects/1600.png", FruitType::Bell => 3000,
8 => "effects/2000.png", FruitType::Key => 5000,
9 => "effects/3000.png", }
10 => "effects/5000.png",
_ => "effects/100.png", // fallback to index 0
} }
}
/// Determines if a collision between two entity types should be handled by the item system. pub fn from_index(index: u8) -> Self {
/// match index {
/// Returns `true` if one entity is a player and the other is a collectible item. 0 => FruitType::Cherry,
#[allow(dead_code)] 1 => FruitType::Strawberry,
pub fn is_valid_item_collision(entity1: EntityType, entity2: EntityType) -> bool { 2 => FruitType::Orange,
match (entity1, entity2) { 3 => FruitType::Apple,
(EntityType::Player, entity) | (entity, EntityType::Player) => entity.is_collectible(), 4 => FruitType::Melon,
_ => false, 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<ItemCollider>>, item_query: Query<(Entity, &EntityType, &Position), With<ItemCollider>>,
mut ghost_query: Query<&mut GhostState, With<GhostCollider>>, mut ghost_query: Query<&mut GhostState, With<GhostCollider>>,
mut events: EventWriter<AudioEvent>, mut events: EventWriter<AudioEvent>,
atlas: NonSendMut<SpriteAtlas>,
) { ) {
for event in collision_events.read() { for event in collision_events.read() {
if let GameEvent::Collision(entity1, entity2) = event { if let GameEvent::Collision(entity1, entity2) = event {
@@ -95,37 +96,11 @@ pub fn item_system(
}; };
// Get the item type and update score // 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() { 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"); 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; 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 // Remove the collected item
commands.entity(item_ent).despawn(); commands.entity(item_ent).despawn();
@@ -135,12 +110,21 @@ pub fn item_system(
trace!(pellet_count = pellet_count.0, "Pellet consumed"); trace!(pellet_count = pellet_count.0, "Pellet consumed");
// Check if we should spawn a fruit // 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"); 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 // Trigger audio if appropriate
if entity_type.is_collectible() { if entity_type.is_collectible() {
events.write(AudioEvent::PlayEat); events.write(AudioEvent::PlayEat);
@@ -169,30 +153,57 @@ pub fn item_system(
} }
/// Trigger to spawn a fruit /// Trigger to spawn a fruit
#[derive(Event, Clone, Copy, Debug, PartialEq, Eq)] #[derive(Event, Clone, Copy, Debug)]
pub struct SpawnFruitTrigger; pub enum SpawnTrigger {
Fruit,
Bonus { position: Position, value: u32, ttl: u32 },
}
pub fn spawn_fruit_observer( pub fn spawn_fruit_observer(
_: Trigger<SpawnFruitTrigger>, trigger: Trigger<SpawnTrigger>,
mut commands: Commands, mut commands: Commands,
atlas: NonSendMut<SpriteAtlas>, atlas: NonSendMut<SpriteAtlas>,
map: Res<Map>, map: Res<Map>,
) { ) {
// Use cherry sprite as the default fruit (first fruit in original Pac-Man) let entity = match *trigger {
let fruit_sprite = &atlas SpawnTrigger::Fruit => {
.get_tile(&GameSprite::Fruit(crate::texture::sprites::FruitSprite::Cherry).to_path()) // Use cherry sprite as the default fruit (first fruit in original Pac-Man)
.unwrap(); 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 { commands.spawn(bundle)
position: map.start_positions.fruit_spawn, }
sprite: Renderable { SpawnTrigger::Bonus { position, value, ttl } => {
sprite: *fruit_sprite, let sprite = &atlas
layer: 1, .get_tile(&GameSprite::Effect(EffectSprite::Bonus(value)).to_path())
}, .unwrap();
entity_type: EntityType::Fruit(crate::texture::sprites::FruitSprite::Cherry),
collider: Collider { size: FRUIT_SIZE },
item_collider: ItemCollider,
});
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");
} }

View File

@@ -1,5 +1,6 @@
//! This module contains all the systems in the game. //! This module contains all the systems in the game.
// These modules are excluded from coverage.
#[cfg_attr(coverage_nightly, coverage(off))] #[cfg_attr(coverage_nightly, coverage(off))]
pub mod audio; pub mod audio;
#[cfg_attr(coverage_nightly, coverage(off))] #[cfg_attr(coverage_nightly, coverage(off))]

View File

@@ -2,20 +2,20 @@ use std::mem::discriminant;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::events::StageTransition; use crate::events::StageTransition;
use crate::systems::SpawnTrigger;
use crate::{ use crate::{
map::builder::Map, map::builder::Map,
systems::{ systems::{
AudioEvent, Blinking, DirectionalAnimation, Dying, Eaten, Frozen, Ghost, GhostCollider, GhostState, Hidden, 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::{ use bevy_ecs::{
entity::Entity, entity::Entity,
event::{EventReader, EventWriter}, event::{EventReader, EventWriter},
query::{With, Without}, query::{With, Without},
resource::Resource, resource::Resource,
system::{Commands, NonSendMut, Query, Res, ResMut, Single}, system::{Commands, Query, Res, ResMut, Single},
}; };
#[derive(Resource, Clone)] #[derive(Resource, Clone)]
@@ -92,24 +92,6 @@ impl Default for PlayerLives {
} }
/// Handles startup sequence transitions and component management /// 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::too_many_arguments)]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub fn stage_system( pub fn stage_system(
@@ -124,7 +106,6 @@ pub fn stage_system(
mut blinking_query: Query<Entity, With<Blinking>>, mut blinking_query: Query<Entity, With<Blinking>>,
player: Single<(Entity, &mut Position), With<PlayerControlled>>, player: Single<(Entity, &mut Position), With<PlayerControlled>>,
mut ghost_query: Query<(Entity, &Ghost, &mut Position), (With<GhostCollider>, Without<PlayerControlled>)>, mut ghost_query: Query<(Entity, &Ghost, &mut Position), (With<GhostCollider>, Without<PlayerControlled>)>,
atlas: NonSendMut<SpriteAtlas>,
) { ) {
let old_state = *game_state; let old_state = *game_state;
let mut new_state: Option<GameStage> = None; let mut new_state: Option<GameStage> = None;
@@ -246,23 +227,12 @@ pub fn stage_system(
commands.entity(ghost_entity).insert(Hidden); commands.entity(ghost_entity).insert(Hidden);
// Spawn bonus points entity at Pac-Man's position // Spawn bonus points entity at Pac-Man's position
let sprite_index = 1; // Index 1 = 200 points (default for ghost eating) commands.trigger(SpawnTrigger::Bonus {
let sprite_path = sprite_index_to_path(sprite_index); position: Position::Stopped { node },
// TODO: Doubling score value for each consecutive ghost eaten
if let Ok(sprite_tile) = SpriteAtlas::get_tile(&atlas, sprite_path) { value: 200,
let tile_sequence = TileSequence::single(sprite_tile); ttl: 30,
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),
));
}
} }
(GameStage::GhostEatenPause { ghost_entity, .. }, GameStage::Playing) => { (GameStage::GhostEatenPause { ghost_entity, .. }, GameStage::Playing) => {
// Unfreeze and reveal the player & all ghosts // Unfreeze and reveal the player & all ghosts

View File

@@ -14,11 +14,6 @@ impl TileSequence {
Self { tiles: tiles.to_vec() } 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 /// Returns the tile at the given frame index, wrapping if necessary
pub fn get_tile(&self, frame: usize) -> AtlasTile { pub fn get_tile(&self, frame: usize) -> AtlasTile {
if self.tiles.is_empty() { if self.tiles.is_empty() {

View File

@@ -58,19 +58,6 @@ impl AtlasTile {
canvas.copy(&atlas.texture, src, dest).map_err(TextureError::RenderFailed)?; canvas.copy(&atlas.texture, src, dest).map_err(TextureError::RenderFailed)?;
Ok(()) Ok(())
} }
/// Creates a new atlas tile.
#[allow(dead_code)]
pub fn new(pos: U16Vec2, size: U16Vec2, color: Option<Color>) -> 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. /// High-performance sprite atlas providing fast texture region lookups and rendering.
@@ -120,32 +107,4 @@ impl SpriteAtlas {
color: self.default_color, 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<Color> {
self.default_color
}
} }

View File

@@ -5,7 +5,10 @@
//! The `GameSprite` enum is the main entry point, and its `to_path` method //! 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. //! 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. /// Represents the different sprites for Pac-Man.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -47,34 +50,10 @@ pub enum MazeSprite {
Energizer, 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(dead_code)] pub enum EffectSprite {
pub enum FruitSprite { Bonus(u32),
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,
}
}
} }
/// A top-level enum that encompasses all game sprites. /// A top-level enum that encompasses all game sprites.
@@ -83,7 +62,8 @@ pub enum GameSprite {
Pacman(PacmanSprite), Pacman(PacmanSprite),
Ghost(GhostSprite), Ghost(GhostSprite),
Maze(MazeSprite), Maze(MazeSprite),
Fruit(FruitSprite), Fruit(FruitType),
Effect(EffectSprite),
} }
impl GameSprite { impl GameSprite {
@@ -138,14 +118,16 @@ impl GameSprite {
GameSprite::Maze(MazeSprite::Energizer) => "maze/energizer.png".to_string(), GameSprite::Maze(MazeSprite::Energizer) => "maze/energizer.png".to_string(),
// Fruit sprites // Fruit sprites
GameSprite::Fruit(FruitSprite::Cherry) => "edible/cherry.png".to_string(), GameSprite::Fruit(fruit) => format!("edible/{}.png", Into::<&'static str>::into(fruit)),
GameSprite::Fruit(FruitSprite::Strawberry) => "edible/strawberry.png".to_string(),
GameSprite::Fruit(FruitSprite::Orange) => "edible/orange.png".to_string(), // Effect sprites
GameSprite::Fruit(FruitSprite::Apple) => "edible/apple.png".to_string(), GameSprite::Effect(EffectSprite::Bonus(value)) => match value {
GameSprite::Fruit(FruitSprite::Melon) => "edible/melon.png".to_string(), 100 | 200 | 300 | 400 | 700 | 800 | 1000 | 2000 | 3000 | 5000 => format!("effects/{}.png", value),
GameSprite::Fruit(FruitSprite::Galaxian) => "edible/galaxian.png".to_string(), _ => {
GameSprite::Fruit(FruitSprite::Bell) => "edible/bell.png".to_string(), tracing::warn!("Invalid bonus value: {}", value);
GameSprite::Fruit(FruitSprite::Key) => "edible/key.png".to_string(), "effects/100.png".to_string()
}
},
} }
} }
} }

View File

@@ -1,5 +1,3 @@
#![allow(dead_code)]
//! This module provides text rendering using the texture atlas. //! This module provides text rendering using the texture atlas.
//! //!
//! The TextTexture system renders text from the atlas using character mapping. //! 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<char, AtlasTile> { pub fn get_char_map(&self) -> &HashMap<char, AtlasTile> {
&self.char_map &self.char_map
} }
@@ -167,26 +166,6 @@ impl TextTexture {
Ok(()) 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<Color> {
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. /// Calculates the width of a string in pixels at the current scale.
pub fn text_width(&self, text: &str) -> u32 { pub fn text_width(&self, text: &str) -> u32 {
let char_width = (8.0 * self.scale) as u32; let char_width = (8.0 * self.scale) as u32;

View File

@@ -14,7 +14,8 @@ use pacman::{
}, },
systems::{ systems::{
AudioEvent, AudioState, BufferedDirection, Collider, DebugState, DeltaTime, EntityType, Ghost, GhostCollider, GhostState, 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}, texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas},
}; };
@@ -85,6 +86,7 @@ pub fn create_test_world() -> World {
world.insert_resource(AudioState::default()); world.insert_resource(AudioState::default());
world.insert_resource(GlobalState { exit: false }); world.insert_resource(GlobalState { exit: false });
world.insert_resource(DebugState::default()); world.insert_resource(DebugState::default());
world.insert_resource(PelletCount(0));
world.insert_resource(DeltaTime { world.insert_resource(DeltaTime {
seconds: 1.0 / 60.0, seconds: 1.0 / 60.0,
ticks: 1, ticks: 1,

View File

@@ -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<i32, io::Error> = Err(io::Error::new(io::ErrorKind::Other, "test error"));
let game_result: GameResult<i32> = 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<i32, io::Error> = Ok(42);
let game_result: GameResult<i32> = result.into_game_error();
assert_that(&game_result.unwrap()).is_equal_to(42);
}
#[test]
fn test_option_ext_some() {
let option: Option<i32> = Some(42);
let result: GameResult<i32> = 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<i32> = None;
let result: GameResult<i32> = 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<i32, io::Error> = Ok(42);
let game_result: GameResult<i32> = 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<i32, io::Error> = Err(io::Error::new(io::ErrorKind::Other, "original error"));
let game_result: GameResult<i32> = 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");
}
}

View File

@@ -1,5 +1,5 @@
use bevy_ecs::{entity::Entity, system::RunSystemOnce}; 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::*; use speculoos::prelude::*;
mod common; mod common;
@@ -24,21 +24,6 @@ fn test_is_collectible_item() {
assert_that(&EntityType::Ghost.is_collectible()).is_false(); 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] #[test]
fn test_item_system_pellet_collection() { fn test_item_system_pellet_collection() {
let mut world = common::create_test_world(); let mut world = common::create_test_world();

View File

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

View File

@@ -81,44 +81,20 @@ fn test_text_scale() -> Result<(), String> {
let string = "ABCDEFG !-/\""; let string = "ABCDEFG !-/\"";
let base_width = (string.len() * 8) as u32; let base_width = (string.len() * 8) as u32;
let mut text_texture = TextTexture::new(0.5); let text_texture = TextTexture::new(0.5);
assert_that(&text_texture.scale()).is_equal_to(0.5);
assert_that(&text_texture.text_height()).is_equal_to(4); 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("")).is_equal_to(0);
assert_that(&text_texture.text_width(string)).is_equal_to(base_width / 2); assert_that(&text_texture.text_width(string)).is_equal_to(base_width / 2);
text_texture.set_scale(2.0); let text_texture = TextTexture::new(2.0);
assert_that(&text_texture.scale()).is_equal_to(2.0);
assert_that(&text_texture.text_height()).is_equal_to(16); 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(string)).is_equal_to(base_width * 2);
assert_that(&text_texture.text_width("")).is_equal_to(0); assert_that(&text_texture.text_width("")).is_equal_to(0);
text_texture.set_scale(1.0); let text_texture = TextTexture::new(1.0);
assert_that(&text_texture.scale()).is_equal_to(1.0);
assert_that(&text_texture.text_height()).is_equal_to(8); 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(string)).is_equal_to(base_width);
assert_that(&text_texture.text_width("")).is_equal_to(0); assert_that(&text_texture.text_width("")).is_equal_to(0);
Ok(()) 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(())
}