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
160 changed files with 3688 additions and 5854 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

850
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,23 +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"
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
@@ -65,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,10 +1,12 @@
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 sdl2::EventPump;
use tracing::{error, event};
use crate::error::{GameError, GameResult}; use crate::error::{GameError, GameResult};
@@ -12,28 +14,26 @@ 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;
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",
@@ -45,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,
}) })
} }
@@ -74,46 +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
.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, 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,16 +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,
AtlasImage, Atlas,
Font, AtlasJson,
// Add more as needed
} }
impl Asset { impl Asset {
@@ -24,18 +36,16 @@ impl Asset {
Wav2 => "sound/waka/2.ogg", Wav2 => "sound/waka/2.ogg",
Wav3 => "sound/waka/3.ogg", Wav3 => "sound/waka/3.ogg",
Wav4 => "sound/waka/4.ogg", Wav4 => "sound/waka/4.ogg",
AtlasImage => "atlas.png", Atlas => "atlas.png",
Font => "TerminalVector.ttf", 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,
} }

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