mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-08 06:07:46 -06:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fff44faa05 | ||
|
|
ca17984d98 | ||
|
|
c8f389b163 | ||
|
|
9c274de901 | ||
|
|
9633611ae8 | ||
|
|
897b9b8621 | ||
|
|
ee2569b70c | ||
|
|
84caa6c25f | ||
|
|
f92c9175b9 |
@@ -3,3 +3,10 @@ fail-fast = false
|
|||||||
|
|
||||||
[profile.coverage]
|
[profile.coverage]
|
||||||
status-level = "none"
|
status-level = "none"
|
||||||
|
|
||||||
|
[[profile.default.overrides]]
|
||||||
|
filter = 'test(pacman::game::)'
|
||||||
|
test-group = 'serial'
|
||||||
|
|
||||||
|
[test-groups]
|
||||||
|
serial = { max-threads = 1 }
|
||||||
|
|||||||
42
.github/workflows/coverage.yaml
vendored
42
.github/workflows/coverage.yaml
vendored
@@ -4,13 +4,11 @@ on: ["push", "pull_request"]
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_TOOLCHAIN: 1.86.0
|
RUST_TOOLCHAIN: nightly
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
coverage:
|
coverage:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
|
||||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
@@ -48,35 +46,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate coverage report
|
- name: Generate coverage report
|
||||||
run: |
|
run: |
|
||||||
just coverage
|
just coverage-lcov
|
||||||
|
|
||||||
- name: Download Coveralls CLI
|
- name: Coveralls upload
|
||||||
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
|
uses: coverallsapp/github-action@v2
|
||||||
run: |
|
with:
|
||||||
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
|
github-token: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||||
curl -L https://github.com/coverallsapp/coverage-reporter/releases/download/v0.6.15/coveralls-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin
|
path-to-lcov: lcov.info
|
||||||
|
debug: true
|
||||||
- name: Upload coverage to Coveralls
|
|
||||||
if: ${{ env.COVERALLS_REPO_TOKEN != '' }}
|
|
||||||
run: |
|
|
||||||
if [ ! -f "lcov.info" ]; then
|
|
||||||
echo "Error: lcov.info file not found. Coverage generation may have failed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for i in {1..10}; do
|
|
||||||
echo "Attempt $i: Uploading coverage to Coveralls..."
|
|
||||||
if coveralls -n report lcov.info; then
|
|
||||||
echo "Successfully uploaded coverage report."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ $i -lt 10 ]; then
|
|
||||||
delay=$((2**i))
|
|
||||||
echo "Attempt $i failed. Retrying in $delay seconds..."
|
|
||||||
sleep $delay
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Failed to upload coverage report after 10 attempts."
|
|
||||||
exit 1
|
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,8 +14,13 @@ assets/site/build.css
|
|||||||
|
|
||||||
# Coverage reports
|
# Coverage reports
|
||||||
lcov.info
|
lcov.info
|
||||||
|
codecov.json
|
||||||
coverage.html
|
coverage.html
|
||||||
|
|
||||||
# Profiling output
|
# Profiling output
|
||||||
flamegraph.svg
|
flamegraph.svg
|
||||||
/profile.*
|
/profile.*
|
||||||
|
|
||||||
|
# temporary
|
||||||
|
assets/game/sound/*.wav
|
||||||
|
/*.py
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -663,7 +663,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pacman"
|
name = "pacman"
|
||||||
version = "0.2.0"
|
version = "0.76.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pacman"
|
name = "pacman"
|
||||||
version = "0.2.0"
|
version = "0.76.1"
|
||||||
authors = ["Xevion"]
|
authors = ["Xevion"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86.0"
|
rust-version = "1.86.0"
|
||||||
@@ -98,3 +98,6 @@ x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" }
|
|||||||
x86_64-unknown-linux-gnu = { triplet = "x64-linux" }
|
x86_64-unknown-linux-gnu = { triplet = "x64-linux" }
|
||||||
x86_64-apple-darwin = { triplet = "x64-osx" }
|
x86_64-apple-darwin = { triplet = "x64-osx" }
|
||||||
aarch64-apple-darwin = { triplet = "arm64-osx" }
|
aarch64-apple-darwin = { triplet = "arm64-osx" }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] }
|
||||||
|
|||||||
14
Justfile
14
Justfile
@@ -1,9 +1,6 @@
|
|||||||
set shell := ["bash", "-c"]
|
set shell := ["bash", "-c"]
|
||||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||||
|
|
||||||
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
|
|
||||||
# You can use src\\\\..., but the filename alone is acceptable too
|
|
||||||
coverage_exclude_pattern := "src\\\\app\\.rs|audio\\.rs|src\\\\error\\.rs|platform\\\\emscripten\\.rs|bin\\\\.+\\.rs|main\\.rs|platform\\\\desktop\\.rs|platform\\\\tracing_buffer\\.rs|platform\\\\buffered_writer\\.rs|systems\\\\debug\\.rs|systems\\\\profiling\\.rs"
|
|
||||||
|
|
||||||
binary_extension := if os() == "windows" { ".exe" } else { "" }
|
binary_extension := if os() == "windows" { ".exe" } else { "" }
|
||||||
|
|
||||||
@@ -14,22 +11,19 @@ binary_extension := if os() == "windows" { ".exe" } else { "" }
|
|||||||
html: coverage
|
html: coverage
|
||||||
cargo llvm-cov report \
|
cargo llvm-cov report \
|
||||||
--remap-path-prefix \
|
--remap-path-prefix \
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
|
||||||
--html \
|
--html \
|
||||||
--open
|
--open
|
||||||
|
|
||||||
# Display report (for humans)
|
# Display report (for humans)
|
||||||
report-coverage: coverage
|
report-coverage: coverage
|
||||||
cargo llvm-cov report \
|
cargo llvm-cov report --remap-path-prefix
|
||||||
--remap-path-prefix \
|
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
|
|
||||||
|
|
||||||
# Run & generate report (for CI)
|
# Run & generate LCOV report (as base report)
|
||||||
coverage:
|
coverage:
|
||||||
cargo llvm-cov \
|
cargo +nightly llvm-cov \
|
||||||
--lcov \
|
--lcov \
|
||||||
--remap-path-prefix \
|
--remap-path-prefix \
|
||||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
--workspace \
|
||||||
--output-path lcov.info \
|
--output-path lcov.info \
|
||||||
--profile coverage \
|
--profile coverage \
|
||||||
--no-fail-fast nextest
|
--no-fail-fast nextest
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# Pac-Man
|
# Pac-Man
|
||||||
|
|
||||||
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
||||||
|
|
||||||
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
||||||
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
||||||
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
|
[badge-coverage]: https://codecov.io/github/Xevion/Pac-Man/branch/master/graph/badge.svg?token=R2RBYUQK3I
|
||||||
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
||||||
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
||||||
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
||||||
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
||||||
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
|
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
|
||||||
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
|
[coverage]: https://codecov.io/github/Xevion/Pac-Man
|
||||||
[demo]: https://xevion.github.io/Pac-Man/
|
[demo]: https://xevion.github.io/Pac-Man/
|
||||||
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
||||||
|
|
||||||
|
|||||||
BIN
assets/game/sound/pacman_death.wav
Normal file
BIN
assets/game/sound/pacman_death.wav
Normal file
Binary file not shown.
3
codecov.yml
Normal file
3
codecov.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ignore:
|
||||||
|
- "src/(?:bin|platform))/.+\\.rs"
|
||||||
|
- "src/(?:app|events|formatter)\\.rs"
|
||||||
@@ -19,6 +19,8 @@ pub enum Asset {
|
|||||||
AtlasImage,
|
AtlasImage,
|
||||||
/// Terminal Vector font for text rendering (TerminalVector.ttf)
|
/// Terminal Vector font for text rendering (TerminalVector.ttf)
|
||||||
Font,
|
Font,
|
||||||
|
/// Sound effect for Pac-Man's death
|
||||||
|
DeathSound,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Asset {
|
impl Asset {
|
||||||
@@ -37,6 +39,7 @@ impl Asset {
|
|||||||
Wav4 => "sound/waka/4.ogg",
|
Wav4 => "sound/waka/4.ogg",
|
||||||
AtlasImage => "atlas.png",
|
AtlasImage => "atlas.png",
|
||||||
Font => "TerminalVector.ttf",
|
Font => "TerminalVector.ttf",
|
||||||
|
DeathSound => "sound/pacman_death.wav",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/audio.rs
45
src/audio.rs
@@ -16,6 +16,7 @@ const SOUND_ASSETS: [Asset; 4] = [Asset::Wav1, Asset::Wav2, Asset::Wav3, Asset::
|
|||||||
pub struct Audio {
|
pub struct Audio {
|
||||||
_mixer_context: Option<mixer::Sdl2MixerContext>,
|
_mixer_context: Option<mixer::Sdl2MixerContext>,
|
||||||
sounds: Vec<Chunk>,
|
sounds: Vec<Chunk>,
|
||||||
|
death_sound: Option<Chunk>,
|
||||||
next_sound_index: usize,
|
next_sound_index: usize,
|
||||||
muted: bool,
|
muted: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
@@ -44,6 +45,7 @@ impl Audio {
|
|||||||
return Self {
|
return Self {
|
||||||
_mixer_context: None,
|
_mixer_context: None,
|
||||||
sounds: Vec::new(),
|
sounds: Vec::new(),
|
||||||
|
death_sound: None,
|
||||||
next_sound_index: 0,
|
next_sound_index: 0,
|
||||||
muted: false,
|
muted: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
@@ -65,6 +67,7 @@ impl Audio {
|
|||||||
return Self {
|
return Self {
|
||||||
_mixer_context: None,
|
_mixer_context: None,
|
||||||
sounds: Vec::new(),
|
sounds: Vec::new(),
|
||||||
|
death_sound: None,
|
||||||
next_sound_index: 0,
|
next_sound_index: 0,
|
||||||
muted: false,
|
muted: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
@@ -93,12 +96,33 @@ impl Audio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let death_sound = match get_asset_bytes(Asset::DeathSound) {
|
||||||
|
Ok(data) => match RWops::from_bytes(&data) {
|
||||||
|
Ok(rwops) => match rwops.load_wav() {
|
||||||
|
Ok(chunk) => Some(chunk),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to load death sound from asset API: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to create RWops for death sound: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to load death sound asset: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// If no sounds loaded successfully, disable audio
|
// If no sounds loaded successfully, disable audio
|
||||||
if sounds.is_empty() {
|
if sounds.is_empty() && death_sound.is_none() {
|
||||||
tracing::warn!("No sounds loaded successfully. Audio will be disabled.");
|
tracing::warn!("No sounds loaded successfully. Audio will be disabled.");
|
||||||
return Self {
|
return Self {
|
||||||
_mixer_context: Some(mixer_context),
|
_mixer_context: Some(mixer_context),
|
||||||
sounds: Vec::new(),
|
sounds: Vec::new(),
|
||||||
|
death_sound: None,
|
||||||
next_sound_index: 0,
|
next_sound_index: 0,
|
||||||
muted: false,
|
muted: false,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
@@ -108,6 +132,7 @@ impl Audio {
|
|||||||
Audio {
|
Audio {
|
||||||
_mixer_context: Some(mixer_context),
|
_mixer_context: Some(mixer_context),
|
||||||
sounds,
|
sounds,
|
||||||
|
death_sound,
|
||||||
next_sound_index: 0,
|
next_sound_index: 0,
|
||||||
muted: false,
|
muted: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@@ -138,6 +163,24 @@ impl Audio {
|
|||||||
self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len();
|
self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Plays the death sound effect.
|
||||||
|
pub fn death(&mut self) {
|
||||||
|
if self.disabled || self.muted {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(chunk) = &self.death_sound {
|
||||||
|
mixer::Channel::all().play(chunk, 0).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Halts all currently playing audio channels.
|
||||||
|
pub fn stop_all(&mut self) {
|
||||||
|
if !self.disabled {
|
||||||
|
mixer::Channel::all().halt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Instantly mutes or unmutes all audio channels by adjusting their volume.
|
/// Instantly mutes or unmutes all audio channels by adjusting their volume.
|
||||||
///
|
///
|
||||||
/// Sets all 4 mixer channels to zero volume when muting, or restores them to
|
/// Sets all 4 mixer channels to zero volume when muting, or restores them to
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||||
|
#![cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use sdl2::event::Event;
|
use sdl2::event::Event;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||||
|
#![cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
|
||||||
use circular_buffer::CircularBuffer;
|
use circular_buffer::CircularBuffer;
|
||||||
use pacman::constants::CANVAS_SIZE;
|
use pacman::constants::CANVAS_SIZE;
|
||||||
use sdl2::event::Event;
|
use sdl2::event::Event;
|
||||||
|
|||||||
14
src/lib.rs
14
src/lib.rs
@@ -1,14 +1,22 @@
|
|||||||
//! Pac-Man game library crate.
|
//! Pac-Man game library crate.
|
||||||
|
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
|
||||||
|
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod asset;
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod constants;
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod formatter;
|
pub mod formatter;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
pub mod platform;
|
||||||
|
|
||||||
|
pub mod asset;
|
||||||
|
pub mod constants;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod platform;
|
|
||||||
pub mod systems;
|
pub mod systems;
|
||||||
pub mod texture;
|
pub mod texture;
|
||||||
|
|||||||
16
src/main.rs
16
src/main.rs
@@ -1,20 +1,27 @@
|
|||||||
// 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))]
|
||||||
|
|
||||||
use crate::{app::App, constants::LOOP_TIME};
|
use crate::{app::App, constants::LOOP_TIME};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
mod app;
|
mod app;
|
||||||
mod asset;
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
mod audio;
|
mod audio;
|
||||||
mod constants;
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
mod events;
|
mod events;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
mod formatter;
|
mod formatter;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
|
mod platform;
|
||||||
|
|
||||||
|
mod asset;
|
||||||
|
mod constants;
|
||||||
mod game;
|
mod game;
|
||||||
mod map;
|
mod map;
|
||||||
mod platform;
|
|
||||||
mod systems;
|
mod systems;
|
||||||
mod texture;
|
mod texture;
|
||||||
|
|
||||||
@@ -22,6 +29,7 @@ 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
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
|
|||||||
Asset::Wav4 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/4.ogg"))),
|
Asset::Wav4 => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/waka/4.ogg"))),
|
||||||
Asset::AtlasImage => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))),
|
Asset::AtlasImage => Ok(Cow::Borrowed(include_bytes!("../../assets/game/atlas.png"))),
|
||||||
Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))),
|
Asset::Font => Ok(Cow::Borrowed(include_bytes!("../../assets/game/TerminalVector.ttf"))),
|
||||||
|
Asset::DeathSound => Ok(Cow::Borrowed(include_bytes!("../../assets/game/sound/pacman_death.wav"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ pub struct AudioState {
|
|||||||
pub enum AudioEvent {
|
pub enum AudioEvent {
|
||||||
/// Play the "eat" sound when Pac-Man consumes a pellet
|
/// Play the "eat" sound when Pac-Man consumes a pellet
|
||||||
PlayEat,
|
PlayEat,
|
||||||
|
/// Play the death sound
|
||||||
|
PlayDeath,
|
||||||
|
/// Stop all currently playing sounds
|
||||||
|
StopAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-send resource wrapper for SDL2 audio system
|
/// Non-send resource wrapper for SDL2 audio system
|
||||||
@@ -59,6 +63,16 @@ pub fn audio_system(
|
|||||||
// 4 eat sounds available
|
// 4 eat sounds available
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AudioEvent::PlayDeath => {
|
||||||
|
if !audio.0.is_disabled() && !audio_state.muted {
|
||||||
|
audio.0.death();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AudioEvent::StopAll => {
|
||||||
|
if !audio.0.is_disabled() {
|
||||||
|
audio.0.stop_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//! Debug rendering system
|
//! Debug rendering system
|
||||||
use std::cmp::Ordering;
|
#[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};
|
||||||
@@ -13,6 +12,7 @@ use sdl2::rect::{Point, Rect};
|
|||||||
use sdl2::render::{Canvas, Texture};
|
use sdl2::render::{Canvas, Texture};
|
||||||
use sdl2::video::Window;
|
use sdl2::video::Window;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use std::cmp::Ordering;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
@@ -149,6 +149,7 @@ fn transform_position_with_offset(pos: Vec2, scale: f32) -> IVec2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Renders timing information in the top-left corner of the screen using the debug text atlas
|
/// Renders timing information in the top-left corner of the screen using the debug text atlas
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
fn render_timing_display(
|
fn render_timing_display(
|
||||||
canvas: &mut Canvas<Window>,
|
canvas: &mut Canvas<Window>,
|
||||||
timings: &SystemTimings,
|
timings: &SystemTimings,
|
||||||
@@ -203,6 +204,7 @@ fn render_timing_display(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub fn debug_render_system(
|
pub fn debug_render_system(
|
||||||
canvas: &mut Canvas<Window>,
|
canvas: &mut Canvas<Window>,
|
||||||
ttf_atlas: &mut TtfAtlasResource,
|
ttf_atlas: &mut TtfAtlasResource,
|
||||||
|
|||||||
@@ -3,17 +3,21 @@
|
|||||||
//! This module contains all the ECS-related logic, including components, systems,
|
//! This module contains all the ECS-related logic, including components, systems,
|
||||||
//! and resources.
|
//! and resources.
|
||||||
|
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod blinking;
|
pub mod blinking;
|
||||||
pub mod collision;
|
pub mod collision;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod ghost;
|
pub mod ghost;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod profiling;
|
pub mod profiling;
|
||||||
|
#[cfg_attr(coverage_nightly, coverage(off))]
|
||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod stage;
|
pub mod stage;
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ pub enum PacmanSprite {
|
|||||||
Moving(Direction, u8),
|
Moving(Direction, u8),
|
||||||
/// The full, closed-mouth Pac-Man sprite.
|
/// The full, closed-mouth Pac-Man sprite.
|
||||||
Full,
|
Full,
|
||||||
|
/// A single frame of the dying animation.
|
||||||
|
Dying(u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the color of a frightened ghost.
|
/// Represents the color of a frightened ghost.
|
||||||
@@ -60,45 +62,50 @@ impl GameSprite {
|
|||||||
/// This path corresponds to the filename in the texture atlas JSON file.
|
/// This path corresponds to the filename in the texture atlas JSON file.
|
||||||
pub fn to_path(self) -> String {
|
pub fn to_path(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
GameSprite::Pacman(sprite) => match sprite {
|
GameSprite::Pacman(PacmanSprite::Moving(dir, frame)) => format!(
|
||||||
PacmanSprite::Moving(dir, frame) => {
|
"pacman/{}_{}.png",
|
||||||
let frame_char = match frame {
|
dir.as_ref(),
|
||||||
0 => 'a',
|
match frame {
|
||||||
1 => 'b',
|
0 => "a",
|
||||||
_ => panic!("Invalid animation frame"),
|
1 => "b",
|
||||||
};
|
_ => panic!("Invalid animation frame"),
|
||||||
format!("pacman/{}_{}.png", dir.as_ref().to_lowercase(), frame_char)
|
|
||||||
}
|
}
|
||||||
PacmanSprite::Full => "pacman/full.png".to_string(),
|
),
|
||||||
},
|
GameSprite::Pacman(PacmanSprite::Full) => "pacman/full.png".to_string(),
|
||||||
GameSprite::Ghost(sprite) => match sprite {
|
GameSprite::Pacman(PacmanSprite::Dying(frame)) => format!("pacman/death/{}.png", frame),
|
||||||
GhostSprite::Normal(ghost, dir, frame) => {
|
|
||||||
let frame_char = match frame {
|
// Ghost sprites
|
||||||
0 => 'a',
|
GameSprite::Ghost(GhostSprite::Normal(ghost_type, dir, frame)) => {
|
||||||
1 => 'b',
|
let frame_char = match frame {
|
||||||
_ => panic!("Invalid animation frame"),
|
0 => 'a',
|
||||||
};
|
1 => 'b',
|
||||||
format!("ghost/{}/{}_{}.png", ghost.as_str(), dir.as_ref().to_lowercase(), frame_char)
|
_ => panic!("Invalid animation frame"),
|
||||||
}
|
};
|
||||||
GhostSprite::Frightened(color, frame) => {
|
format!(
|
||||||
let frame_char = match frame {
|
"ghost/{}/{}_{}.png",
|
||||||
0 => 'a',
|
ghost_type.as_str(),
|
||||||
1 => 'b',
|
dir.as_ref().to_lowercase(),
|
||||||
_ => panic!("Invalid animation frame"),
|
frame_char
|
||||||
};
|
)
|
||||||
let color_str = match color {
|
}
|
||||||
FrightenedColor::Blue => "blue",
|
GameSprite::Ghost(GhostSprite::Frightened(color, frame)) => {
|
||||||
FrightenedColor::White => "white",
|
let frame_char = match frame {
|
||||||
};
|
0 => 'a',
|
||||||
format!("ghost/frightened/{}_{}.png", color_str, frame_char)
|
1 => 'b',
|
||||||
}
|
_ => panic!("Invalid animation frame"),
|
||||||
GhostSprite::Eyes(dir) => format!("ghost/eyes/{}.png", dir.as_ref().to_lowercase()),
|
};
|
||||||
},
|
let color_str = match color {
|
||||||
GameSprite::Maze(sprite) => match sprite {
|
FrightenedColor::Blue => "blue",
|
||||||
MazeSprite::Tile(index) => format!("maze/tiles/{}.png", index),
|
FrightenedColor::White => "white",
|
||||||
MazeSprite::Pellet => "maze/pellet.png".to_string(),
|
};
|
||||||
MazeSprite::Energizer => "maze/energizer.png".to_string(),
|
format!("ghost/frightened/{}_{}.png", color_str, frame_char)
|
||||||
},
|
}
|
||||||
|
GameSprite::Ghost(GhostSprite::Eyes(dir)) => format!("ghost/eyes/{}.png", dir.as_ref().to_lowercase()),
|
||||||
|
|
||||||
|
// Maze sprites
|
||||||
|
GameSprite::Maze(MazeSprite::Tile(index)) => format!("maze/tiles/{}.png", index),
|
||||||
|
GameSprite::Maze(MazeSprite::Pellet) => "maze/pellet.png".to_string(),
|
||||||
|
GameSprite::Maze(MazeSprite::Energizer) => "maze/energizer.png".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
tests/sprites.rs
Normal file
73
tests/sprites.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
//! Tests for the sprite path generation.
|
||||||
|
use pacman::{
|
||||||
|
game::ATLAS_FRAMES,
|
||||||
|
map::direction::Direction,
|
||||||
|
systems::components::Ghost,
|
||||||
|
texture::sprites::{FrightenedColor, GameSprite, GhostSprite, MazeSprite, PacmanSprite},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_all_sprite_paths_exist() {
|
||||||
|
let mut sprites_to_test = Vec::new();
|
||||||
|
|
||||||
|
// Pac-Man sprites
|
||||||
|
for &dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
|
||||||
|
for frame in 0..2 {
|
||||||
|
sprites_to_test.push(GameSprite::Pacman(PacmanSprite::Moving(dir, frame)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sprites_to_test.push(GameSprite::Pacman(PacmanSprite::Full));
|
||||||
|
for frame in 0..=10 {
|
||||||
|
sprites_to_test.push(GameSprite::Pacman(PacmanSprite::Dying(frame)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ghost sprites
|
||||||
|
for &ghost in &[Ghost::Blinky, Ghost::Pinky, Ghost::Inky, Ghost::Clyde] {
|
||||||
|
for &dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
|
||||||
|
for frame in 0..2 {
|
||||||
|
sprites_to_test.push(GameSprite::Ghost(GhostSprite::Normal(ghost, dir, frame)));
|
||||||
|
}
|
||||||
|
sprites_to_test.push(GameSprite::Ghost(GhostSprite::Eyes(dir)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for &color in &[FrightenedColor::Blue, FrightenedColor::White] {
|
||||||
|
for frame in 0..2 {
|
||||||
|
sprites_to_test.push(GameSprite::Ghost(GhostSprite::Frightened(color, frame)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maze sprites
|
||||||
|
for i in 0..=34 {
|
||||||
|
sprites_to_test.push(GameSprite::Maze(MazeSprite::Tile(i)));
|
||||||
|
}
|
||||||
|
sprites_to_test.push(GameSprite::Maze(MazeSprite::Pellet));
|
||||||
|
sprites_to_test.push(GameSprite::Maze(MazeSprite::Energizer));
|
||||||
|
|
||||||
|
for sprite in sprites_to_test {
|
||||||
|
let path = sprite.to_path();
|
||||||
|
assert!(
|
||||||
|
ATLAS_FRAMES.contains_key(&path),
|
||||||
|
"Sprite path '{}' does not exist in the atlas.",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_sprite_paths_do_not_exist() {
|
||||||
|
let invalid_sprites = vec![
|
||||||
|
// An invalid Pac-Man dying frame
|
||||||
|
GameSprite::Pacman(PacmanSprite::Dying(99)),
|
||||||
|
// An invalid maze tile
|
||||||
|
GameSprite::Maze(MazeSprite::Tile(99)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for sprite in invalid_sprites {
|
||||||
|
let path = sprite.to_path();
|
||||||
|
assert!(
|
||||||
|
!ATLAS_FRAMES.contains_key(&path),
|
||||||
|
"Invalid sprite path '{}' was found in the atlas, but it should not exist.",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
tests/ttf.rs
Normal file
115
tests/ttf.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use pacman::texture::ttf::{TtfAtlas, TtfRenderer};
|
||||||
|
use sdl2::pixels::Color;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_width_calculates_correctly_for_empty_string() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let width = renderer.text_width(&atlas, "");
|
||||||
|
|
||||||
|
assert_eq!(width, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_width_calculates_correctly_for_single_character() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let width = renderer.text_width(&atlas, "A");
|
||||||
|
|
||||||
|
assert!(width > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_width_scales_correctly() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer1 = TtfRenderer::new(1.0);
|
||||||
|
let renderer2 = TtfRenderer::new(2.0);
|
||||||
|
|
||||||
|
let width1 = renderer1.text_width(&atlas, "Test");
|
||||||
|
let width2 = renderer2.text_width(&atlas, "Test");
|
||||||
|
|
||||||
|
assert_eq!(width2, width1 * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_height_returns_non_zero_for_valid_atlas() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let height = renderer.text_height(&atlas);
|
||||||
|
|
||||||
|
assert!(height > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_height_scales_correctly() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer1 = TtfRenderer::new(1.0);
|
||||||
|
let renderer2 = TtfRenderer::new(2.0);
|
||||||
|
|
||||||
|
let height1 = renderer1.text_height(&atlas);
|
||||||
|
let height2 = renderer2.text_height(&atlas);
|
||||||
|
|
||||||
|
assert_eq!(height2, height1 * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_text_handles_empty_string() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let result = renderer.render_text(&mut canvas, &mut atlas, "", glam::Vec2::new(0.0, 0.0), Color::WHITE);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_text_handles_single_character() {
|
||||||
|
let (mut canvas, texture_creator, _sdl) = common::setup_sdl().unwrap();
|
||||||
|
let _ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = _ttf_context.load_font("assets/game/TerminalVector.ttf", 16).unwrap();
|
||||||
|
|
||||||
|
let mut atlas = TtfAtlas::new(&texture_creator, &font).unwrap();
|
||||||
|
atlas.populate_atlas(&mut canvas, &texture_creator, &font).unwrap();
|
||||||
|
|
||||||
|
let renderer = TtfRenderer::new(1.0);
|
||||||
|
let result = renderer.render_text(&mut canvas, &mut atlas, "A", glam::Vec2::new(10.0, 10.0), Color::RED);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user