mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 01:15:42 -06:00
refactor: remove dead code, tune lints, remove useless tests
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
56
src/error.rs
56
src/error.rs
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()))?;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
|
||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user