Compare commits

...

33 Commits

Author SHA1 Message Date
b270318640 feat: directional rendering, interactivity 2025-08-14 15:44:07 -05:00
bc759f1ed4 refactor!: begin switching to bevy ECS, all tests broken, all systems broken 2025-08-14 15:06:56 -05:00
2f1ff85d8f refactor: handle pausing within game, reduce input system allocations 2025-08-14 10:36:39 -05:00
b7429cd9ec chore: solve tests/ clippy warnings 2025-08-14 09:46:10 -05:00
12a63374a8 feat: avoid using spin sleep unless focused 2025-08-13 23:30:07 -05:00
d80d7061e7 refactor: build decoupled input processing & add event queue system 2025-08-13 20:45:56 -05:00
abdefe0af0 chore: add hidden note about why Coveralls.io is disappointing today 2025-08-13 19:52:58 -05:00
4f76de7c9f feat: enable vsync & hardware acceleration 2025-08-13 19:49:02 -05:00
db8cd6220a feat: cache dynamicly rendered map texture 2025-08-13 19:48:50 -05:00
ced4e87d41 feat: embed atlas.json via phf instead of runtime parsing 2025-08-13 00:37:37 -05:00
09e3d85821 feat!: dynamic map rendering from tiles 2025-08-13 00:25:34 -05:00
c1e421bbbb test: new graph tests 2025-08-12 19:58:37 -05:00
3a9381a56c chore: use NodeId explicitly in collision.rs types 2025-08-12 19:58:11 -05:00
90bdfbd2ae chore: remove emscripten.rs platform from coverage, add html generation task, hide absolute path with remap-path-prefix, organize gitignore 2025-08-12 19:57:52 -05:00
a230d15ffc test: setup common submodule, add text.rs tests, pattern exclude error.rs 2025-08-12 19:24:06 -05:00
60bbd1f5d6 ci: add retry mechanism for coverage reporting via Coveralls CLI 2025-08-12 18:31:07 -05:00
43ce8a4e01 ci: use justfile for coverage, separate report/generate coverage tasks 2025-08-12 18:00:57 -05:00
1529a64588 test: add asset path validity tests 2025-08-12 17:24:12 -05:00
be5eec64c9 Add justfile for handling multiple coverage steps, prevent early termination of coverage job 2025-08-12 17:24:12 -05:00
780a33f657 test: add coverage job to bacon.toml, coverage profile for nextest 2025-08-12 16:48:01 -05:00
c1c5dae6f2 refactor: restructure game logic and state management into separate modules
- Moved game logic from `game.rs` to `game/mod.rs` and `game/state.rs` for better organization.
- Updated `App` to utilize the new `Game` struct and its state management.
- Refactored error handling
- Removed unused audio subsystem references
2025-08-12 14:40:48 -05:00
c489f32908 fix: audio and other subsystems being dropped in App::new(), use Box::leak to ensure static ownership 2025-08-12 13:08:08 -05:00
b91f70cf2f ci: add concurrency group to 'wasm' job to prevent concurrent page deployments 2025-08-12 11:56:03 -05:00
24a207be01 chore: use steps.$.outputs in build workflow, document 1.86.0 toolchain version 2025-08-12 11:41:29 -05:00
44e31d9b21 chore: sync lockfile, add lcov.info to .gitignore 2025-08-12 10:31:10 -05:00
dependabot[bot]
b67234765a chore(deps): bump actions/checkout from 4 to 5 in the dependencies group (#1)
Bumps the dependencies group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 5
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 09:27:54 -05:00
dependabot[bot]
d07498c30e chore(deps): bump the dependencies group with 5 updates (#2)
Bumps the dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [thiserror](https://github.com/dtolnay/thiserror) | `1.0.69` | `2.0.12` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.98` | `1.0.99` |
| [glam](https://github.com/bitshifter/glam-rs) | `0.30.4` | `0.30.5` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.141` | `1.0.142` |
| [libc](https://github.com/rust-lang/libc) | `0.2.174` | `0.2.175` |


Updates `thiserror` from 1.0.69 to 2.0.12
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.69...2.0.12)

Updates `anyhow` from 1.0.98 to 1.0.99
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.98...1.0.99)

Updates `glam` from 0.30.4 to 0.30.5
- [Changelog](https://github.com/bitshifter/glam-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitshifter/glam-rs/compare/0.30.4...0.30.5)

Updates `serde_json` from 1.0.141 to 1.0.142
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.141...v1.0.142)

Updates `libc` from 0.2.174 to 0.2.175
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.175/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.174...0.2.175)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.12
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: dependencies
- dependency-name: anyhow
  dependency-version: 1.0.99
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: glam
  dependency-version: 0.30.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: serde_json
  dependency-version: 1.0.142
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: libc
  dependency-version: 0.2.175
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Xevion <xevion@xevion.dev>
2025-08-12 09:26:46 -05:00
183a432116 test: add tests for collision, items, directional, sprite
enum macros for FruitKind
2025-08-12 09:18:53 -05:00
ead1466b2d chore: specify 'llvm-tools-preview' toolchain component for coverage in toolchain file 2025-08-12 00:22:27 -05:00
8ef09a4e3e test: drop minimal_test_board, use RAW_BOARD constant, item generation tests 2025-08-11 23:26:28 -05:00
33672d8d5a feat: implement collision detection system for entities 2025-08-11 23:24:23 -05:00
1dc8aca373 feat: item collection & collisions, pellet & energizer generation 2025-08-11 22:45:36 -05:00
02089a78da chore: downgrade toolchain to 1.86 on all versions
This is just because managing both 1.86 and 1.88 is really annoying, so
it's better to just be unified. There's no real point to using 1.88
besides more clippy warnings, which are already impeding my work right
now. So we're downgrading.
2025-08-11 22:10:41 -05:00
143 changed files with 4734 additions and 2414 deletions

View File

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

View File

@@ -14,23 +14,23 @@ jobs:
- os: ubuntu-latest - os: ubuntu-latest
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
artifact_name: pacman artifact_name: pacman
toolchain: 1.88.0 toolchain: 1.86.0
- os: macos-13 - os: macos-13
target: x86_64-apple-darwin target: x86_64-apple-darwin
artifact_name: pacman artifact_name: pacman
toolchain: 1.88.0 toolchain: 1.86.0
- os: macos-latest - os: macos-latest
target: aarch64-apple-darwin target: aarch64-apple-darwin
artifact_name: pacman artifact_name: pacman
toolchain: 1.88.0 toolchain: 1.86.0
- os: windows-latest - os: windows-latest
target: x86_64-pc-windows-gnu target: x86_64-pc-windows-gnu
artifact_name: pacman.exe artifact_name: pacman.exe
toolchain: 1.88.0 toolchain: 1.86.0
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Setup Rust Toolchain - name: Setup Rust Toolchain
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
@@ -64,15 +64,16 @@ jobs:
run: cargo build --release run: cargo build --release
- name: Acquire Package Version - name: Acquire Package Version
shell: bash id: get_version
shell: bash # required to prevent Windows runners from failing
run: | run: |
PACKAGE_VERSION=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r) set -euo pipefail # exit on error
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV echo "version=$(cargo metadata --format-version 1 --no-deps | jq '.packages[0].version' -r)" >> $GITHUB_OUTPUT
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ matrix.target }}" name: "pacman-${{ steps.get_version.outputs.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
@@ -83,10 +84,13 @@ 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@v4 uses: actions/checkout@v5
- name: Setup Emscripten SDK - name: Setup Emscripten SDK
uses: pyodide/setup-emsdk@v15 uses: pyodide/setup-emsdk@v15
@@ -98,7 +102,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 # we are unfortunately pinned to 1.86.0 for some reason, bulk-memory-opt related issues toolchain: 1.86.0
- 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@v4 uses: actions/checkout@v5
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master
@@ -42,15 +42,39 @@ 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: |
cargo llvm-cov --no-fail-fast --lcov --output-path lcov.info nextest just coverage
- name: Download Coveralls CLI
run: |
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
curl -L https://github.com/coverallsapp/coverage-reporter/releases/download/v0.6.15/coveralls-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin
- name: Upload coverage to Coveralls - 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

View File

@@ -4,7 +4,7 @@ on: ["push", "pull_request"]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
RUST_TOOLCHAIN: 1.88.0 RUST_TOOLCHAIN: 1.86.0
jobs: jobs:
test: test:
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Install Rust toolchain - name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master uses: dtolnay/rust-toolchain@master

16
.gitignore vendored
View File

@@ -1,7 +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
coverage.html

839
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -15,12 +15,16 @@ 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 = "1.0" thiserror = "2.0"
anyhow = "1.0" anyhow = "1.0"
glam = { version = "0.30.4", features = [] } glam = { version = "0.30.5", features = [] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.141" serde_json = "1.0.142"
smallvec = "1.15.1" smallvec = "1.15.1"
strum = "0.27.2"
strum_macros = "0.27.2"
phf = { version = "0.11", features = ["macros"] }
bevy_ecs = "0.16.1"
[profile.release] [profile.release]
lto = true lto = true
@@ -54,4 +58,9 @@ 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.16" libc = "0.2.175"
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
phf = { version = "0.11", features = ["macros"] }

33
Justfile Normal file
View File

@@ -0,0 +1,33 @@
set shell := ["bash", "-c"]
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
# You can use src\\\\..., but the filename alone is acceptable too
coverage_exclude_pattern := "src\\\\app.rs|audio.rs|src\\\\error.rs|platform\\\\emscripten.rs"
# !!! --ignore-filename-regex should be used on both reports & coverage testing
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
# Generate HTML report (for humans, source line inspection)
html: coverage
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--html \
--open
# Display report (for humans)
report-coverage: coverage
cargo llvm-cov report \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
# Run & generate report (for CI)
coverage:
cargo llvm-cov \
--lcov \
--remap-path-prefix \
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
--output-path lcov.info \
--profile coverage \
--no-fail-fast nextest

View File

@@ -1,6 +1,6 @@
# Pac-Man # 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
@@ -72,6 +72,8 @@ 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: 23 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -34,6 +34,30 @@ 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
@@ -59,3 +83,4 @@ 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"

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");
}

4
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,4 @@
[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"
components = ["rustfmt", "llvm-tools-preview", "clippy"]

View File

@@ -1,12 +1,11 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use glam::Vec2; use glam::Vec2;
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::Keycode;
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator}; use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::{Window, WindowContext}; use sdl2::video::{Window, WindowContext};
use sdl2::EventPump; use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
use tracing::{error, event}; use tracing::{error, warn};
use crate::error::{GameError, GameResult}; use crate::error::{GameError, GameResult};
@@ -14,26 +13,28 @@ 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<'a> { pub struct App {
game: Game, pub 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,35 +46,33 @@ 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 mut canvas = Box::leak(Box::new(
window
.into_canvas()
.accelerated()
.present_vsync()
.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: &'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_static, &ttf_context, &audio_subsystem)?; 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_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 // Initial draw
game.draw(&mut canvas, &mut backbuffer) // game.draw(&mut canvas, &mut backbuffer)
.map_err(|e| GameError::Sdl(e.to_string()))?; // .map_err(|e| GameError::Sdl(e.to_string()))?;
game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO) // game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO)
.map_err(|e| GameError::Sdl(e.to_string()))?; // .map_err(|e| GameError::Sdl(e.to_string()))?;
Ok(Self { Ok(App {
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,
}) })
@@ -83,78 +82,51 @@ 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.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();
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);
}
} }
// if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
// error!("Failed to draw game: {}", e);
// }
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 { } else {
event!( warn!("Game loop behind schedule by: {:?}", start.elapsed() - LOOP_TIME);
tracing::Level::WARN,
"Game loop behind schedule by: {:?}",
start.elapsed() - LOOP_TIME
);
} }
true true

View File

@@ -3,28 +3,15 @@
//! 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 std::io; use strum_macros::EnumIter;
use thiserror::Error;
#[derive(Error, Debug)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
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 {
@@ -37,15 +24,16 @@ 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,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,

44
src/ecs/interact.rs Normal file
View File

@@ -0,0 +1,44 @@
use bevy_ecs::{
event::{EventReader, EventWriter},
query::With,
system::{Query, ResMut},
};
use crate::{
ecs::{GlobalState, PlayerControlled, Velocity},
error::GameError,
game::events::GameEvent,
input::commands::GameCommand,
};
// Handles
pub fn interact_system(
mut events: EventReader<GameEvent>,
mut state: ResMut<GlobalState>,
mut players: Query<(&PlayerControlled, &mut Velocity)>,
mut errors: EventWriter<GameError>,
) {
// Get the player's velocity (handling to ensure there is only one player)
let mut velocity = match players.single_mut() {
Ok((_, velocity)) => velocity,
Err(e) => {
errors.write(GameError::InvalidState(format!("Player not found: {}", e)).into());
return;
}
};
// Handle events
for event in events.read() {
match event {
GameEvent::Command(command) => match command {
GameCommand::MovePlayer(direction) => {
velocity.direction = *direction;
}
GameCommand::Exit => {
state.exit = true;
}
_ => {}
},
}
}
}

149
src/ecs/mod.rs Normal file
View File

@@ -0,0 +1,149 @@
//! The Entity-Component-System (ECS) module.
//!
//! This module contains all the ECS-related logic, including components, systems,
//! and resources.
use bevy_ecs::{bundle::Bundle, component::Component, resource::Resource};
use glam::Vec2;
use crate::{
entity::{direction::Direction, graph::Graph, traversal},
error::{EntityError, GameResult},
texture::{
animated::AnimatedTexture,
directional::DirectionalAnimatedTexture,
sprite::{AtlasTile, Sprite},
},
};
/// A tag component for entities that are controlled by the player.
#[derive(Default, Component)]
pub struct PlayerControlled;
/// A component for entities that have a sprite, with a layer for ordering.
///
/// This is intended to be modified by other entities allowing animation.
#[derive(Component)]
pub struct Renderable {
pub sprite: AtlasTile,
pub layer: u8,
}
/// A component for entities that have a directional animated texture.
#[derive(Component)]
pub struct DirectionalAnimated {
pub textures: [Option<AnimatedTexture>; 4],
pub stopped_textures: [Option<AnimatedTexture>; 4],
}
/// A unique identifier for a node, represented by its index in the graph's storage.
pub type NodeId = usize;
/// Represents the current position of an entity traversing the graph.
///
/// This enum allows for precise tracking of whether an entity is exactly at a node
/// or moving along an edge between two nodes.
#[derive(Component, Debug, Copy, Clone, PartialEq)]
pub enum Position {
/// The traverser is located exactly at a node.
AtNode(NodeId),
/// The traverser is on an edge between two nodes.
BetweenNodes {
from: NodeId,
to: NodeId,
/// The floating-point distance traversed along the edge from the `from` node.
traversed: f32,
},
}
impl Position {
/// Calculates the current pixel position in the game world.
///
/// Converts the graph position to screen coordinates, accounting for
/// the board offset and centering the sprite.
pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult<Vec2> {
let pos = match self {
Position::AtNode(node_id) => {
let node = graph.get_node(*node_id).ok_or(EntityError::NodeNotFound(*node_id))?;
node.position
}
Position::BetweenNodes { from, to, traversed } => {
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 edge = graph
.find_edge(*from, *to)
.ok_or(EntityError::EdgeNotFound { from: *from, to: *to })?;
from_node.position + (to_node.position - from_node.position) * (traversed / edge.distance)
}
};
Ok(Vec2::new(
pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32,
pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32,
))
}
}
impl Default for Position {
fn default() -> Self {
Position::AtNode(0)
}
}
#[allow(dead_code)]
impl Position {
/// Returns `true` if the position is exactly at a node.
pub fn is_at_node(&self) -> bool {
matches!(self, Position::AtNode(_))
}
/// Returns the `NodeId` of the current or most recently departed node.
#[allow(clippy::wrong_self_convention)]
pub fn from_node_id(&self) -> NodeId {
match self {
Position::AtNode(id) => *id,
Position::BetweenNodes { from, .. } => *from,
}
}
/// Returns the `NodeId` of the destination node, if currently on an edge.
#[allow(clippy::wrong_self_convention)]
pub fn to_node_id(&self) -> Option<NodeId> {
match self {
Position::AtNode(_) => None,
Position::BetweenNodes { to, .. } => Some(*to),
}
}
/// Returns `true` if the traverser is stopped at a node.
pub fn is_stopped(&self) -> bool {
matches!(self, Position::AtNode(_))
}
}
/// A component for entities that have a velocity, with a direction and speed.
#[derive(Default, Component)]
pub struct Velocity {
pub direction: Direction,
pub speed: Option<f32>,
}
#[derive(Bundle)]
pub struct PlayerBundle {
pub player: PlayerControlled,
pub position: Position,
pub velocity: Velocity,
pub sprite: Renderable,
pub directional_animated: DirectionalAnimated,
}
#[derive(Resource)]
pub struct GlobalState {
pub exit: bool,
}
#[derive(Resource)]
pub struct DeltaTime(pub f32);
pub mod interact;
pub mod render;

95
src/ecs/render.rs Normal file
View File

@@ -0,0 +1,95 @@
use crate::ecs::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity};
use crate::error::{EntityError, GameError, TextureError};
use crate::map::builder::Map;
use crate::texture::sprite::SpriteAtlas;
use bevy_ecs::entity::Entity;
use bevy_ecs::event::EventWriter;
use bevy_ecs::system::{NonSendMut, Query, Res};
use sdl2::render::{Canvas, Texture};
use sdl2::video::Window;
/// Updates the directional animated texture of an entity.
pub fn directional_render_system(
dt: Res<DeltaTime>,
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable)>,
mut errors: EventWriter<GameError>,
) {
for (velocity, mut texture, mut renderable) in renderables.iter_mut() {
let texture = if velocity.speed.is_none() {
texture.stopped_textures[velocity.direction.as_usize()].as_mut()
} else {
texture.textures[velocity.direction.as_usize()].as_mut()
};
if let Some(texture) = texture {
texture.tick(dt.0);
renderable.sprite = *texture.current_tile();
} else {
errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into());
continue;
}
}
}
pub struct MapTextureResource(pub Texture<'static>);
pub struct BackbufferResource(pub Texture<'static>);
pub fn render_system(
mut canvas: NonSendMut<&mut Canvas<Window>>,
map_texture: NonSendMut<MapTextureResource>,
mut backbuffer: NonSendMut<BackbufferResource>,
mut atlas: NonSendMut<SpriteAtlas>,
map: Res<Map>,
mut renderables: Query<(Entity, &mut Renderable, &Position)>,
mut errors: EventWriter<GameError>,
) {
// Clear the main canvas first
canvas.set_draw_color(sdl2::pixels::Color::BLACK);
canvas.clear();
// Render to backbuffer
canvas
.with_texture_canvas(&mut backbuffer.0, |backbuffer_canvas| {
// Clear the backbuffer
backbuffer_canvas.set_draw_color(sdl2::pixels::Color::BLACK);
backbuffer_canvas.clear();
// Copy the pre-rendered map texture to the backbuffer
backbuffer_canvas
.copy(&map_texture.0, None, None)
.err()
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
// Render all entities to the backbuffer
for (_, mut renderable, position) in renderables.iter_mut() {
let pos = position.get_pixel_pos(&map.graph);
match pos {
Ok(pos) => {
let dest = crate::helpers::centered_with_size(
glam::IVec2::new(pos.x as i32, pos.y as i32),
glam::UVec2::new(renderable.sprite.size.x as u32, renderable.sprite.size.y as u32),
);
renderable
.sprite
.render(backbuffer_canvas, &mut atlas, dest)
.err()
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
}
Err(e) => {
errors.write(e.into());
}
}
}
})
.err()
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
// Copy backbuffer to main canvas and present
canvas
.copy(&backbuffer.0, None, None)
.err()
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
canvas.present();
}

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