Compare commits

..

57 Commits

Author SHA1 Message Date
90adaf9e84 feat: add cursor-based node highlighting for debug 2025-08-16 12:26:24 -05:00
2140fbec1b fix: allow key holddown 2025-08-16 12:00:58 -05:00
78300bdf9c feat: rewrite movement systems separately for player/ghosts 2025-08-16 11:44:10 -05:00
514a447162 refactor: use strum::EnumCount for const compile time system mapping 2025-08-16 11:43:46 -05:00
3d0bc66e40 feat: ghosts system 2025-08-15 20:38:18 -05:00
e0a15c1ca8 feat: implement audio muting functionality 2025-08-15 20:30:41 -05:00
fa12611c69 feat: ecs audio system 2025-08-15 20:28:47 -05:00
342f378860 fix: use renderable layer properly, sorting entities before presenting 2025-08-15 20:10:16 -05:00
e8944598cc chore: fix clippy warnings 2025-08-15 20:10:16 -05:00
6af25af5f3 test: better formatting tests, alignment-based 2025-08-15 19:39:59 -05:00
f1935ad016 refactor: use smallvec instead of collect string, explicit formatting, accumulator fold 2025-08-15 19:15:06 -05:00
4d397bba5f feat: item collection system, score mutations 2025-08-15 18:41:08 -05:00
80930ddd35 fix: use const MAX_SYSTEMS to ensure micromap maps are aligned in size 2025-08-15 18:40:24 -05:00
0133dd5329 feat: add background for text contrast to debug window 2025-08-15 18:39:39 -05:00
635418a4da refactor: use stack allocated circular buffer, use RwLock+Mutex for concurrent system timing access 2025-08-15 18:06:25 -05:00
31193160a9 feat: debug text rendering of statistics, formatting with tests 2025-08-15 17:52:16 -05:00
3086453c7b chore: adjust collider sizes 2025-08-15 16:25:42 -05:00
a8b83b8e2b feat: high resolution debug rendering 2025-08-15 16:20:24 -05:00
8ce2af89c8 fix: add visibility check to rendering implementation 2025-08-15 15:10:09 -05:00
5f0ee87dd9 feat: better profiling statistics, less spammy 2025-08-15 15:06:53 -05:00
b88895e82f feat: separate dirty rendering with flag resource 2025-08-15 14:19:39 -05:00
2f0c734d13 feat: only present/render canvas when renderables change 2025-08-15 14:15:18 -05:00
e96b3159d7 fix: disable vsync 2025-08-15 13:46:57 -05:00
8c95ecc547 feat: add profiling 2025-08-15 13:46:39 -05:00
02a98c9f32 chore: remove unnecessary log, simplify match to if let 2025-08-15 13:05:55 -05:00
7f95c0233e refactor: move position/movement related components into systems/movement 2025-08-15 13:05:03 -05:00
a531228b95 chore: update thiserror & phf crates 2025-08-15 13:04:39 -05:00
de86f383bf refactor: improve representation of movement system 2025-08-15 12:50:07 -05:00
bd811ee783 fix: initial next direction for pacman (mitigation) 2025-08-15 12:30:29 -05:00
57d7f75940 feat: implement generic optimized collision system 2025-08-15 12:21:29 -05:00
c5d6ea28e1 fix: discard PlayerControlled tag component 2025-08-15 11:28:08 -05:00
730daed20a feat: entity type for proper edge permission calculations 2025-08-15 10:06:09 -05:00
b9bae99a4c refactor: reorganize systems properly, move events to events.rs 2025-08-15 09:48:16 -05:00
2c65048fb0 refactor: rename 'ecs' submodule to 'systems' 2025-08-15 09:27:28 -05:00
3388d77ec5 refactor: remove all unused/broken tests, remove many now unused types/functions 2025-08-15 09:24:42 -05:00
242da2e263 refactor: reorganize ecs components 2025-08-15 09:17:43 -05:00
70fb2b9503 fix: working movement again with ecs 2025-08-14 18:35:23 -05:00
0aa056a0ae feat: ecs keyboard interactions 2025-08-14 18:17:58 -05:00
b270318640 feat: directional rendering, interactivity 2025-08-14 15:44:07 -05:00
bc759f1ed4 refactor!: begin switching to bevy ECS, all tests broken, all systems broken 2025-08-14 15:06:56 -05:00
2f1ff85d8f refactor: handle pausing within game, reduce input system allocations 2025-08-14 10:36:39 -05:00
b7429cd9ec chore: solve tests/ clippy warnings 2025-08-14 09:46:10 -05:00
12a63374a8 feat: avoid using spin sleep unless focused 2025-08-13 23:30:07 -05:00
d80d7061e7 refactor: build decoupled input processing & add event queue system 2025-08-13 20:45:56 -05:00
abdefe0af0 chore: add hidden note about why Coveralls.io is disappointing today 2025-08-13 19:52:58 -05:00
4f76de7c9f feat: enable vsync & hardware acceleration 2025-08-13 19:49:02 -05:00
db8cd6220a feat: cache dynamicly rendered map texture 2025-08-13 19:48:50 -05:00
ced4e87d41 feat: embed atlas.json via phf instead of runtime parsing 2025-08-13 00:37:37 -05:00
09e3d85821 feat!: dynamic map rendering from tiles 2025-08-13 00:25:34 -05:00
c1e421bbbb test: new graph tests 2025-08-12 19:58:37 -05:00
3a9381a56c chore: use NodeId explicitly in collision.rs types 2025-08-12 19:58:11 -05:00
90bdfbd2ae chore: remove emscripten.rs platform from coverage, add html generation task, hide absolute path with remap-path-prefix, organize gitignore 2025-08-12 19:57:52 -05:00
a230d15ffc test: setup common submodule, add text.rs tests, pattern exclude error.rs 2025-08-12 19:24:06 -05:00
60bbd1f5d6 ci: add retry mechanism for coverage reporting via Coveralls CLI 2025-08-12 18:31:07 -05:00
43ce8a4e01 ci: use justfile for coverage, separate report/generate coverage tasks 2025-08-12 18:00:57 -05:00
1529a64588 test: add asset path validity tests 2025-08-12 17:24:12 -05:00
be5eec64c9 Add justfile for handling multiple coverage steps, prevent early termination of coverage job 2025-08-12 17:24:12 -05:00
149 changed files with 5646 additions and 3272 deletions

View File

@@ -42,15 +42,39 @@ jobs:
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest
- uses: taiki-e/install-action@just
# Note: We manually link zlib. This should be synchronized with the flags set for Linux in .cargo/config.toml.
- name: Generate coverage report
run: |
cargo llvm-cov --no-fail-fast --lcov --output-path lcov.info nextest
just coverage
- name: Download Coveralls CLI
run: |
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
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
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
files: ./lcov.info
format: lcov
allow-empty: false
env:
COVERALLS_REPO_TOKEN: ${{ secrets.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

15
.gitignore vendored
View File

@@ -1,8 +1,17 @@
# IDE, Other files
.vscode
.idea
rust-sdl2-emscripten/
# Build files
target/
dist/
emsdk/
.idea
rust-sdl2-emscripten/
assets/site/build.css
# Site build f iles
tailwindcss-*
assets/site/build.css
# Coverage reports
lcov.info
coverage.html

814
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tracing = { version = "0.1.40", features = ["max_level_debug", "release_max_level_debug"]}
tracing = { version = "0.1.41", features = ["max_level_debug", "release_max_level_debug"]}
tracing-error = "0.2.0"
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
lazy_static = "1.5.0"
@@ -15,14 +15,23 @@ spin_sleep = "1.3.2"
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
pathfinding = "4.14"
once_cell = "1.21.3"
thiserror = "2.0"
thiserror = "2.0.14"
anyhow = "1.0"
glam = { version = "0.30.5", features = [] }
glam = "0.30.5"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
smallvec = "1.15.1"
strum = "0.27.2"
strum_macros = "0.27.2"
phf = { version = "0.12.1", features = ["macros"] }
bevy_ecs = "0.16.1"
bitflags = "2.9.1"
parking_lot = "0.12.3"
micromap = "0.1.0"
thousands = "0.2.0"
pretty_assertions = "1.4.1"
num-width = "0.1.0"
circular-buffer = "1.1.0"
[profile.release]
lto = true
@@ -57,3 +66,8 @@ aarch64-apple-darwin = { triplet = "arm64-osx" }
[target.'cfg(target_os = "emscripten")'.dependencies]
libc = "0.2.175"
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
phf = { version = "0.12.1", features = ["macros"] }

33
Justfile Normal file
View File

@@ -0,0 +1,33 @@
set shell := ["bash", "-c"]
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"
# !!! --ignore-filename-regex should be used on both reports & coverage testing
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
# Generate HTML report (for humans, source line inspection)
html: coverage
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--html \
--open
# Display report (for humans)
report-coverage: coverage
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
# Run & generate report (for CI)
coverage:
cargo llvm-cov \
--lcov \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--output-path lcov.info \
--profile coverage \
--no-fail-fast nextest

View File

@@ -1,6 +1,6 @@
# Pac-Man
[![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]
[![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]
[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

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -36,7 +36,7 @@ analyzer = "nextest"
[jobs.coverage]
command = [
"cargo", "llvm-cov", "--profile", "coverage", "--color", "always", "--no-fail-fast", "nextest", "--no-capture", "--summary-only", "--"
"just", "report-coverage"
]
need_stdout = true
ignored_lines = [
@@ -49,8 +49,14 @@ ignored_lines = [
"[─]+",
"test.+ok",
"PASS|START",
"Starting \\d+ test"
"Starting \\d+ test",
"\\s*#",
"\\s*Finished.+in \\d+",
"\\s*Summary\\s+\\[",
"\\s*Blocking",
"Finished report saved to"
]
on_change_strategy = "wait_then_restart"
[jobs.doc]
command = ["cargo", "doc", "--no-deps"]

50
build.rs Normal file
View File

@@ -0,0 +1,50 @@
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AtlasMapper {
frames: HashMap<String, MapperFrame>,
}
#[derive(Copy, Clone, Debug, Deserialize)]
struct MapperFrame {
x: u16,
y: u16,
width: u16,
height: u16,
}
fn main() {
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("atlas_data.rs");
let mut file = BufWriter::new(File::create(&path).unwrap());
let atlas_json = include_str!("./assets/game/atlas.json");
let atlas_mapper: AtlasMapper = serde_json::from_str(atlas_json).unwrap();
writeln!(&mut file, "use phf::phf_map;").unwrap();
writeln!(&mut file, "use crate::texture::sprite::MapperFrame;").unwrap();
writeln!(
&mut file,
"pub static ATLAS_FRAMES: phf::Map<&'static str, MapperFrame> = phf_map! {{"
)
.unwrap();
for (name, frame) in atlas_mapper.frames {
writeln!(
&mut file,
" \"{}\" => MapperFrame {{ x: {}, y: {}, width: {}, height: {} }},",
name, frame.x, frame.y, frame.width, frame.height
)
.unwrap();
}
writeln!(&mut file, "}};").unwrap();
println!("cargo:rerun-if-changed=assets/game/atlas.json");
}

View File

@@ -1,13 +1,10 @@
use std::time::{Duration, Instant};
use glam::Vec2;
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::Keycode;
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
use sdl2::render::TextureCreator;
use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::{Window, WindowContext};
use sdl2::video::WindowContext;
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
use tracing::{error, event};
use crate::error::{GameError, GameResult};
@@ -16,13 +13,10 @@ use crate::game::Game;
use crate::platform::get_platform;
pub struct App {
game: Game,
canvas: Canvas<Window>,
event_pump: &'static mut EventPump,
backbuffer: Texture<'static>,
paused: bool,
pub game: Game,
last_tick: Instant,
cursor_pos: Vec2,
focused: bool,
_cursor_pos: Vec2,
}
impl App {
@@ -51,35 +45,28 @@ impl App {
.build()
.map_err(|e| GameError::Sdl(e.to_string()))?;
let mut canvas = window.into_canvas().build().map_err(|e| GameError::Sdl(e.to_string()))?;
let canvas = Box::leak(Box::new(
window
.into_canvas()
.accelerated()
.build()
.map_err(|e| GameError::Sdl(e.to_string()))?,
));
canvas
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
let texture_creator: &'static TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
let texture_creator: &'static mut TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
let mut game = Game::new(texture_creator)?;
let game = Game::new(canvas, texture_creator, event_pump)?;
// game.audio.set_mute(cfg!(debug_assertions));
let mut backbuffer = texture_creator
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
backbuffer.set_scale_mode(ScaleMode::Nearest);
// Initial draw
game.draw(&mut canvas, &mut backbuffer)
.map_err(|e| GameError::Sdl(e.to_string()))?;
game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO)
.map_err(|e| GameError::Sdl(e.to_string()))?;
Ok(Self {
Ok(App {
game,
canvas,
event_pump,
backbuffer,
paused: false,
focused: true,
last_tick: Instant::now(),
cursor_pos: Vec2::ZERO,
_cursor_pos: Vec2::ZERO,
})
}
@@ -87,78 +74,46 @@ impl App {
{
let start = Instant::now();
for event in self.event_pump.poll_iter() {
match event {
Event::Window { win_event, .. } => match win_event {
WindowEvent::Hidden => {
event!(tracing::Level::DEBUG, "Window hidden");
}
WindowEvent::Shown => {
event!(tracing::Level::DEBUG, "Window shown");
}
_ => {}
},
// It doesn't really make sense to have this available in the browser
#[cfg(not(target_os = "emscripten"))]
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
..
} => {
event!(tracing::Level::INFO, "Exit requested. Exiting...");
return false;
}
Event::KeyDown {
keycode: Some(Keycode::P),
..
} => {
self.paused = !self.paused;
event!(tracing::Level::INFO, "{}", if self.paused { "Paused" } else { "Unpaused" });
}
Event::KeyDown {
keycode: Some(Keycode::Space),
..
} => {
self.game.toggle_debug_mode();
}
Event::KeyDown { keycode: Some(key), .. } => {
self.game.keyboard_event(key);
}
Event::MouseMotion { x, y, .. } => {
// Convert window coordinates to logical coordinates
self.cursor_pos = Vec2::new(x as f32, y as f32);
}
_ => {}
}
}
// for event in self
// .game
// .world
// .get_non_send_resource_mut::<&'static mut EventPump>()
// .unwrap()
// .poll_iter()
// {
// match event {
// Event::Window { win_event, .. } => match win_event {
// WindowEvent::FocusGained => {
// self.focused = true;
// }
// WindowEvent::FocusLost => {
// self.focused = false;
// }
// _ => {}
// },
// Event::MouseMotion { x, y, .. } => {
// // Convert window coordinates to logical coordinates
// self.cursor_pos = Vec2::new(x as f32, y as f32);
// }
// _ => {}
// }
// }
let dt = self.last_tick.elapsed().as_secs_f32();
self.last_tick = Instant::now();
if !self.paused {
self.game.tick(dt);
if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
error!("Failed to draw game: {}", e);
}
if let Err(e) = self
.game
.present_backbuffer(&mut self.canvas, &self.backbuffer, self.cursor_pos)
{
error!("Failed to present backbuffer: {}", e);
}
let exit = self.game.tick(dt);
if exit {
return false;
}
// Sleep if we still have time left
if start.elapsed() < LOOP_TIME {
let time = LOOP_TIME.saturating_sub(start.elapsed());
if time != Duration::ZERO {
get_platform().sleep(time);
get_platform().sleep(time, self.focused);
}
} else {
event!(
tracing::Level::WARN,
"Game loop behind schedule by: {:?}",
start.elapsed() - LOOP_TIME
);
}
true

View File

@@ -3,16 +3,15 @@
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
use std::borrow::Cow;
use strum_macros::EnumIter;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum Asset {
Wav1,
Wav2,
Wav3,
Wav4,
Atlas,
AtlasJson,
// Add more as needed
}
impl Asset {
@@ -25,7 +24,6 @@ impl Asset {
Wav3 => "sound/waka/3.ogg",
Wav4 => "sound/waka/4.ogg",
Atlas => "atlas.png",
AtlasJson => "atlas.json",
}
}
}
@@ -35,6 +33,7 @@ mod imp {
use crate::error::AssetError;
use crate::platform::get_platform;
/// Returns the raw bytes of the given asset.
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
get_platform().get_asset_bytes(asset)
}

View File

@@ -18,8 +18,6 @@ pub const SCALE: f32 = 2.6;
pub const BOARD_CELL_OFFSET: UVec2 = UVec2::new(0, 3);
/// The offset of the game board from the top-left corner of the window, in pixels.
pub const BOARD_PIXEL_OFFSET: UVec2 = UVec2::new(BOARD_CELL_OFFSET.x * CELL_SIZE, BOARD_CELL_OFFSET.y * CELL_SIZE);
/// The size of the game board, in pixels.
pub const BOARD_PIXEL_SIZE: UVec2 = UVec2::new(BOARD_CELL_SIZE.x * CELL_SIZE, BOARD_CELL_SIZE.y * CELL_SIZE);
/// The size of the canvas, in pixels.
pub const CANVAS_SIZE: UVec2 = UVec2::new(
(BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE,

View File

@@ -1,128 +1,128 @@
use smallvec::SmallVec;
use std::collections::HashMap;
// use smallvec::SmallVec;
// use std::collections::HashMap;
use crate::entity::traversal::Position;
// use crate::entity::{graph::NodeId, traversal::Position};
/// Trait for entities that can participate in collision detection.
pub trait Collidable {
/// Returns the current position of this entity.
fn position(&self) -> Position;
// /// Trait for entities that can participate in collision detection.
// pub trait Collidable {
// /// Returns the current position of this entity.
// fn position(&self) -> Position;
/// Checks if this entity is colliding with another entity.
#[allow(dead_code)]
fn is_colliding_with(&self, other: &dyn Collidable) -> bool {
positions_overlap(&self.position(), &other.position())
}
}
// /// Checks if this entity is colliding with another entity.
// #[allow(dead_code)]
// fn is_colliding_with(&self, other: &dyn Collidable) -> bool {
// positions_overlap(&self.position(), &other.position())
// }
// }
/// System for tracking entities by their positions for efficient collision detection.
#[derive(Default)]
pub struct CollisionSystem {
/// Maps node IDs to lists of entity IDs that are at that node
node_entities: HashMap<usize, Vec<EntityId>>,
/// Maps entity IDs to their current positions
entity_positions: HashMap<EntityId, Position>,
/// Next available entity ID
next_id: EntityId,
}
// /// System for tracking entities by their positions for efficient collision detection.
// #[derive(Default)]
// pub struct CollisionSystem {
// /// Maps node IDs to lists of entity IDs that are at that node
// node_entities: HashMap<NodeId, Vec<EntityId>>,
// /// Maps entity IDs to their current positions
// entity_positions: HashMap<EntityId, Position>,
// /// Next available entity ID
// next_id: EntityId,
// }
/// Unique identifier for an entity in the collision system
pub type EntityId = u32;
// /// Unique identifier for an entity in the collision system
// pub type EntityId = u32;
impl CollisionSystem {
/// Registers an entity with the collision system and returns its ID
pub fn register_entity(&mut self, position: Position) -> EntityId {
let id = self.next_id;
self.next_id += 1;
// impl CollisionSystem {
// /// Registers an entity with the collision system and returns its ID
// pub fn register_entity(&mut self, position: Position) -> EntityId {
// let id = self.next_id;
// self.next_id += 1;
self.entity_positions.insert(id, position);
self.update_node_entities(id, position);
// self.entity_positions.insert(id, position);
// self.update_node_entities(id, position);
id
}
// id
// }
/// Updates an entity's position
pub fn update_position(&mut self, entity_id: EntityId, new_position: Position) {
if let Some(old_position) = self.entity_positions.get(&entity_id) {
// Remove from old nodes
self.remove_from_nodes(entity_id, *old_position);
}
// /// Updates an entity's position
// pub fn update_position(&mut self, entity_id: EntityId, new_position: Position) {
// if let Some(old_position) = self.entity_positions.get(&entity_id) {
// // Remove from old nodes
// self.remove_from_nodes(entity_id, *old_position);
// }
// Update position and add to new nodes
self.entity_positions.insert(entity_id, new_position);
self.update_node_entities(entity_id, new_position);
}
// // Update position and add to new nodes
// self.entity_positions.insert(entity_id, new_position);
// self.update_node_entities(entity_id, new_position);
// }
/// Removes an entity from the collision system
#[allow(dead_code)]
pub fn remove_entity(&mut self, entity_id: EntityId) {
if let Some(position) = self.entity_positions.remove(&entity_id) {
self.remove_from_nodes(entity_id, position);
}
}
// /// Removes an entity from the collision system
// #[allow(dead_code)]
// pub fn remove_entity(&mut self, entity_id: EntityId) {
// if let Some(position) = self.entity_positions.remove(&entity_id) {
// self.remove_from_nodes(entity_id, position);
// }
// }
/// Gets all entity IDs at a specific node
pub fn entities_at_node(&self, node: usize) -> &[EntityId] {
self.node_entities.get(&node).map(|v| v.as_slice()).unwrap_or(&[])
}
// /// Gets all entity IDs at a specific node
// pub fn entities_at_node(&self, node: NodeId) -> &[EntityId] {
// self.node_entities.get(&node).map(|v| v.as_slice()).unwrap_or(&[])
// }
/// Gets all entity IDs that could collide with an entity at the given position
pub fn potential_collisions(&self, position: &Position) -> Vec<EntityId> {
let mut collisions = Vec::new();
let nodes = get_nodes(position);
// /// Gets all entity IDs that could collide with an entity at the given position
// pub fn potential_collisions(&self, position: &Position) -> Vec<EntityId> {
// let mut collisions = Vec::new();
// let nodes = get_nodes(position);
for node in nodes {
collisions.extend(self.entities_at_node(node));
}
// for node in nodes {
// collisions.extend(self.entities_at_node(node));
// }
// Remove duplicates
collisions.sort_unstable();
collisions.dedup();
collisions
}
// // Remove duplicates
// collisions.sort_unstable();
// collisions.dedup();
// collisions
// }
/// Updates the node_entities map when an entity's position changes
fn update_node_entities(&mut self, entity_id: EntityId, position: Position) {
let nodes = get_nodes(&position);
for node in nodes {
self.node_entities.entry(node).or_default().push(entity_id);
}
}
// /// Updates the node_entities map when an entity's position changes
// fn update_node_entities(&mut self, entity_id: EntityId, position: Position) {
// let nodes = get_nodes(&position);
// for node in nodes {
// self.node_entities.entry(node).or_default().push(entity_id);
// }
// }
/// Removes an entity from all nodes it was previously at
fn remove_from_nodes(&mut self, entity_id: EntityId, position: Position) {
let nodes = get_nodes(&position);
for node in nodes {
if let Some(entities) = self.node_entities.get_mut(&node) {
entities.retain(|&id| id != entity_id);
if entities.is_empty() {
self.node_entities.remove(&node);
}
}
}
}
}
// /// Removes an entity from all nodes it was previously at
// fn remove_from_nodes(&mut self, entity_id: EntityId, position: Position) {
// let nodes = get_nodes(&position);
// for node in nodes {
// if let Some(entities) = self.node_entities.get_mut(&node) {
// entities.retain(|&id| id != entity_id);
// if entities.is_empty() {
// self.node_entities.remove(&node);
// }
// }
// }
// }
// }
/// Checks if two positions overlap (entities are at the same location).
fn positions_overlap(a: &Position, b: &Position) -> bool {
let a_nodes = get_nodes(a);
let b_nodes = get_nodes(b);
// /// Checks if two positions overlap (entities are at the same location).
// fn positions_overlap(a: &Position, b: &Position) -> bool {
// let a_nodes = get_nodes(a);
// let b_nodes = get_nodes(b);
// Check if any nodes overlap
a_nodes.iter().any(|a_node| b_nodes.contains(a_node))
// // Check if any nodes overlap
// a_nodes.iter().any(|a_node| b_nodes.contains(a_node))
// TODO: More complex overlap detection, the above is a simple check, but it could become an early filter for more precise calculations later
}
// // TODO: More complex overlap detection, the above is a simple check, but it could become an early filter for more precise calculations later
// }
/// Gets all nodes that an entity is currently at or between.
fn get_nodes(pos: &Position) -> SmallVec<[usize; 2]> {
let mut nodes = SmallVec::new();
match pos {
Position::AtNode(node) => nodes.push(*node),
Position::BetweenNodes { from, to, .. } => {
nodes.push(*from);
nodes.push(*to);
}
}
nodes
}
// /// Gets all nodes that an entity is currently at or between.
// fn get_nodes(pos: &Position) -> SmallVec<[NodeId; 2]> {
// let mut nodes = SmallVec::new();
// match pos {
// Position::AtNode(node) => nodes.push(*node),
// Position::BetweenNodes { from, to, .. } => {
// nodes.push(*from);
// nodes.push(*to);
// }
// }
// nodes
// }

View File

@@ -1,11 +1,13 @@
use glam::IVec2;
/// The four cardinal directions.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(usize)]
pub enum Direction {
Up,
Down,
Left,
#[default]
Right,
}

View File

@@ -1,254 +1,254 @@
//! Ghost entity implementation.
//!
//! This module contains the ghost character logic, including movement,
//! animation, and rendering. Ghosts move through the game graph using
//! a traverser and display directional animated textures.
// //! Ghost entity implementation.
// //!
// //! This module contains the ghost character logic, including movement,
// //! animation, and rendering. Ghosts move through the game graph using
// //! a traverser and display directional animated textures.
use pathfinding::prelude::dijkstra;
use rand::prelude::*;
use smallvec::SmallVec;
use tracing::error;
// use pathfinding::prelude::dijkstra;
// use rand::prelude::*;
// use smallvec::SmallVec;
// use tracing::error;
use crate::entity::{
collision::Collidable,
direction::Direction,
graph::{Edge, EdgePermissions, Graph, NodeId},
r#trait::Entity,
traversal::Traverser,
};
use crate::texture::animated::AnimatedTexture;
use crate::texture::directional::DirectionalAnimatedTexture;
use crate::texture::sprite::SpriteAtlas;
// use crate::entity::{
// collision::Collidable,
// direction::Direction,
// graph::{Edge, EdgePermissions, Graph, NodeId},
// r#trait::Entity,
// traversal::Traverser,
// };
// use crate::texture::animated::AnimatedTexture;
// use crate::texture::directional::DirectionalAnimatedTexture;
// use crate::texture::sprite::SpriteAtlas;
use crate::error::{EntityError, GameError, GameResult, TextureError};
// use crate::error::{EntityError, GameError, GameResult, TextureError};
/// Determines if a ghost can traverse a given edge.
///
/// Ghosts can move through edges that allow all entities or ghost-only edges.
fn can_ghost_traverse(edge: Edge) -> bool {
matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly)
}
// /// Determines if a ghost can traverse a given edge.
// ///
// /// Ghosts can move through edges that allow all entities or ghost-only edges.
// fn can_ghost_traverse(edge: Edge) -> bool {
// matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly)
// }
/// The four classic ghost types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GhostType {
Blinky,
Pinky,
Inky,
Clyde,
}
// /// The four classic ghost types.
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
// pub enum GhostType {
// Blinky,
// Pinky,
// Inky,
// Clyde,
// }
impl GhostType {
/// Returns the ghost type name for atlas lookups.
pub fn as_str(self) -> &'static str {
match self {
GhostType::Blinky => "blinky",
GhostType::Pinky => "pinky",
GhostType::Inky => "inky",
GhostType::Clyde => "clyde",
}
}
// impl GhostType {
// /// Returns the ghost type name for atlas lookups.
// pub fn as_str(self) -> &'static str {
// match self {
// GhostType::Blinky => "blinky",
// GhostType::Pinky => "pinky",
// GhostType::Inky => "inky",
// GhostType::Clyde => "clyde",
// }
// }
/// Returns the base movement speed for this ghost type.
pub fn base_speed(self) -> f32 {
match self {
GhostType::Blinky => 1.0,
GhostType::Pinky => 0.95,
GhostType::Inky => 0.9,
GhostType::Clyde => 0.85,
}
}
}
// /// Returns the base movement speed for this ghost type.
// pub fn base_speed(self) -> f32 {
// match self {
// GhostType::Blinky => 1.0,
// GhostType::Pinky => 0.95,
// GhostType::Inky => 0.9,
// GhostType::Clyde => 0.85,
// }
// }
// }
/// A ghost entity that roams the game world.
///
/// Ghosts move through the game world using a graph-based navigation system
/// and display directional animated sprites. They randomly choose directions
/// at each intersection.
pub struct Ghost {
/// Handles movement through the game graph
pub traverser: Traverser,
/// The type of ghost (affects appearance and speed)
pub ghost_type: GhostType,
/// Manages directional animated textures for different movement states
texture: DirectionalAnimatedTexture,
/// Current movement speed
speed: f32,
}
// /// A ghost entity that roams the game world.
// ///
// /// Ghosts move through the game world using a graph-based navigation system
// /// and display directional animated sprites. They randomly choose directions
// /// at each intersection.
// pub struct Ghost {
// /// Handles movement through the game graph
// pub traverser: Traverser,
// /// The type of ghost (affects appearance and speed)
// pub ghost_type: GhostType,
// /// Manages directional animated textures for different movement states
// texture: DirectionalAnimatedTexture,
// /// Current movement speed
// speed: f32,
// }
impl Entity for Ghost {
fn traverser(&self) -> &Traverser {
&self.traverser
}
// impl Entity for Ghost {
// fn traverser(&self) -> &Traverser {
// &self.traverser
// }
fn traverser_mut(&mut self) -> &mut Traverser {
&mut self.traverser
}
// fn traverser_mut(&mut self) -> &mut Traverser {
// &mut self.traverser
// }
fn texture(&self) -> &DirectionalAnimatedTexture {
&self.texture
}
// fn texture(&self) -> &DirectionalAnimatedTexture {
// &self.texture
// }
fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture {
&mut self.texture
}
// fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture {
// &mut self.texture
// }
fn speed(&self) -> f32 {
self.speed
}
// fn speed(&self) -> f32 {
// self.speed
// }
fn can_traverse(&self, edge: Edge) -> bool {
can_ghost_traverse(edge)
}
// fn can_traverse(&self, edge: Edge) -> bool {
// can_ghost_traverse(edge)
// }
fn tick(&mut self, dt: f32, graph: &Graph) {
// Choose random direction when at a node
if self.traverser.position.is_at_node() {
self.choose_random_direction(graph);
}
// fn tick(&mut self, dt: f32, graph: &Graph) {
// // Choose random direction when at a node
// if self.traverser.position.is_at_node() {
// self.choose_random_direction(graph);
// }
if let Err(e) = self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse) {
error!("Ghost movement error: {}", e);
}
self.texture.tick(dt);
}
}
// if let Err(e) = self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse) {
// error!("Ghost movement error: {}", e);
// }
// self.texture.tick(dt);
// }
// }
impl Ghost {
/// Creates a new ghost instance at the specified starting node.
///
/// Sets up animated textures for all four directions with moving and stopped states.
/// The moving animation cycles through two sprite variants.
pub fn new(graph: &Graph, start_node: NodeId, ghost_type: GhostType, atlas: &SpriteAtlas) -> GameResult<Self> {
let mut textures = [None, None, None, None];
let mut stopped_textures = [None, None, None, None];
// impl Ghost {
// /// Creates a new ghost instance at the specified starting node.
// ///
// /// Sets up animated textures for all four directions with moving and stopped states.
// /// The moving animation cycles through two sprite variants.
// pub fn new(graph: &Graph, start_node: NodeId, ghost_type: GhostType, atlas: &SpriteAtlas) -> GameResult<Self> {
// let mut textures = [None, None, None, None];
// let mut stopped_textures = [None, None, None, None];
for direction in Direction::DIRECTIONS {
let moving_prefix = match direction {
Direction::Up => "up",
Direction::Down => "down",
Direction::Left => "left",
Direction::Right => "right",
};
let moving_tiles = vec![
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"))
.ok_or_else(|| {
GameError::Texture(TextureError::AtlasTileNotFound(format!(
"ghost/{}/{}_{}.png",
ghost_type.as_str(),
moving_prefix,
"a"
)))
})?,
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "b"))
.ok_or_else(|| {
GameError::Texture(TextureError::AtlasTileNotFound(format!(
"ghost/{}/{}_{}.png",
ghost_type.as_str(),
moving_prefix,
"b"
)))
})?,
];
// for direction in Direction::DIRECTIONS {
// let moving_prefix = match direction {
// Direction::Up => "up",
// Direction::Down => "down",
// Direction::Left => "left",
// Direction::Right => "right",
// };
// let moving_tiles = vec![
// SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"))
// .ok_or_else(|| {
// GameError::Texture(TextureError::AtlasTileNotFound(format!(
// "ghost/{}/{}_{}.png",
// ghost_type.as_str(),
// moving_prefix,
// "a"
// )))
// })?,
// SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "b"))
// .ok_or_else(|| {
// GameError::Texture(TextureError::AtlasTileNotFound(format!(
// "ghost/{}/{}_{}.png",
// ghost_type.as_str(),
// moving_prefix,
// "b"
// )))
// })?,
// ];
let stopped_tiles =
vec![
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"))
.ok_or_else(|| {
GameError::Texture(TextureError::AtlasTileNotFound(format!(
"ghost/{}/{}_{}.png",
ghost_type.as_str(),
moving_prefix,
"a"
)))
})?,
];
// let stopped_tiles =
// vec![
// SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"))
// .ok_or_else(|| {
// GameError::Texture(TextureError::AtlasTileNotFound(format!(
// "ghost/{}/{}_{}.png",
// ghost_type.as_str(),
// moving_prefix,
// "a"
// )))
// })?,
// ];
textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2)?);
stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?);
}
// textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2)?);
// stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?);
// }
Ok(Self {
traverser: Traverser::new(graph, start_node, Direction::Left, &can_ghost_traverse),
ghost_type,
texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
speed: ghost_type.base_speed(),
})
}
// Ok(Self {
// traverser: Traverser::new(graph, start_node, Direction::Left, &can_ghost_traverse),
// ghost_type,
// texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
// speed: ghost_type.base_speed(),
// })
// }
/// Chooses a random available direction at the current intersection.
fn choose_random_direction(&mut self, graph: &Graph) {
let current_node = self.traverser.position.from_node_id();
let intersection = &graph.adjacency_list[current_node];
// /// Chooses a random available direction at the current intersection.
// fn choose_random_direction(&mut self, graph: &Graph) {
// let current_node = self.traverser.position.from_node_id();
// let intersection = &graph.adjacency_list[current_node];
// Collect all available directions
let mut available_directions = SmallVec::<[_; 4]>::new();
for direction in Direction::DIRECTIONS {
if let Some(edge) = intersection.get(direction) {
if can_ghost_traverse(edge) {
available_directions.push(direction);
}
}
}
// Choose a random direction (avoid reversing unless necessary)
if !available_directions.is_empty() {
let mut rng = SmallRng::from_os_rng();
// // Collect all available directions
// let mut available_directions = SmallVec::<[_; 4]>::new();
// for direction in Direction::DIRECTIONS {
// if let Some(edge) = intersection.get(direction) {
// if can_ghost_traverse(edge) {
// available_directions.push(direction);
// }
// }
// }
// // Choose a random direction (avoid reversing unless necessary)
// if !available_directions.is_empty() {
// let mut rng = SmallRng::from_os_rng();
// Filter out the opposite direction if possible, but allow it if we have limited options
let opposite = self.traverser.direction.opposite();
let filtered_directions: Vec<_> = available_directions
.iter()
.filter(|&&dir| dir != opposite || available_directions.len() <= 2)
.collect();
// // Filter out the opposite direction if possible, but allow it if we have limited options
// let opposite = self.traverser.direction.opposite();
// let filtered_directions: Vec<_> = available_directions
// .iter()
// .filter(|&&dir| dir != opposite || available_directions.len() <= 2)
// .collect();
if let Some(&random_direction) = filtered_directions.choose(&mut rng) {
self.traverser.set_next_direction(*random_direction);
}
}
}
// if let Some(&random_direction) = filtered_directions.choose(&mut rng) {
// self.traverser.set_next_direction(*random_direction);
// }
// }
// }
/// Calculates the shortest path from the ghost's current position to a target node using Dijkstra's algorithm.
///
/// Returns a vector of NodeIds representing the path, or an error if pathfinding fails.
/// The path includes the current node and the target node.
pub fn calculate_path_to_target(&self, graph: &Graph, target: NodeId) -> GameResult<Vec<NodeId>> {
let start_node = self.traverser.position.from_node_id();
// /// Calculates the shortest path from the ghost's current position to a target node using Dijkstra's algorithm.
// ///
// /// Returns a vector of NodeIds representing the path, or an error if pathfinding fails.
// /// The path includes the current node and the target node.
// pub fn calculate_path_to_target(&self, graph: &Graph, target: NodeId) -> GameResult<Vec<NodeId>> {
// let start_node = self.traverser.position.from_node_id();
// Use Dijkstra's algorithm to find the shortest path
let result = dijkstra(
&start_node,
|&node_id| {
// Get all edges from the current node
graph.adjacency_list[node_id]
.edges()
.filter(|edge| can_ghost_traverse(*edge))
.map(|edge| (edge.target, (edge.distance * 100.0) as u32))
.collect::<Vec<_>>()
},
|&node_id| node_id == target,
);
// // Use Dijkstra's algorithm to find the shortest path
// let result = dijkstra(
// &start_node,
// |&node_id| {
// // Get all edges from the current node
// graph.adjacency_list[node_id]
// .edges()
// .filter(|edge| can_ghost_traverse(*edge))
// .map(|edge| (edge.target, (edge.distance * 100.0) as u32))
// .collect::<Vec<_>>()
// },
// |&node_id| node_id == target,
// );
result.map(|(path, _cost)| path).ok_or_else(|| {
GameError::Entity(EntityError::PathfindingFailed(format!(
"No path found from node {} to target {}",
start_node, target
)))
})
}
// result.map(|(path, _cost)| path).ok_or_else(|| {
// GameError::Entity(EntityError::PathfindingFailed(format!(
// "No path found from node {} to target {}",
// start_node, target
// )))
// })
// }
/// Returns the ghost's color for debug rendering.
pub fn debug_color(&self) -> sdl2::pixels::Color {
match self.ghost_type {
GhostType::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red
GhostType::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink
GhostType::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan
GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange
}
}
}
// /// Returns the ghost's color for debug rendering.
// pub fn debug_color(&self) -> sdl2::pixels::Color {
// match self.ghost_type {
// GhostType::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red
// GhostType::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink
// GhostType::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan
// GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange
// }
// }
// }
impl Collidable for Ghost {
fn position(&self) -> crate::entity::traversal::Position {
self.traverser.position
}
}
// impl Collidable for Ghost {
// fn position(&self) -> crate::entity::traversal::Position {
// self.traverser.position
// }
// }

View File

@@ -1,18 +1,21 @@
use glam::Vec2;
use crate::systems::movement::NodeId;
use super::direction::Direction;
/// A unique identifier for a node, represented by its index in the graph's storage.
pub type NodeId = usize;
use bitflags::bitflags;
/// Defines who can traverse a given edge.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EdgePermissions {
/// Anyone can use this edge.
#[default]
All,
/// Only ghosts can use this edge.
GhostsOnly,
bitflags! {
/// Defines who can traverse a given edge using flags for fast checking.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TraversalFlags: u8 {
const PACMAN = 1 << 0;
const GHOST = 1 << 1;
/// Convenience flag for edges that all entities can use
const ALL = Self::PACMAN.bits() | Self::GHOST.bits();
}
}
/// Represents a directed edge from one node to another with a given weight (e.g., distance).
@@ -25,7 +28,7 @@ pub struct Edge {
/// The cardinal direction of this edge.
pub direction: Direction,
/// Defines who is allowed to traverse this edge.
pub permissions: EdgePermissions,
pub traversal_flags: TraversalFlags,
}
/// Represents a node in the graph, defined by its position.
@@ -133,8 +136,8 @@ impl Graph {
return Err("To node does not exist.");
}
let edge_a = self.add_edge(from, to, replace, distance, direction, EdgePermissions::default());
let edge_b = self.add_edge(to, from, replace, distance, direction.opposite(), EdgePermissions::default());
let edge_a = self.add_edge(from, to, replace, distance, direction, TraversalFlags::ALL);
let edge_b = self.add_edge(to, from, replace, distance, direction.opposite(), TraversalFlags::ALL);
if edge_a.is_err() && edge_b.is_err() {
return Err("Failed to connect nodes in both directions.");
@@ -162,7 +165,7 @@ impl Graph {
replace: bool,
distance: Option<f32>,
direction: Direction,
permissions: EdgePermissions,
traversal_flags: TraversalFlags,
) -> Result<(), &'static str> {
let edge = Edge {
target: to,
@@ -181,7 +184,7 @@ impl Graph {
}
},
direction,
permissions,
traversal_flags,
};
if from >= self.adjacency_list.len() {
@@ -192,14 +195,15 @@ impl Graph {
// Check if the edge already exists in this direction or to the same target
if let Some(err) = adjacency_list.edges().find_map(|e| {
// If we're not replacing the edge, we don't want to replace an edge that already exists in this direction
if !replace && e.direction == direction {
Some(Err("Edge already exists in this direction."))
} else if e.target == to {
Some(Err("Edge already exists."))
} else {
None
if !replace {
// If we're not replacing the edge, we don't want to replace an edge that already exists in this direction
if e.direction == direction {
return Some(Err("Edge already exists in this direction."));
} else if e.target == to {
return Some(Err("Edge already exists."));
}
}
None
}) {
return err;
}
@@ -219,6 +223,19 @@ impl Graph {
self.nodes.len()
}
/// Returns an iterator over all nodes in the graph.
pub fn nodes(&self) -> impl Iterator<Item = &Node> {
self.nodes.iter()
}
/// Returns an iterator over all edges in the graph.
pub fn edges(&self) -> impl Iterator<Item = (NodeId, Edge)> + '_ {
self.adjacency_list
.iter()
.enumerate()
.flat_map(|(node_id, intersection)| intersection.edges().map(move |edge| (node_id, edge)))
}
/// Finds a specific edge from a source node to a target node.
pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option<Edge> {
self.adjacency_list.get(from)?.edges().find(|edge| edge.target == to)

View File

@@ -1,117 +1,117 @@
use crate::{
constants,
entity::{collision::Collidable, graph::Graph},
error::{EntityError, GameResult},
texture::sprite::{Sprite, SpriteAtlas},
};
use sdl2::render::{Canvas, RenderTarget};
use strum_macros::{EnumCount, EnumIter};
// use crate::{
// constants,
// entity::{collision::Collidable, graph::Graph},
// error::{EntityError, GameResult},
// texture::sprite::{Sprite, SpriteAtlas},
// };
// use sdl2::render::{Canvas, RenderTarget};
// use strum_macros::{EnumCount, EnumIter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemType {
Pellet,
Energizer,
#[allow(dead_code)]
Fruit {
kind: FruitKind,
},
}
// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
// pub enum ItemType {
// Pellet,
// Energizer,
// #[allow(dead_code)]
// Fruit {
// kind: FruitKind,
// },
// }
impl ItemType {
pub fn get_score(self) -> u32 {
match self {
ItemType::Pellet => 10,
ItemType::Energizer => 50,
ItemType::Fruit { kind } => kind.get_score(),
}
}
}
// impl ItemType {
// pub fn get_score(self) -> u32 {
// match self {
// ItemType::Pellet => 10,
// ItemType::Energizer => 50,
// ItemType::Fruit { kind } => kind.get_score(),
// }
// }
// }
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCount)]
#[allow(dead_code)]
pub enum FruitKind {
Apple,
Strawberry,
Orange,
Melon,
Bell,
Key,
Galaxian,
}
// #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCount)]
// #[allow(dead_code)]
// pub enum FruitKind {
// Apple,
// Strawberry,
// Orange,
// Melon,
// Bell,
// Key,
// Galaxian,
// }
impl FruitKind {
#[allow(dead_code)]
pub fn index(self) -> u8 {
match self {
FruitKind::Apple => 0,
FruitKind::Strawberry => 1,
FruitKind::Orange => 2,
FruitKind::Melon => 3,
FruitKind::Bell => 4,
FruitKind::Key => 5,
FruitKind::Galaxian => 6,
}
}
// impl FruitKind {
// #[allow(dead_code)]
// pub fn index(self) -> u8 {
// match self {
// FruitKind::Apple => 0,
// FruitKind::Strawberry => 1,
// FruitKind::Orange => 2,
// FruitKind::Melon => 3,
// FruitKind::Bell => 4,
// FruitKind::Key => 5,
// FruitKind::Galaxian => 6,
// }
// }
pub fn get_score(self) -> u32 {
match self {
FruitKind::Apple => 100,
FruitKind::Strawberry => 300,
FruitKind::Orange => 500,
FruitKind::Melon => 700,
FruitKind::Bell => 1000,
FruitKind::Key => 2000,
FruitKind::Galaxian => 3000,
}
}
}
// pub fn get_score(self) -> u32 {
// match self {
// FruitKind::Apple => 100,
// FruitKind::Strawberry => 300,
// FruitKind::Orange => 500,
// FruitKind::Melon => 700,
// FruitKind::Bell => 1000,
// FruitKind::Key => 2000,
// FruitKind::Galaxian => 3000,
// }
// }
// }
pub struct Item {
pub node_index: usize,
pub item_type: ItemType,
pub sprite: Sprite,
pub collected: bool,
}
// pub struct Item {
// pub node_index: usize,
// pub item_type: ItemType,
// pub sprite: Sprite,
// pub collected: bool,
// }
impl Item {
pub fn new(node_index: usize, item_type: ItemType, sprite: Sprite) -> Self {
Self {
node_index,
item_type,
sprite,
collected: false,
}
}
// impl Item {
// pub fn new(node_index: usize, item_type: ItemType, sprite: Sprite) -> Self {
// Self {
// node_index,
// item_type,
// sprite,
// collected: false,
// }
// }
pub fn is_collected(&self) -> bool {
self.collected
}
// pub fn is_collected(&self) -> bool {
// self.collected
// }
pub fn collect(&mut self) {
self.collected = true;
}
// pub fn collect(&mut self) {
// self.collected = true;
// }
pub fn get_score(&self) -> u32 {
self.item_type.get_score()
}
// pub fn get_score(&self) -> u32 {
// self.item_type.get_score()
// }
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> GameResult<()> {
if self.collected {
return Ok(());
}
// pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> GameResult<()> {
// if self.collected {
// return Ok(());
// }
let node = graph
.get_node(self.node_index)
.ok_or(EntityError::NodeNotFound(self.node_index))?;
let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2();
// let node = graph
// .get_node(self.node_index)
// .ok_or(EntityError::NodeNotFound(self.node_index))?;
// let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2();
self.sprite.render(canvas, atlas, position)?;
Ok(())
}
}
// self.sprite.render(canvas, atlas, position)?;
// Ok(())
// }
// }
impl Collidable for Item {
fn position(&self) -> crate::entity::traversal::Position {
crate::entity::traversal::Position::AtNode(self.node_index)
}
}
// impl Collidable for Item {
// fn position(&self) -> crate::entity::traversal::Position {
// crate::entity::traversal::Position::AtNode(self.node_index)
// }
// }

View File

@@ -5,4 +5,3 @@ pub mod graph;
pub mod item;
pub mod pacman;
pub mod r#trait;
pub mod traversal;

View File

@@ -1,134 +1,115 @@
//! Pac-Man entity implementation.
//!
//! This module contains the main player character logic, including movement,
//! animation, and rendering. Pac-Man moves through the game graph using
//! a traverser and displays directional animated textures.
// //! Pac-Man entity implementation.
// //!
// //! This module contains the main player character logic, including movement,
// //! animation, and rendering. Pac-Man moves through the game graph using
// //! a traverser and displays directional animated textures.
use crate::entity::{
collision::Collidable,
direction::Direction,
graph::{Edge, EdgePermissions, Graph, NodeId},
r#trait::Entity,
traversal::Traverser,
};
use crate::texture::animated::AnimatedTexture;
use crate::texture::directional::DirectionalAnimatedTexture;
use crate::texture::sprite::SpriteAtlas;
use sdl2::keyboard::Keycode;
use tracing::error;
// use crate::entity::{
// collision::Collidable,
// direction::Direction,
// graph::{Edge, EdgePermissions, Graph, NodeId},
// r#trait::Entity,
// traversal::Traverser,
// };
// use crate::texture::animated::AnimatedTexture;
// use crate::texture::directional::DirectionalAnimatedTexture;
// use crate::texture::sprite::SpriteAtlas;
// use tracing::error;
use crate::error::{GameError, GameResult, TextureError};
// use crate::error::{GameError, GameResult, TextureError};
/// Determines if Pac-Man can traverse a given edge.
///
/// Pac-Man can only move through edges that allow all entities.
fn can_pacman_traverse(edge: Edge) -> bool {
matches!(edge.permissions, EdgePermissions::All)
}
// /// Determines if Pac-Man can traverse a given edge.
// ///
// /// Pac-Man can only move through edges that allow all entities.
// fn can_pacman_traverse(edge: Edge) -> bool {
// matches!(edge.permissions, EdgePermissions::All)
// }
/// The main player character entity.
///
/// Pac-Man moves through the game world using a graph-based navigation system
/// and displays directional animated sprites based on movement state.
pub struct Pacman {
/// Handles movement through the game graph
pub traverser: Traverser,
/// Manages directional animated textures for different movement states
texture: DirectionalAnimatedTexture,
}
// /// The main player character entity.
// ///
// /// Pac-Man moves through the game world using a graph-based navigation system
// /// and displays directional animated sprites based on movement state.
// pub struct Pacman {
// /// Handles movement through the game graph
// pub traverser: Traverser,
// /// Manages directional animated textures for different movement states
// texture: DirectionalAnimatedTexture,
// }
impl Entity for Pacman {
fn traverser(&self) -> &Traverser {
&self.traverser
}
// impl Entity for Pacman {
// fn traverser(&self) -> &Traverser {
// &self.traverser
// }
fn traverser_mut(&mut self) -> &mut Traverser {
&mut self.traverser
}
// fn traverser_mut(&mut self) -> &mut Traverser {
// &mut self.traverser
// }
fn texture(&self) -> &DirectionalAnimatedTexture {
&self.texture
}
// fn texture(&self) -> &DirectionalAnimatedTexture {
// &self.texture
// }
fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture {
&mut self.texture
}
// fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture {
// &mut self.texture
// }
fn speed(&self) -> f32 {
1.125
}
// fn speed(&self) -> f32 {
// 1.125
// }
fn can_traverse(&self, edge: Edge) -> bool {
can_pacman_traverse(edge)
}
// fn can_traverse(&self, edge: Edge) -> bool {
// can_pacman_traverse(edge)
// }
fn tick(&mut self, dt: f32, graph: &Graph) {
if let Err(e) = self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse) {
error!("Pac-Man movement error: {}", e);
}
self.texture.tick(dt);
}
}
// fn tick(&mut self, dt: f32, graph: &Graph) {
// if let Err(e) = self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse) {
// error!("Pac-Man movement error: {}", e);
// }
// self.texture.tick(dt);
// }
// }
impl Pacman {
/// Creates a new Pac-Man instance at the specified starting node.
///
/// Sets up animated textures for all four directions with moving and stopped states.
/// The moving animation cycles through open mouth, closed mouth, and full sprites.
pub fn new(graph: &Graph, start_node: NodeId, atlas: &SpriteAtlas) -> GameResult<Self> {
let mut textures = [None, None, None, None];
let mut stopped_textures = [None, None, None, None];
// impl Pacman {
// /// Creates a new Pac-Man instance at the specified starting node.
// ///
// /// Sets up animated textures for all four directions with moving and stopped states.
// /// The moving animation cycles through open mouth, closed mouth, and full sprites.
// pub fn new(graph: &Graph, start_node: NodeId, atlas: &SpriteAtlas) -> GameResult<Self> {
// let mut textures = [None, None, None, None];
// let mut stopped_textures = [None, None, None, None];
for direction in Direction::DIRECTIONS {
let moving_prefix = match direction {
Direction::Up => "pacman/up",
Direction::Down => "pacman/down",
Direction::Left => "pacman/left",
Direction::Right => "pacman/right",
};
let moving_tiles = vec![
SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_a.png"))
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_a.png"))))?,
SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png"))
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?,
SpriteAtlas::get_tile(atlas, "pacman/full.png")
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?,
];
// for direction in Direction::DIRECTIONS {
// let moving_prefix = match direction {
// Direction::Up => "pacman/up",
// Direction::Down => "pacman/down",
// Direction::Left => "pacman/left",
// Direction::Right => "pacman/right",
// };
// let moving_tiles = vec![
// SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_a.png"))
// .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_a.png"))))?,
// SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png"))
// .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?,
// SpriteAtlas::get_tile(atlas, "pacman/full.png")
// .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?,
// ];
let stopped_tiles = vec![SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png"))
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?];
// let stopped_tiles = vec![SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png"))
// .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?];
textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.08)?);
stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?);
}
// textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.08)?);
// stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?);
// }
Ok(Self {
traverser: Traverser::new(graph, start_node, Direction::Left, &can_pacman_traverse),
texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
})
}
// Ok(Self {
// traverser: Traverser::new(graph, start_node, Direction::Left, &can_pacman_traverse),
// texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
// })
// }
// }
/// Handles keyboard input to change Pac-Man's direction.
///
/// Maps arrow keys to directions and queues the direction change
/// for the next valid intersection.
pub fn handle_key(&mut self, keycode: Keycode) {
let direction = match keycode {
Keycode::Up => Some(Direction::Up),
Keycode::Down => Some(Direction::Down),
Keycode::Left => Some(Direction::Left),
Keycode::Right => Some(Direction::Right),
_ => None,
};
if let Some(direction) = direction {
self.traverser.set_next_direction(direction);
}
}
}
impl Collidable for Pacman {
fn position(&self) -> crate::entity::traversal::Position {
self.traverser.position
}
}
// impl Collidable for Pacman {
// fn position(&self) -> crate::entity::traversal::Position {
// self.traverser.position
// }
// }

Some files were not shown because too many files have changed in this diff Show More