Compare commits

...

54 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
147 changed files with 5603 additions and 3268 deletions

View File

@@ -48,9 +48,33 @@ jobs:
run: | run: |
just coverage 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 - name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2 env:
with: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
files: ./lcov.info run: |
format: lcov if [ ! -f "lcov.info" ]; then
allow-empty: false 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/ target/
dist/ dist/
emsdk/ emsdk/
.idea
rust-sdl2-emscripten/ # Site build f iles
assets/site/build.css
tailwindcss-* tailwindcss-*
assets/site/build.css
# Coverage reports
lcov.info 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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-error = "0.2.0"
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]} tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
lazy_static = "1.5.0" 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"] } rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
pathfinding = "4.14" pathfinding = "4.14"
once_cell = "1.21.3" once_cell = "1.21.3"
thiserror = "2.0" thiserror = "2.0.14"
anyhow = "1.0" anyhow = "1.0"
glam = { version = "0.30.5", features = [] } glam = "0.30.5"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142" serde_json = "1.0.142"
smallvec = "1.15.1" smallvec = "1.15.1"
strum = "0.27.2" strum = "0.27.2"
strum_macros = "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] [profile.release]
lto = true lto = true
@@ -57,3 +66,8 @@ aarch64-apple-darwin = { triplet = "arm64-osx" }
[target.'cfg(target_os = "emscripten")'.dependencies] [target.'cfg(target_os = "emscripten")'.dependencies]
libc = "0.2.175" libc = "0.2.175"
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
phf = { version = "0.12.1", features = ["macros"] }

View File

@@ -1,17 +1,32 @@
set shell := ["bash", "-c"] set shell := ["bash", "-c"]
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
coverage_exclude_pattern := "app.rs|audio.rs" # 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) # Display report (for humans)
report-coverage: coverage report-coverage: coverage
cargo llvm-cov report \ cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" --ignore-filename-regex "{{ coverage_exclude_pattern }}"
# Run & generate report (for CI) # Run & generate report (for CI)
coverage: coverage:
cargo llvm-cov \ cargo llvm-cov \
--lcov \ --lcov \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \ --ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--output-path lcov.info \ --output-path lcov.info \
--profile coverage \ --profile coverage \

View File

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

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

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

View File

@@ -12,8 +12,6 @@ pub enum Asset {
Wav3, Wav3,
Wav4, Wav4,
Atlas, Atlas,
AtlasJson,
// Add more as needed
} }
impl Asset { impl Asset {
@@ -26,7 +24,6 @@ impl Asset {
Wav3 => "sound/waka/3.ogg", Wav3 => "sound/waka/3.ogg",
Wav4 => "sound/waka/4.ogg", Wav4 => "sound/waka/4.ogg",
Atlas => "atlas.png", Atlas => "atlas.png",
AtlasJson => "atlas.json",
} }
} }
} }
@@ -36,6 +33,7 @@ mod imp {
use crate::error::AssetError; use crate::error::AssetError;
use crate::platform::get_platform; 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> { pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
get_platform().get_asset_bytes(asset) 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); 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. /// 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); 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. /// The size of the canvas, in pixels.
pub const CANVAS_SIZE: UVec2 = UVec2::new( pub const CANVAS_SIZE: UVec2 = UVec2::new(
(BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE, (BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,114 +1,114 @@
//! Entity trait for common movement and rendering functionality. // //! Entity trait for common movement and rendering functionality.
//! // //!
//! This module defines a trait that captures the shared behavior between // //! This module defines a trait that captures the shared behavior between
//! different game entities like Ghosts and Pac-Man, including movement, // //! different game entities like Ghosts and Pac-Man, including movement,
//! rendering, and position calculations. // //! rendering, and position calculations.
use glam::Vec2; // use glam::Vec2;
use sdl2::render::{Canvas, RenderTarget}; // use sdl2::render::{Canvas, RenderTarget};
use crate::entity::direction::Direction; // use crate::entity::direction::Direction;
use crate::entity::graph::{Edge, Graph, NodeId}; // use crate::entity::graph::{Edge, Graph, NodeId};
use crate::entity::traversal::{Position, Traverser}; // use crate::entity::traversal::{Position, Traverser};
use crate::error::{EntityError, GameError, GameResult, TextureError}; // use crate::error::{EntityError, GameError, GameResult, TextureError};
use crate::texture::directional::DirectionalAnimatedTexture; // use crate::texture::directional::DirectionalAnimatedTexture;
use crate::texture::sprite::SpriteAtlas; // use crate::texture::sprite::SpriteAtlas;
/// Trait defining common functionality for game entities that move through the graph. // /// Trait defining common functionality for game entities that move through the graph.
/// // ///
/// This trait provides a unified interface for entities that: // /// This trait provides a unified interface for entities that:
/// - Move through the game graph using a traverser // /// - Move through the game graph using a traverser
/// - Render using directional animated textures // /// - Render using directional animated textures
/// - Have position calculations and movement speed // /// - Have position calculations and movement speed
#[allow(dead_code)] // #[allow(dead_code)]
pub trait Entity { // pub trait Entity {
/// Returns a reference to the entity's traverser for movement control. // /// Returns a reference to the entity's traverser for movement control.
fn traverser(&self) -> &Traverser; // fn traverser(&self) -> &Traverser;
/// Returns a mutable reference to the entity's traverser for movement control. // /// Returns a mutable reference to the entity's traverser for movement control.
fn traverser_mut(&mut self) -> &mut Traverser; // fn traverser_mut(&mut self) -> &mut Traverser;
/// Returns a reference to the entity's directional animated texture. // /// Returns a reference to the entity's directional animated texture.
fn texture(&self) -> &DirectionalAnimatedTexture; // fn texture(&self) -> &DirectionalAnimatedTexture;
/// Returns a mutable reference to the entity's directional animated texture. // /// Returns a mutable reference to the entity's directional animated texture.
fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture; // fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture;
/// Returns the movement speed multiplier for this entity. // /// Returns the movement speed multiplier for this entity.
fn speed(&self) -> f32; // fn speed(&self) -> f32;
/// Determines if this entity can traverse a given edge. // /// Determines if this entity can traverse a given edge.
fn can_traverse(&self, edge: Edge) -> bool; // fn can_traverse(&self, edge: Edge) -> bool;
/// Updates the entity's position and animation state. // /// Updates the entity's position and animation state.
/// // ///
/// This method advances movement through the graph and updates texture animation. // /// This method advances movement through the graph and updates texture animation.
fn tick(&mut self, dt: f32, graph: &Graph); // fn tick(&mut self, dt: f32, graph: &Graph);
/// Calculates the current pixel position in the game world. // /// Calculates the current pixel position in the game world.
/// // ///
/// Converts the graph position to screen coordinates, accounting for // /// Converts the graph position to screen coordinates, accounting for
/// the board offset and centering the sprite. // /// the board offset and centering the sprite.
fn get_pixel_pos(&self, graph: &Graph) -> GameResult<Vec2> { // fn get_pixel_pos(&self, graph: &Graph) -> GameResult<Vec2> {
let pos = match self.traverser().position { // let pos = match self.traverser().position {
Position::AtNode(node_id) => { // Position::AtNode(node_id) => {
let node = graph.get_node(node_id).ok_or(EntityError::NodeNotFound(node_id))?; // let node = graph.get_node(node_id).ok_or(EntityError::NodeNotFound(node_id))?;
node.position // node.position
} // }
Position::BetweenNodes { from, to, traversed } => { // Position::BetweenNodes { from, to, traversed } => {
let from_node = graph.get_node(from).ok_or(EntityError::NodeNotFound(from))?; // let from_node = graph.get_node(from).ok_or(EntityError::NodeNotFound(from))?;
let to_node = graph.get_node(to).ok_or(EntityError::NodeNotFound(to))?; // let to_node = graph.get_node(to).ok_or(EntityError::NodeNotFound(to))?;
let edge = graph.find_edge(from, to).ok_or(EntityError::EdgeNotFound { from, to })?; // let edge = graph.find_edge(from, to).ok_or(EntityError::EdgeNotFound { from, to })?;
from_node.position + (to_node.position - from_node.position) * (traversed / edge.distance) // from_node.position + (to_node.position - from_node.position) * (traversed / edge.distance)
} // }
}; // };
Ok(Vec2::new( // Ok(Vec2::new(
pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32, // pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32,
pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32, // pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32,
)) // ))
} // }
/// Returns the current node ID that the entity is at or moving towards. // /// Returns the current node ID that the entity is at or moving towards.
/// // ///
/// If the entity is at a node, returns that node ID. // /// If the entity is at a node, returns that node ID.
/// If the entity is between nodes, returns the node it's moving towards. // /// If the entity is between nodes, returns the node it's moving towards.
fn current_node_id(&self) -> NodeId { // fn current_node_id(&self) -> NodeId {
match self.traverser().position { // match self.traverser().position {
Position::AtNode(node_id) => node_id, // Position::AtNode(node_id) => node_id,
Position::BetweenNodes { to, .. } => to, // Position::BetweenNodes { to, .. } => to,
} // }
} // }
/// Sets the next direction for the entity to take. // /// Sets the next direction for the entity to take.
/// // ///
/// The direction is buffered and will be applied at the next opportunity, // /// The direction is buffered and will be applied at the next opportunity,
/// typically when the entity reaches a new node. // /// typically when the entity reaches a new node.
fn set_next_direction(&mut self, direction: Direction) { // fn set_next_direction(&mut self, direction: Direction) {
self.traverser_mut().set_next_direction(direction); // self.traverser_mut().set_next_direction(direction);
} // }
/// Renders the entity at its current position. // /// Renders the entity at its current position.
/// // ///
/// Draws the appropriate directional sprite based on the entity's // /// Draws the appropriate directional sprite based on the entity's
/// current movement state and direction. // /// current movement state and direction.
fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> GameResult<()> { // fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> GameResult<()> {
let pixel_pos = self.get_pixel_pos(graph)?; // let pixel_pos = self.get_pixel_pos(graph)?;
let dest = crate::helpers::centered_with_size( // let dest = crate::helpers::centered_with_size(
glam::IVec2::new(pixel_pos.x as i32, pixel_pos.y as i32), // glam::IVec2::new(pixel_pos.x as i32, pixel_pos.y as i32),
glam::UVec2::new(16, 16), // glam::UVec2::new(16, 16),
); // );
if self.traverser().position.is_stopped() { // if self.traverser().position.is_stopped() {
self.texture() // self.texture()
.render_stopped(canvas, atlas, dest, self.traverser().direction) // .render_stopped(canvas, atlas, dest, self.traverser().direction)
.map_err(|e| GameError::Texture(TextureError::RenderFailed(e.to_string())))?; // .map_err(|e| GameError::Texture(TextureError::RenderFailed(e.to_string())))?;
} else { // } else {
self.texture() // self.texture()
.render(canvas, atlas, dest, self.traverser().direction) // .render(canvas, atlas, dest, self.traverser().direction)
.map_err(|e| GameError::Texture(TextureError::RenderFailed(e.to_string())))?; // .map_err(|e| GameError::Texture(TextureError::RenderFailed(e.to_string())))?;
} // }
Ok(()) // Ok(())
} // }
} // }

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