Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ce2af89c8 | |||
| 5f0ee87dd9 | |||
| b88895e82f | |||
| 2f0c734d13 | |||
| e96b3159d7 | |||
| 8c95ecc547 | |||
| 02a98c9f32 | |||
| 7f95c0233e | |||
| a531228b95 | |||
| de86f383bf | |||
| bd811ee783 | |||
| 57d7f75940 | |||
| c5d6ea28e1 | |||
| 730daed20a | |||
| b9bae99a4c | |||
| 2c65048fb0 | |||
| 3388d77ec5 | |||
| 242da2e263 | |||
| 70fb2b9503 | |||
| 0aa056a0ae | |||
| b270318640 | |||
| bc759f1ed4 | |||
| 2f1ff85d8f | |||
| b7429cd9ec | |||
| 12a63374a8 | |||
| d80d7061e7 | |||
| abdefe0af0 | |||
| 4f76de7c9f | |||
| db8cd6220a | |||
| ced4e87d41 | |||
| 09e3d85821 | |||
| c1e421bbbb | |||
| 3a9381a56c | |||
| 90bdfbd2ae | |||
| a230d15ffc | |||
| 60bbd1f5d6 | |||
| 43ce8a4e01 | |||
| 1529a64588 | |||
| be5eec64c9 | |||
| 780a33f657 | |||
| c1c5dae6f2 | |||
| c489f32908 | |||
| b91f70cf2f | |||
| 24a207be01 | |||
| 44e31d9b21 | |||
|
|
b67234765a | ||
|
|
d07498c30e | ||
| 183a432116 | |||
| ead1466b2d | |||
| 8ef09a4e3e | |||
| 33672d8d5a | |||
| 1dc8aca373 | |||
| 02089a78da | |||
| 1f8e7c6d71 | |||
| 27079e127d | |||
| 5e9bb3535e | |||
| 250cf2fc89 | |||
| 57975495a9 | |||
| f3e7a780e2 | |||
| ee6cb0a670 | |||
| b3df34b405 | |||
| dbafa17670 | |||
| d9c8f97903 | |||
| ad2ec35bfb | |||
| 6331ba0b2f | |||
| 3d275b8e85 | |||
| bd61db9aae | |||
| ed8bd07518 | |||
| 27705f1ba2 | |||
| e964adc818 | |||
| c5213320ac | |||
| e0f8443e75 | |||
| 6702b3723a | |||
| f6e7228f75 | |||
| 14cebe4462 | |||
| c39fcaa7d7 | |||
| 1d9499c4f8 | |||
| 61050a5585 | |||
| 85420711df | |||
| 2efa7a4df5 | |||
| 1d018db5e9 | |||
| 023697dcd7 | |||
| 87ee12543e | |||
| b308bc0ef7 | |||
| 9d5ca54234 |
@@ -1,2 +1,5 @@
|
|||||||
[profile.default]
|
[profile.default]
|
||||||
fail-fast = false
|
fail-fast = false
|
||||||
|
|
||||||
|
[profile.coverage]
|
||||||
|
status-level = "none"
|
||||||
|
|||||||
20
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "cargo"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
groups:
|
||||||
|
dependencies:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "monthly"
|
||||||
|
groups:
|
||||||
|
dependencies:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
27
.github/workflows/audit.yaml
vendored
@@ -1,27 +0,0 @@
|
|||||||
name: Audit
|
|
||||||
|
|
||||||
on: ["push", "pull_request"]
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
RUST_TOOLCHAIN: 1.88.0
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
audit:
|
|
||||||
name: Audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
|
||||||
uses: dtolnay/rust-toolchain@master
|
|
||||||
with:
|
|
||||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
|
||||||
|
|
||||||
- name: Install cargo-audit
|
|
||||||
run: cargo install cargo-audit
|
|
||||||
|
|
||||||
- name: Run security audit
|
|
||||||
run: cargo audit
|
|
||||||
48
.github/workflows/build.yaml
vendored
@@ -1,5 +1,4 @@
|
|||||||
name: Builds
|
name: Builds
|
||||||
|
|
||||||
on: ["push", "pull_request"]
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -15,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
|
||||||
@@ -65,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
|
||||||
@@ -84,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
|
||||||
@@ -99,16 +102,15 @@ 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
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Setup Bun
|
||||||
uses: pnpm/action-setup@v3
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
version: 8
|
bun-version: latest
|
||||||
run_install: true
|
|
||||||
|
|
||||||
- name: Build with Emscripten
|
- name: Build with Emscripten
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -121,7 +123,7 @@ jobs:
|
|||||||
echo "Build attempt $attempt of $MAX_RETRIES"
|
echo "Build attempt $attempt of $MAX_RETRIES"
|
||||||
|
|
||||||
# Capture output and check for specific error while preserving real-time output
|
# Capture output and check for specific error while preserving real-time output
|
||||||
if cargo build --target=wasm32-unknown-emscripten --release 2>&1 | tee /tmp/build_output.log; then
|
if bun run -i web.build.ts 2>&1 | tee /tmp/build_output.log; then
|
||||||
echo "Build successful on attempt $attempt"
|
echo "Build successful on attempt $attempt"
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
@@ -148,18 +150,6 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Assemble
|
|
||||||
run: |
|
|
||||||
echo "Generating CSS"
|
|
||||||
pnpx postcss-cli ./assets/site/styles.scss -o ./assets/site/build.css
|
|
||||||
|
|
||||||
echo "Copying WASM files"
|
|
||||||
|
|
||||||
mkdir -p dist
|
|
||||||
cp assets/site/{build.css,favicon.ico,index.html} dist
|
|
||||||
output_folder="target/wasm32-unknown-emscripten/release"
|
|
||||||
cp $output_folder/pacman.{wasm,js} $output_folder/deps/pacman.data dist
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||||
|
|||||||
43
.github/workflows/coverage.yaml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Coverage
|
name: Code Coverage
|
||||||
|
|
||||||
on: ["push", "pull_request"]
|
on: ["push", "pull_request"]
|
||||||
|
|
||||||
@@ -8,11 +8,10 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
coverage:
|
coverage:
|
||||||
name: Code Coverage
|
|
||||||
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
|
||||||
@@ -43,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
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
name: Tests
|
name: Tests & Checks
|
||||||
|
|
||||||
on: ["push", "pull_request"]
|
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:
|
||||||
name: Test
|
|
||||||
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
|
||||||
@@ -52,3 +51,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: cargo fmt -- --check
|
run: cargo fmt -- --check
|
||||||
|
|
||||||
|
- uses: taiki-e/install-action@cargo-audit
|
||||||
|
|
||||||
|
- name: Run security audit
|
||||||
|
run: cargo audit
|
||||||
16
.gitignore
vendored
@@ -1,5 +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
|
||||||
|
tailwindcss-*
|
||||||
|
assets/site/build.css
|
||||||
|
|
||||||
|
# Coverage reports
|
||||||
|
lcov.info
|
||||||
|
coverage.html
|
||||||
|
|||||||
840
Cargo.lock
generated
24
Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing = { version = "0.1.40", features = ["max_level_debug", "release_max_level_debug"]}
|
tracing = { version = "0.1.41", features = ["max_level_debug", "release_max_level_debug"]}
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
|
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
@@ -15,11 +15,20 @@ 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.14"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
glam = { version = "0.30.4", features = [] }
|
glam = "0.30.5"
|
||||||
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"
|
||||||
|
strum = "0.27.2"
|
||||||
|
strum_macros = "0.27.2"
|
||||||
|
phf = { version = "0.12.1", features = ["macros"] }
|
||||||
|
bevy_ecs = "0.16.1"
|
||||||
|
bitflags = "2.9.1"
|
||||||
|
parking_lot = "0.12.3"
|
||||||
|
micromap = "0.1.0"
|
||||||
|
thousands = "0.2.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
@@ -53,4 +62,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.12.1", features = ["macros"] }
|
||||||
|
|||||||
33
Justfile
Normal 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
|
||||||
55
README.md
@@ -1,21 +1,19 @@
|
|||||||
# 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/test.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
|
||||||
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
|
[badge-coverage]: https://coveralls.io/repos/github/Xevion/Pac-Man/badge.svg?branch=master
|
||||||
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
[badge-demo]: https://img.shields.io/github/deployments/Xevion/Pac-Man/github-pages?label=GitHub%20Pages
|
||||||
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
[badge-online-demo]: https://img.shields.io/badge/GitHub%20Pages-Demo-brightgreen
|
||||||
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
[badge-last-commit]: https://img.shields.io/github/last-commit/Xevion/Pac-Man
|
||||||
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
[build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml
|
||||||
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/test.yaml
|
[test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml
|
||||||
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
|
[coverage]: https://coveralls.io/github/Xevion/Pac-Man?branch=master
|
||||||
[demo]: https://xevion.github.io/Pac-Man/
|
[demo]: https://xevion.github.io/Pac-Man/
|
||||||
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
[commits]: https://github.com/Xevion/Pac-Man/commits/master
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
A faithful recreation of the classic Pac-Man arcade game written in Rust. This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
|
A faithful recreation of the classic Pac-Man arcade game written in Rust. This project aims to replicate the original game's mechanics, graphics, sound, and behavior as accurately as possible while providing modern development features like cross-platform compatibility and WebAssembly support.
|
||||||
|
|
||||||
The game includes all the original features you'd expect from Pac-Man:
|
The game includes all the original features you'd expect from Pac-Man:
|
||||||
@@ -27,38 +25,65 @@ The game includes all the original features you'd expect from Pac-Man:
|
|||||||
- [ ] Progressive difficulty with faster ghosts and shorter power pellet duration
|
- [ ] Progressive difficulty with faster ghosts and shorter power pellet duration
|
||||||
- [x] Authentic sound effects and sprites
|
- [x] Authentic sound effects and sprites
|
||||||
|
|
||||||
Built with SDL2 for cross-platform graphics and audio, this implementation can run on Windows, Linux, macOS, and in web browsers via WebAssembly.
|
This cross-platform implementation is built with SDL2 for graphics, audio, and input handling. It can run on Windows, Linux, macOS, and in web browsers via WebAssembly.
|
||||||
|
|
||||||
## Feature Targets
|
## Why?
|
||||||
|
|
||||||
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors.
|
Just because. And because I wanted to learn more about Rust, inter-operability with C, and compiling to WebAssembly.
|
||||||
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly.
|
|
||||||
|
I was inspired by a certain code review video on YouTube; [SOME UNIQUE C++ CODE // Pacman Clone Code Review](https://www.youtube.com/watch?v=OKs_JewEeOo) by The Cherno.
|
||||||
|
|
||||||
|
For some reason, I was inspired to try and replicate it in Rust, and it was uniquely challenging.
|
||||||
|
|
||||||
|
I wanted to hit a log of goals and features, making it a 'perfect' project that I could be proud of.
|
||||||
|
|
||||||
|
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors. No hacks, workarounds, or poor designs.
|
||||||
|
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly. Statically linked, no runtime dependencies.
|
||||||
|
- Performant, low memory, CPU and GPU usage.
|
||||||
- Online demo, playable in a browser.
|
- Online demo, playable in a browser.
|
||||||
- Automatic build system, with releases for Windows, Linux, and Mac & Web-Assembly.
|
- Completely automatic build system with releases for all platforms.
|
||||||
|
- Well documented, well-tested, and maintainable.
|
||||||
|
|
||||||
|
## Experimental Ideas
|
||||||
|
|
||||||
- Debug tooling
|
- Debug tooling
|
||||||
- Game state visualization
|
- Game state visualization
|
||||||
- Game speed controls + pausing
|
- Game speed controls + pausing
|
||||||
- Log tracing
|
- Log tracing
|
||||||
- Performance details
|
- Performance details
|
||||||
|
- Customized Themes & Colors
|
||||||
## Experimental Ideas
|
- Color-blind friendly
|
||||||
|
|
||||||
- Perfected Ghost Algorithms
|
- Perfected Ghost Algorithms
|
||||||
- More than 4 ghosts
|
- More than 4 ghosts
|
||||||
- Custom Level Generation
|
- Custom Level Generation
|
||||||
- Multi-map tunnelling
|
- Multi-map tunnelling
|
||||||
- Online Scoreboard
|
- Online Scoreboard
|
||||||
- WebAssembly build contains a special API key for communicating with server.
|
- An online axum server with a simple database and OAuth2 authentication.
|
||||||
- To prevent abuse, the server will only accept scores from the WebAssembly build.
|
- Integrates with GitHub, Discord, and Google OAuth2 to acquire an email identifier & avatar.
|
||||||
|
- Avatars are optional for score submission and can be disabled, instead using a blank avatar.
|
||||||
|
- Avatars are downscaled to a low resolution pixellated image to maintain the 8-bit aesthetic.
|
||||||
|
- A custom name is used for the score submission, which is checked for potential abusive language.
|
||||||
|
- A max length of 14 characters, and a min length of 3 characters.
|
||||||
|
- Names are checked for potential abusive language via an external API.
|
||||||
|
- The client implementation should require zero configuration, environment variables, or special secrets.
|
||||||
|
- It simply defaults to the pacman server API, or can be overriden manually.
|
||||||
|
|
||||||
## Build Notes
|
## Build Notes
|
||||||
|
|
||||||
|
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.
|
||||||
|
- I'm still not sure _why_ 3.1.43 is required, but it is. Perhaps in the future I will attempt to use a more modern version.
|
||||||
|
- Occasionally, the build will fail due to dependencies failing to download. I even have a retry mechanism in the build workflow due to this.
|
||||||
- You can then activate the Emscripten SDK with `source ./emsdk/emsdk_env.sh` or `./emsdk/emsdk_env.ps1` or `./emsdk/emsdk_env.bat` depending on your OS/terminal.
|
- You can then activate the Emscripten SDK with `source ./emsdk/emsdk_env.sh` or `./emsdk/emsdk_env.ps1` or `./emsdk/emsdk_env.bat` depending on your OS/terminal.
|
||||||
- While using the `web.build.ts` is not technically required, it simplifies the build process and is very helpful.
|
- While using the `web.build.ts` is not technically required, it simplifies the build process and is very helpful.
|
||||||
- It is intended to be run with `bun`, which you can acquire at [bun.sh](https://bun.sh/)
|
- It is intended to be run with `bun`, which you can acquire at [bun.sh](https://bun.sh/)
|
||||||
- Tip: You can launch a fileserver with `python` or `caddy` to serve the files in the `dist` folder.
|
- Tip: You can launch a fileserver with `python` or `caddy` to serve the files in the `dist` folder.
|
||||||
- `python3 -m http.server 8080 -d dist`
|
- `python3 -m http.server 8080 -d dist`
|
||||||
- `caddy file-server --root dist` (install with `[sudo apt|brew|choco] install caddy` or [a dozen other ways](https://caddyserver.com/docs/install))
|
- `caddy file-server --root dist` (install with `[sudo apt|brew|choco] install caddy` or [a dozen other ways](https://caddyserver.com/docs/install))
|
||||||
|
- `web.build.ts` auto installs dependencies, but you may need to pass `-i` or `--install=fallback|force` to install missing packages. My guess is that if you have some packages installed, it won't install any missing ones. If you have no packages installed, it will install all of them.
|
||||||
|
- If you want to have TypeScript resolution for development, you can manually install the dependencies with `bun install` in the `assets/site` folder.
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 16 KiB |
@@ -2,12 +2,25 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Pac-Man Arcade</title>
|
<title>Pac-Man in Rust</title>
|
||||||
<link rel="stylesheet" href="build.css" />
|
<link rel="stylesheet" href="build.css" />
|
||||||
<style>
|
<style>
|
||||||
|
/* Minimal fallback to prevent white flash and canvas pop-in before CSS loads */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background: #000;
|
||||||
|
color: #facc15;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#canvas {
|
||||||
|
display: block;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-black text-yellow-400 text-center">
|
<body class="bg-black text-yellow-400 text-center min-h-screen">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/Xevion/Pac-Man"
|
href="https://github.com/Xevion/Pac-Man"
|
||||||
class="absolute top-0 right-0"
|
class="absolute top-0 right-0"
|
||||||
@@ -17,7 +30,7 @@
|
|||||||
width="80"
|
width="80"
|
||||||
height="80"
|
height="80"
|
||||||
viewBox="0 0 250 250"
|
viewBox="0 0 250 250"
|
||||||
class="fill-yellow-400 text-black"
|
class="fill-yellow-400 [&>.octo-arm,.octo-body]:fill-black"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||||
@@ -31,33 +44,51 @@
|
|||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<h1 class="text-4xl mt-10 scaled-text">Pac-Man Arcade</h1>
|
|
||||||
<p class="text-lg mt-5 scaled-text">
|
<div class="min-h-screen flex flex-col">
|
||||||
Welcome to the Pac-Man Arcade! Use the controls below to play.
|
<main class="flex-1 flex items-center justify-center px-4">
|
||||||
</p>
|
<div class="w-full max-w-5xl">
|
||||||
<canvas
|
<canvas
|
||||||
id="canvas"
|
id="canvas"
|
||||||
class="block mx-auto mt-5"
|
oncontextmenu="event.preventDefault()"
|
||||||
width="800"
|
class="block w-full h-full max-h-[90vh] aspect-square"
|
||||||
height="600"
|
></canvas>
|
||||||
></canvas>
|
|
||||||
<div class="mt-10">
|
<div
|
||||||
<span
|
class="mt-8 flex flex-wrap gap-3 justify-center items-center text-sm"
|
||||||
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
|
>
|
||||||
>← ↑ → ↓ Move</span
|
<span class="code">← ↑ → ↓</span>
|
||||||
>
|
<span class="opacity-70">Move</span>
|
||||||
<span
|
|
||||||
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
|
<span class="mx-2 opacity-30">|</span>
|
||||||
>Space Change Sprite</span
|
|
||||||
>
|
<span class="code">Space</span>
|
||||||
<span
|
<span class="opacity-70">Toggle Debug</span>
|
||||||
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
|
|
||||||
>Shift + ↑↓ Change Volume</span
|
<span class="mx-2 opacity-30">|</span>
|
||||||
>
|
|
||||||
|
<span class="code">P</span>
|
||||||
|
<span class="opacity-70">Pause / Unpause</span>
|
||||||
|
|
||||||
|
<span class="mx-2 opacity-30">|</span>
|
||||||
|
|
||||||
|
<span class="code">M</span>
|
||||||
|
<span class="opacity-70">Mute / Unmute</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
const canvas = document.getElementById("canvas");
|
||||||
var Module = {
|
var Module = {
|
||||||
canvas: document.getElementById("canvas"),
|
canvas: canvas,
|
||||||
|
preRun: [
|
||||||
|
() => {
|
||||||
|
[...canvas.classList]
|
||||||
|
.filter((className) => className.includes("shadow-"))
|
||||||
|
.forEach((className) => canvas.classList.remove(className));
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" src="pacman.js"></script>
|
<script type="text/javascript" src="pacman.js"></script>
|
||||||
|
|||||||
28
assets/site/styles.css
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "TerminalVector";
|
||||||
|
src: url("TerminalVector.ttf");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Key badge styling */
|
||||||
|
.code {
|
||||||
|
@apply px-3 py-1 rounded-md font-mono text-[0.9em] lowercase inline-block align-middle;
|
||||||
|
background: rgba(250, 204, 21, 0.08); /* yellow-400 at low opacity */
|
||||||
|
border: 1px solid rgba(250, 204, 21, 0.25);
|
||||||
|
color: #fde68a; /* lighter yellow for readability */
|
||||||
|
font-family: "TerminalVector", ui-monospace, Consolas, "Courier New",
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title styling */
|
||||||
|
.arcade-title {
|
||||||
|
font-family: "TerminalVector", ui-monospace, Consolas, "Courier New",
|
||||||
|
monospace;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-shadow: 0 0 18px rgba(250, 204, 21, 0.15),
|
||||||
|
0 0 2px rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Liberation Mono";
|
|
||||||
src:
|
|
||||||
url("LiberationMono.woff2") format("woff2"),
|
|
||||||
url("LiberationMono.woff") format("woff");
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
@apply w-full h-[65vh] min-h-[200px] block mx-auto bg-black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
|
||||||
@apply px-1 rounded font-mono bg-zinc-900 border border-zinc-700 lowercase;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
BIN
assets/unpacked/maze/tiles/0.png
Normal file
|
After Width: | Height: | Size: 102 B |
BIN
assets/unpacked/maze/tiles/1.png
Normal file
|
After Width: | Height: | Size: 79 B |
BIN
assets/unpacked/maze/tiles/10.png
Normal file
|
After Width: | Height: | Size: 84 B |
BIN
assets/unpacked/maze/tiles/11.png
Normal file
|
After Width: | Height: | Size: 77 B |
BIN
assets/unpacked/maze/tiles/12.png
Normal file
|
After Width: | Height: | Size: 80 B |
BIN
assets/unpacked/maze/tiles/13.png
Normal file
|
After Width: | Height: | Size: 87 B |
BIN
assets/unpacked/maze/tiles/14.png
Normal file
|
After Width: | Height: | Size: 79 B |
BIN
assets/unpacked/maze/tiles/15.png
Normal file
|
After Width: | Height: | Size: 89 B |
BIN
assets/unpacked/maze/tiles/16.png
Normal file
|
After Width: | Height: | Size: 91 B |
BIN
assets/unpacked/maze/tiles/17.png
Normal file
|
After Width: | Height: | Size: 87 B |
BIN
assets/unpacked/maze/tiles/18.png
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
assets/unpacked/maze/tiles/19.png
Normal file
|
After Width: | Height: | Size: 77 B |
BIN
assets/unpacked/maze/tiles/2.png
Normal file
|
After Width: | Height: | Size: 93 B |
BIN
assets/unpacked/maze/tiles/20.png
Normal file
|
After Width: | Height: | Size: 91 B |
BIN
assets/unpacked/maze/tiles/21.png
Normal file
|
After Width: | Height: | Size: 97 B |
BIN
assets/unpacked/maze/tiles/22.png
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
assets/unpacked/maze/tiles/23.png
Normal file
|
After Width: | Height: | Size: 88 B |
BIN
assets/unpacked/maze/tiles/24.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/25.png
Normal file
|
After Width: | Height: | Size: 80 B |
BIN
assets/unpacked/maze/tiles/26.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/27.png
Normal file
|
After Width: | Height: | Size: 93 B |
BIN
assets/unpacked/maze/tiles/28.png
Normal file
|
After Width: | Height: | Size: 89 B |
BIN
assets/unpacked/maze/tiles/29.png
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
assets/unpacked/maze/tiles/3.png
Normal file
|
After Width: | Height: | Size: 87 B |
BIN
assets/unpacked/maze/tiles/30.png
Normal file
|
After Width: | Height: | Size: 79 B |
BIN
assets/unpacked/maze/tiles/31.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
assets/unpacked/maze/tiles/32.png
Normal file
|
After Width: | Height: | Size: 98 B |
BIN
assets/unpacked/maze/tiles/33.png
Normal file
|
After Width: | Height: | Size: 96 B |
BIN
assets/unpacked/maze/tiles/34.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
assets/unpacked/maze/tiles/4.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
assets/unpacked/maze/tiles/5.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/6.png
Normal file
|
After Width: | Height: | Size: 71 B |
BIN
assets/unpacked/maze/tiles/7.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/8.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/9.png
Normal file
|
After Width: | Height: | Size: 82 B |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
86
bacon.toml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# This is a configuration file for the bacon tool
|
||||||
|
#
|
||||||
|
# Complete help on configuration: https://dystroy.org/bacon/config/
|
||||||
|
#
|
||||||
|
# You may check the current default at
|
||||||
|
# https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml
|
||||||
|
|
||||||
|
default_job = "check"
|
||||||
|
env.CARGO_TERM_COLOR = "always"
|
||||||
|
|
||||||
|
[jobs.check]
|
||||||
|
command = ["cargo", "check"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.check-all]
|
||||||
|
command = ["cargo", "check", "--all-targets"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
# Run clippy on the default target
|
||||||
|
[jobs.clippy]
|
||||||
|
command = ["cargo", "clippy"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
# Run clippy on all targets
|
||||||
|
[jobs.clippy-all]
|
||||||
|
command = ["cargo", "clippy", "--all-targets"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.test]
|
||||||
|
command = [
|
||||||
|
"cargo", "nextest", "run",
|
||||||
|
"--hide-progress-bar", "--failure-output", "final"
|
||||||
|
]
|
||||||
|
need_stdout = true
|
||||||
|
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]
|
||||||
|
command = ["cargo", "doc", "--no-deps"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
# If the doc compiles, then it opens in your browser and bacon switches to the previous job
|
||||||
|
[jobs.doc-open]
|
||||||
|
command = ["cargo", "doc", "--no-deps", "--open"]
|
||||||
|
need_stdout = false
|
||||||
|
on_success = "back" # so that we don't open the browser at each change
|
||||||
|
|
||||||
|
[jobs.run]
|
||||||
|
command = [
|
||||||
|
"cargo", "run",
|
||||||
|
]
|
||||||
|
need_stdout = true
|
||||||
|
allow_warnings = true
|
||||||
|
background = false
|
||||||
|
on_change_strategy = "kill_then_restart"
|
||||||
|
# kill = ["pkill", "-TERM", "-P"]'
|
||||||
|
|
||||||
|
[keybindings]
|
||||||
|
c = "job:clippy"
|
||||||
|
alt-c = "job:check"
|
||||||
|
ctrl-alt-c = "job:check-all"
|
||||||
|
shift-c = "job:clippy-all"
|
||||||
|
f = "job:coverage"
|
||||||
50
build.rs
Normal 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
@@ -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"]
|
||||||
207
src/app.rs
@@ -1,44 +1,42 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use glam::Vec2;
|
||||||
use sdl2::event::{Event, WindowEvent};
|
use sdl2::render::TextureCreator;
|
||||||
use sdl2::keyboard::Keycode;
|
use sdl2::ttf::Sdl2TtfContext;
|
||||||
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
use sdl2::video::WindowContext;
|
||||||
use sdl2::video::{Window, WindowContext};
|
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
||||||
use sdl2::EventPump;
|
use thousands::Separable;
|
||||||
use tracing::{error, event};
|
use tracing::info;
|
||||||
|
|
||||||
|
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::systems::profiling::SystemTimings;
|
||||||
|
|
||||||
#[cfg(target_os = "emscripten")]
|
pub struct App {
|
||||||
use crate::emscripten;
|
pub game: Game,
|
||||||
|
last_timings: Instant,
|
||||||
#[cfg(not(target_os = "emscripten"))]
|
|
||||||
fn sleep(value: Duration) {
|
|
||||||
spin_sleep::sleep(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "emscripten")]
|
|
||||||
fn sleep(value: Duration) {
|
|
||||||
emscripten::emscripten::sleep(value.as_millis() as u32);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct App<'a> {
|
|
||||||
game: Game,
|
|
||||||
canvas: Canvas<Window>,
|
|
||||||
event_pump: EventPump,
|
|
||||||
backbuffer: Texture<'a>,
|
|
||||||
paused: bool,
|
|
||||||
last_tick: Instant,
|
last_tick: Instant,
|
||||||
|
focused: bool,
|
||||||
|
_cursor_pos: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App<'_> {
|
impl App {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> GameResult<Self> {
|
||||||
let sdl_context = sdl2::init().map_err(|e| anyhow!(e))?;
|
let sdl_context: &'static Sdl = Box::leak(Box::new(sdl2::init().map_err(|e| GameError::Sdl(e.to_string()))?));
|
||||||
let video_subsystem = sdl_context.video().map_err(|e| anyhow!(e))?;
|
let video_subsystem: &'static VideoSubsystem =
|
||||||
let audio_subsystem = sdl_context.audio().map_err(|e| anyhow!(e))?;
|
Box::leak(Box::new(sdl_context.video().map_err(|e| GameError::Sdl(e.to_string()))?));
|
||||||
let ttf_context = sdl2::ttf::init().map_err(|e| anyhow!(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
|
||||||
|
get_platform().init_console()?;
|
||||||
|
|
||||||
let window = video_subsystem
|
let window = video_subsystem
|
||||||
.window(
|
.window(
|
||||||
@@ -48,32 +46,32 @@ impl App<'_> {
|
|||||||
)
|
)
|
||||||
.resizable()
|
.resizable()
|
||||||
.position_centered()
|
.position_centered()
|
||||||
.build()?;
|
.build()
|
||||||
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
let mut canvas = window.into_canvas().build()?;
|
let canvas = Box::leak(Box::new(
|
||||||
canvas.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)?;
|
window
|
||||||
|
.into_canvas()
|
||||||
|
.accelerated()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| GameError::Sdl(e.to_string()))?,
|
||||||
|
));
|
||||||
|
|
||||||
let texture_creator_static: &'static TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
|
canvas
|
||||||
|
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
|
||||||
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
let mut game = Game::new(texture_creator_static, &ttf_context, &audio_subsystem);
|
let texture_creator: &'static mut TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
|
||||||
game.audio.set_mute(cfg!(debug_assertions));
|
|
||||||
|
|
||||||
let mut backbuffer = texture_creator_static.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)?;
|
let game = Game::new(canvas, texture_creator, event_pump)?;
|
||||||
backbuffer.set_scale_mode(ScaleMode::Nearest);
|
// game.audio.set_mute(cfg!(debug_assertions));
|
||||||
|
|
||||||
let event_pump = sdl_context.event_pump().map_err(|e| anyhow!(e))?;
|
Ok(App {
|
||||||
|
|
||||||
// Initial draw
|
|
||||||
game.draw(&mut canvas, &mut backbuffer)?;
|
|
||||||
game.present_backbuffer(&mut canvas, &backbuffer)?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
game,
|
game,
|
||||||
canvas,
|
focused: true,
|
||||||
event_pump,
|
|
||||||
backbuffer,
|
|
||||||
paused: false,
|
|
||||||
last_tick: Instant::now(),
|
last_tick: Instant::now(),
|
||||||
|
last_timings: Instant::now() - Duration::from_secs_f32(0.5),
|
||||||
|
_cursor_pos: Vec2::ZERO,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,71 +79,68 @@ 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, .. } => {
|
|
||||||
self.game.keyboard_event(keycode.unwrap());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
error!("Failed to present backbuffer: {e}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.last_timings.elapsed() > Duration::from_secs(1) {
|
||||||
|
// Show timing statistics over the last 90 frames
|
||||||
|
if let Some(timings) = self.game.world.get_resource::<SystemTimings>() {
|
||||||
|
let stats = timings.get_stats();
|
||||||
|
let (total_avg, total_std) = timings.get_total_stats();
|
||||||
|
|
||||||
|
let mut individual_timings = String::new();
|
||||||
|
for (name, (avg, std_dev)) in stats.iter() {
|
||||||
|
individual_timings.push_str(&format!("{}={:?} ± {:?} ", name, avg, std_dev));
|
||||||
|
}
|
||||||
|
|
||||||
|
let effective_fps = match 1.0 / total_avg.as_secs_f64() {
|
||||||
|
f if f > 100.0 => (f as u32).separate_with_commas(),
|
||||||
|
f if f < 10.0 => format!("{:.1} FPS", f),
|
||||||
|
f => format!("{:.0} FPS", f),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("({effective_fps}) {total_avg:?} ± {total_std:?} ({individual_timings})");
|
||||||
|
}
|
||||||
|
self.last_timings = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
sleep(time);
|
get_platform().sleep(time, self.focused);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
event!(
|
|
||||||
tracing::Level::WARN,
|
|
||||||
"Game loop behind schedule by: {:?}",
|
|
||||||
start.elapsed() - LOOP_TIME
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|||||||