Compare commits

..

1 Commits

Author SHA1 Message Date
68cbbd56f2 test: add tests for collision, items, directional, sprite
enum macros for FruitKind
2025-08-12 00:44:19 -05:00
150 changed files with 3490 additions and 4702 deletions

View File

@@ -1,5 +1,2 @@
[profile.default] [profile.default]
fail-fast = false fail-fast = false
[profile.coverage]
status-level = "none"

View File

@@ -30,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Setup Rust Toolchain - name: Setup Rust Toolchain
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
@@ -64,16 +64,15 @@ jobs:
run: cargo build --release run: cargo build --release
- name: Acquire Package Version - name: Acquire Package Version
id: get_version shell: bash
shell: bash # required to prevent Windows runners from failing
run: | run: |
set -euo pipefail # exit on error PACKAGE_VERSION=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r)
echo "version=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r)" >> $GITHUB_OUTPUT echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: "pacman-${{ steps.get_version.outputs.version }}-${{ matrix.target }}" name: "pacman-${{ env.PACKAGE_VERSION }}-${{ matrix.target }}"
path: ./target/release/${{ matrix.artifact_name }} path: ./target/release/${{ matrix.artifact_name }}
retention-days: 7 retention-days: 7
if-no-files-found: error if-no-files-found: error
@@ -84,13 +83,10 @@ jobs:
permissions: permissions:
pages: write pages: write
id-token: write id-token: write
# concurrency group is used to prevent multiple page deployments from being attempted at the same time
concurrency:
group: ${{ github.workflow }}-wasm
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Setup Emscripten SDK - name: Setup Emscripten SDK
uses: pyodide/setup-emsdk@v15 uses: pyodide/setup-emsdk@v15
@@ -102,7 +98,7 @@ jobs:
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
with: with:
target: wasm32-unknown-emscripten target: wasm32-unknown-emscripten
toolchain: 1.86.0 toolchain: 1.86.0 # we are unfortunately pinned to 1.86.0 for some reason, bulk-memory-opt related issues
- name: Rust Cache - name: Rust Cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
@@ -42,39 +42,15 @@ jobs:
- uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest - 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 - name: Generate coverage report
run: | run: |
just coverage cargo llvm-cov --no-fail-fast --lcov --output-path lcov.info nextest
- 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
env: uses: coverallsapp/github-action@v2
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} with:
run: | files: ./lcov.info
if [ ! -f "lcov.info" ]; then format: lcov
echo "Error: lcov.info file not found. Coverage generation may have failed." allow-empty: false
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

View File

@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master

16
.gitignore vendored
View File

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

805
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.41", features = ["max_level_debug", "release_max_level_debug"]} tracing = { version = "0.1.40", 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,18 +15,14 @@ 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.14" thiserror = "1.0"
anyhow = "1.0" anyhow = "1.0"
glam = "0.30.5" glam = { version = "0.30.4", features = [] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142" serde_json = "1.0.141"
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"
[profile.release] [profile.release]
lto = true lto = true
@@ -60,9 +56,4 @@ x86_64-apple-darwin = { triplet = "x64-osx" }
aarch64-apple-darwin = { triplet = "arm64-osx" } 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.16"
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
phf = { version = "0.12.1", features = ["macros"] }

View File

@@ -1,33 +0,0 @@
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 # Pac-Man
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits] [![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg [badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg [badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
@@ -72,8 +72,6 @@ I wanted to hit a log of goals and features, making it a 'perfect' project that
Since this project is still in progress, I'm only going to cover non-obvious build details. By reading the code, build scripts, and copying the online build workflows, you should be able to replicate the build process. Since this project is still in progress, I'm only going to cover non-obvious build details. By reading the code, build scripts, and copying the online build workflows, you should be able to replicate the build process.
- We use rustc 1.86.0 for the build, due to bulk-memory-opt related issues on wasm32-unknown-emscripten.
- Technically, we could probably use stable or even nightly on desktop targets, but using different versions for different targets is a pain, mainly because of clippy warnings changing between versions.
- Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build` to build the requisite dependencies via vcpkg. - Install `cargo-vcpkg` with `cargo install cargo-vcpkg`, then run `cargo vcpkg build` to build the requisite dependencies via vcpkg.
- For the WASM build, you need to have the Emscripten SDK cloned; you can do so with `git clone https://github.com/emscripten-core/emsdk.git` - For the WASM build, you need to have the Emscripten SDK cloned; you can do so with `git clone https://github.com/emscripten-core/emsdk.git`
- The first time you clone, you'll need to install the appropriate SDK version with `./emsdk install 3.1.43` and then activate it with `./emsdk activate 3.1.43`. On Windows, use `./emsdk/emsdk.ps1` instead. - The first time you clone, you'll need to install the appropriate SDK version with `./emsdk install 3.1.43` and then activate it with `./emsdk activate 3.1.43`. On Windows, use `./emsdk/emsdk.ps1` instead.

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -34,30 +34,6 @@ command = [
need_stdout = true need_stdout = true
analyzer = "nextest" analyzer = "nextest"
[jobs.coverage]
command = [
"just", "report-coverage"
]
need_stdout = true
ignored_lines = [
"info:",
"\\s+Compiling",
"test result: ok",
"^\\s*$",
"running \\d+ test",
"Nextest run ID",
"[─]+",
"test.+ok",
"PASS|START",
"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] [jobs.doc]
command = ["cargo", "doc", "--no-deps"] command = ["cargo", "doc", "--no-deps"]
need_stdout = false need_stdout = false
@@ -83,4 +59,3 @@ c = "job:clippy"
alt-c = "job:check" alt-c = "job:check"
ctrl-alt-c = "job:check-all" ctrl-alt-c = "job:check-all"
shift-c = "job:clippy-all" shift-c = "job:clippy-all"
f = "job:coverage"

View File

@@ -1,50 +0,0 @@
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,4 +1,3 @@
[toolchain] [toolchain]
# we are unfortunately pinned to 1.86.0 for some reason, bulk-memory-opt related issues on wasm32-unknown-emscripten
channel = "1.86.0" channel = "1.86.0"
components = ["rustfmt", "llvm-tools-preview", "clippy"] components = ["rustfmt", "llvm-tools-preview", "clippy"]

View File

@@ -1,41 +1,39 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use glam::Vec2; use glam::Vec2;
use sdl2::render::TextureCreator; use sdl2::event::{Event, WindowEvent};
use sdl2::ttf::Sdl2TtfContext; use sdl2::keyboard::Keycode;
use sdl2::video::WindowContext; use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem}; use sdl2::video::{Window, WindowContext};
use tracing::{field, info, warn}; use sdl2::EventPump;
use tracing::{error, event};
use crate::error::{GameError, GameResult}; use crate::error::{GameError, GameResult};
use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE}; use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
use crate::game::Game; use crate::game::Game;
use crate::platform::get_platform; use crate::platform::get_platform;
use crate::systems::profiling::SystemTimings;
pub struct App { pub struct App<'a> {
pub game: Game, game: Game,
canvas: Canvas<Window>,
event_pump: EventPump,
backbuffer: Texture<'a>,
paused: bool,
last_tick: Instant, last_tick: Instant,
focused: bool, cursor_pos: Vec2,
_cursor_pos: Vec2,
} }
impl App { impl App<'_> {
pub fn new() -> GameResult<Self> { pub fn new() -> GameResult<Self> {
let sdl_context: &'static Sdl = Box::leak(Box::new(sdl2::init().map_err(|e| GameError::Sdl(e.to_string()))?));
let video_subsystem: &'static VideoSubsystem =
Box::leak(Box::new(sdl_context.video().map_err(|e| GameError::Sdl(e.to_string()))?));
let _audio_subsystem: &'static AudioSubsystem =
Box::leak(Box::new(sdl_context.audio().map_err(|e| GameError::Sdl(e.to_string()))?));
let _ttf_context: &'static Sdl2TtfContext =
Box::leak(Box::new(sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?));
let event_pump: &'static mut EventPump =
Box::leak(Box::new(sdl_context.event_pump().map_err(|e| GameError::Sdl(e.to_string()))?));
// Initialize platform-specific console // Initialize platform-specific console
get_platform().init_console()?; get_platform().init_console()?;
let sdl_context = sdl2::init().map_err(|e| GameError::Sdl(e.to_string()))?;
let video_subsystem = sdl_context.video().map_err(|e| GameError::Sdl(e.to_string()))?;
let audio_subsystem = sdl_context.audio().map_err(|e| GameError::Sdl(e.to_string()))?;
let ttf_context = sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?;
let window = video_subsystem let window = video_subsystem
.window( .window(
"Pac-Man", "Pac-Man",
@@ -47,28 +45,37 @@ impl App {
.build() .build()
.map_err(|e| GameError::Sdl(e.to_string()))?; .map_err(|e| GameError::Sdl(e.to_string()))?;
let canvas = Box::leak(Box::new( let mut canvas = window.into_canvas().build().map_err(|e| GameError::Sdl(e.to_string()))?;
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 mut TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator())); let texture_creator_static: &'static TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
let game = Game::new(canvas, texture_creator, event_pump)?; let mut game = Game::new(texture_creator_static, &ttf_context, &audio_subsystem)?;
// game.audio.set_mute(cfg!(debug_assertions)); game.audio.set_mute(cfg!(debug_assertions));
Ok(App { let mut backbuffer = texture_creator_static
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
backbuffer.set_scale_mode(ScaleMode::Nearest);
let event_pump = sdl_context.event_pump().map_err(|e| GameError::Sdl(e.to_string()))?;
// 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,
focused: true, canvas,
event_pump,
backbuffer,
paused: false,
last_tick: Instant::now(), last_tick: Instant::now(),
_cursor_pos: Vec2::ZERO, cursor_pos: Vec2::ZERO,
}) })
} }
@@ -76,57 +83,78 @@ impl App {
{ {
let start = Instant::now(); let start = Instant::now();
// for event in self for event in self.event_pump.poll_iter() {
// .game match event {
// .world Event::Window { win_event, .. } => match win_event {
// .get_non_send_resource_mut::<&'static mut EventPump>() WindowEvent::Hidden => {
// .unwrap() event!(tracing::Level::DEBUG, "Window hidden");
// .poll_iter() }
// { WindowEvent::Shown => {
// match event { event!(tracing::Level::DEBUG, "Window shown");
// 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
// WindowEvent::FocusLost => { #[cfg(not(target_os = "emscripten"))]
// self.focused = false; Event::Quit { .. }
// } | Event::KeyDown {
// _ => {} keycode: Some(Keycode::Escape) | Some(Keycode::Q),
// }, ..
// Event::MouseMotion { x, y, .. } => { } => {
// // Convert window coordinates to logical coordinates event!(tracing::Level::INFO, "Exit requested. Exiting...");
// self.cursor_pos = Vec2::new(x as f32, y as f32); 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.debug_mode = !self.game.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();
let exit = self.game.tick(dt); if !self.paused {
self.game.tick(dt);
if exit { if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
return false; error!("Failed to draw game: {}", e);
} }
if let Err(e) = self
// Show timings if the loop took more than 25% of the loop time .game
let show_timings = start.elapsed() > (LOOP_TIME / 4); .present_backbuffer(&mut self.canvas, &self.backbuffer, self.cursor_pos)
if show_timings || true { {
if let Some(timings) = self.game.world.get_resource::<SystemTimings>() { error!("Failed to present backbuffer: {}", e);
let mut timings = timings.timings.lock();
let total = timings.values().sum::<Duration>();
info!("Total: {:?}, Timings: {:?}", total, field::debug(&timings));
timings.clear();
} }
} }
// 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, self.focused); get_platform().sleep(time);
} }
} else {
event!(
tracing::Level::WARN,
"Game loop behind schedule by: {:?}",
start.elapsed() - LOOP_TIME
);
} }
true true

View File

@@ -3,15 +3,28 @@
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem. //! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
use std::borrow::Cow; use std::borrow::Cow;
use strum_macros::EnumIter; use std::io;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)] #[derive(Error, Debug)]
pub enum AssetError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Asset not found: {0}")]
NotFound(String),
#[error("Invalid asset format: {0}")]
InvalidFormat(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Asset { pub enum Asset {
Wav1, Wav1,
Wav2, Wav2,
Wav3, Wav3,
Wav4, Wav4,
Atlas, Atlas,
AtlasJson,
// Add more as needed
} }
impl Asset { impl Asset {
@@ -24,16 +37,15 @@ 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",
} }
} }
} }
mod imp { mod imp {
use super::*; use super::*;
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,6 +18,8 @@ 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::{graph::NodeId, traversal::Position}; use crate::entity::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<NodeId, Vec<EntityId>>, node_entities: HashMap<usize, 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: NodeId) -> &[EntityId] { pub fn entities_at_node(&self, node: usize) -> &[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<[NodeId; 2]> { fn get_nodes(pos: &Position) -> SmallVec<[usize; 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,13 +1,11 @@
use glam::IVec2; use glam::IVec2;
/// The four cardinal directions. /// The four cardinal directions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[repr(usize)]
pub enum Direction { pub enum Direction {
Up, Up,
Down, Down,
Left, Left,
#[default]
Right, Right,
} }

View File

@@ -1,254 +1,256 @@
// //! 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()] =
// stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?); Some(AnimatedTexture::new(moving_tiles, 0.2).map_err(|e| GameError::Texture(TextureError::Animated(e)))?);
// } stopped_textures[direction.as_usize()] =
Some(AnimatedTexture::new(stopped_tiles, 0.1).map_err(|e| GameError::Texture(TextureError::Animated(e)))?);
}
// 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
// } }
// } }

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