mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 01:15:42 -06:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cc47d5904 | |||
| 11e89a63d0 | |||
| 50afd8c09f | |||
| 06841fd0d7 | |||
| 4365639a1d | |||
| 7744c06046 | |||
| 978752f0f3 | |||
| f024ce7a54 | |||
| 0196282a78 | |||
| 785a760343 | |||
| de1a89b9b0 | |||
| 66b6cdf01b | |||
| 5a48e83b1a | |||
| df8f858651 | |||
| 1fa7a0807f | |||
| 6d3d3bf49c | |||
| 0a46f64866 | |||
| fd7eecf53e | |||
| f540dc5373 | |||
| f51a3ddeb0 | |||
| 9f7c460369 | |||
| 2c0df99dea | |||
| cf12a04c69 | |||
| fa7e985c0d | |||
| f5ff90cb11 | |||
| a0f65b551c | |||
| 8808a1aa3b | |||
| 62b2c607a9 | |||
| 14b34db6de | |||
| 9238b53c40 | |||
| 3e498ee5c3 | |||
| 715eeb6296 | |||
| 682ce059fa | |||
| c8314b23dd | |||
| 40acffafd1 | |||
| 2187212b7c | |||
| 229d2242ef | |||
| 00c4c76299 | |||
| 8b30a602bf | |||
| 83a5ccdb8e | |||
| 44d8184d8b | |||
| 0630fc56ec | |||
| 98d8960c57 | |||
| 394344c11f | |||
| 93ba470ce9 | |||
| 129aed0ffb | |||
| e062ada301 | |||
| af57199915 | |||
| 538cf1efb5 | |||
| 03b2c5a659 | |||
| 64e226be70 | |||
| f998ddd344 | |||
| b2ad8e7afe | |||
| 799d5d85e8 | |||
| 9730d02da5 | |||
| f634beffee | |||
| d15dbe3982 | |||
| de5cddd9b6 | |||
| e3f37ab48e | |||
| 3dd8d5aff7 | |||
| ad084d1cd8 | |||
| 852e54f1bf | |||
| a62ddab9af | |||
| 50d0bc7d5f | |||
| 2c6045aa1b | |||
| bf8370ef35 | |||
| c71b6d69ab | |||
| a7e87c18a3 | |||
| 95298fbc00 | |||
| fe18eafbaf | |||
| 60eaa428ac | |||
| 18eaeee19e | |||
| b3c1a30a74 | |||
| 0d76c6528b | |||
| da98b54216 | |||
| 6ce3a5ce79 | |||
| b987599f10 | |||
| 786fbb5002 | |||
| 422535c00d |
@@ -1,4 +1,12 @@
|
||||
[target.wasm32-unknown-emscripten]
|
||||
[target.'cfg(target_os = "emscripten")']
|
||||
# TODO: Document what the fuck this is.
|
||||
rustflags = [
|
||||
"--use-preload-plugins --preload-file assets -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s ASSERTIONS=1",
|
||||
# "-O", "-C", "link-args=-O2 --profiling",
|
||||
#"-C", "link-args=-O3 --closure 1",
|
||||
# "-C", "link-args=-g -gsource-map",
|
||||
"-C", "link-args=-sASYNCIFY -sALLOW_MEMORY_GROWTH=1",
|
||||
# "-C", "link-args=-sALLOW_MEMORY_GROWTH=1",
|
||||
"-C", "link-args=-sUSE_SDL=2 -sUSE_SDL_IMAGE=2 -sUSE_SDL_MIXER=2 -sUSE_OGG=1 -sUSE_SDL_GFX=2 -sUSE_SDL_TTF=2 -sSDL2_IMAGE_FORMATS=['png']",
|
||||
# USE_OGG, USE_VORBIS for OGG/VORBIS usage
|
||||
"-C", "link-args=--preload-file assets/",
|
||||
]
|
||||
229
.github/workflows/deploy.yaml
vendored
229
.github/workflows/deploy.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Github Pages
|
||||
name: Build
|
||||
|
||||
on: [push]
|
||||
|
||||
@@ -6,23 +6,220 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-github-pages:
|
||||
wasm:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2 # repo checkout
|
||||
- uses: mymindstorm/setup-emsdk@v11 # setup emscripten toolchain
|
||||
# with:
|
||||
# version: 3.1.35
|
||||
- uses: actions-rs/toolchain@v1 # get rust toolchain for wasm
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Emscripten SDK
|
||||
uses: mymindstorm/setup-emsdk@v14
|
||||
with:
|
||||
version: 3.1.43
|
||||
|
||||
- name: Setup Rust (WASM32 Emscripten)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-emscripten
|
||||
override: true
|
||||
- name: Rust Cache # cache the rust build artefacts
|
||||
uses: Swatinem/rust-cache@v1
|
||||
- name: Build # build
|
||||
run: ./build.sh
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
folder: dist
|
||||
version: 8
|
||||
run_install: true
|
||||
|
||||
- name: Build
|
||||
run: ./scripts/build.sh -er # release mode, skip emsdk
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "./dist/"
|
||||
retention-days: 7
|
||||
|
||||
- name: Deploy
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TARGET: x86_64-unknown-linux-gnu
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install SDL2 Packages
|
||||
run: sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev libsdl2-gfx-dev
|
||||
|
||||
- name: Setup Rust Toolchain (Linux)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: ${{ env.TARGET }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
- name: Assemble Archive
|
||||
run: |
|
||||
mkdir /tmp/example/
|
||||
cp ./target/release/pacman /tmp/example/
|
||||
chmod a+x /tmp/example/pacman
|
||||
mkdir /tmp/example/assets
|
||||
cp ./assets/TerminalVector.ttf ./assets/fruit.png /tmp/example/assets
|
||||
|
||||
- name: Install Cargo Binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Acquire Package Version
|
||||
run: |
|
||||
cargo binstall toml-cli -y
|
||||
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
|
||||
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
|
||||
path: /tmp/example/
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
macos:
|
||||
runs-on: macos-13
|
||||
env:
|
||||
TARGET: x86_64-apple-darwin
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install SDL2 Packages
|
||||
run: brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf sdl2_gfx
|
||||
|
||||
- name: Setup Rust Toolchain (MacOS)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: ${{ env.TARGET }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
- name: Assemble Archive
|
||||
run: |
|
||||
mkdir /tmp/example/
|
||||
cp ./target/release/pacman /tmp/example/
|
||||
mkdir /tmp/example/assets
|
||||
cp ./assets/TerminalVector.ttf ./assets/fruit.png /tmp/example/assets
|
||||
|
||||
- name: Install Cargo Binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Acquire Package Version
|
||||
run: |
|
||||
cargo binstall toml-cli -y
|
||||
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
|
||||
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
|
||||
path: /tmp/example/
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
windows:
|
||||
env:
|
||||
TARGET: x86_64-pc-windows-gnu
|
||||
SDL2: 2.30.2
|
||||
SDL2_TTF: 2.22.0
|
||||
SDL2_MIXER: 2.8.0
|
||||
SDL2_IMAGE: 2.8.2
|
||||
# SDL2_GFX: 1.0.4
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download SDL2 Libraries
|
||||
run: |
|
||||
curl -L "https://github.com/libsdl-org/SDL/releases/download/release-${{ env.SDL2 }}/SDL2-devel-${{ env.SDL2 }}-VC.zip" -o "sdl2_devel.zip"
|
||||
curl -L "https://github.com/libsdl-org/SDL_mixer/releases/download/release-${{ env.SDL2_MIXER }}/SDL2_mixer-devel-${{ env.SDL2_MIXER }}-VC.zip" -o "sdl2_mixer_devel.zip"
|
||||
curl -L "https://github.com/libsdl-org/SDL_ttf/releases/download/release-${{ env.SDL2_TTF }}/SDL2_ttf-devel-${{ env.SDL2_TTF }}-VC.zip" -o "sdl2_ttf_devel.zip"
|
||||
curl -L "https://github.com/libsdl-org/SDL_image/releases/download/release-${{ env.SDL2_IMAGE }}/SDL2_image-devel-${{ env.SDL2_IMAGE }}-VC.zip" -o "sdl2_image_devel.zip"
|
||||
|
||||
- name: Extract SDL2 DLLs
|
||||
run: |
|
||||
7z x ./sdl2_devel.zip -o"./tmp/"
|
||||
mv ./tmp/SDL2-${{ env.SDL2 }}/lib/x64/SDL2.dll ./
|
||||
mv ./tmp/SDL2-${{ env.SDL2 }}/lib/x64/SDL2.lib ./
|
||||
|
||||
7z x ./sdl2_mixer_devel.zip -o"./tmp/"
|
||||
mv ./tmp/SDL2_mixer-${{ env.SDL2_MIXER }}/lib/x64/SDL2_mixer.dll ./
|
||||
mv ./tmp/SDL2_mixer-${{ env.SDL2_MIXER }}/lib/x64/SDL2_mixer.lib ./
|
||||
|
||||
7z x ./sdl2_ttf_devel.zip -o"./tmp/"
|
||||
mv ./tmp/SDL2_ttf-${{ env.SDL2_TTF }}/lib/x64/SDL2_ttf.dll ./
|
||||
mv ./tmp/SDL2_ttf-${{ env.SDL2_TTF }}/lib/x64/SDL2_ttf.lib ./
|
||||
|
||||
7z x ./sdl2_image_devel.zip -o"./tmp/"
|
||||
mv ./tmp/SDL2_image-${{ env.SDL2_IMAGE }}/lib/x64/SDL2_image.dll ./
|
||||
mv ./tmp/SDL2_image-${{ env.SDL2_IMAGE }}/lib/x64/SDL2_image.lib ./
|
||||
|
||||
- name: Install SDL2_gfx
|
||||
run: |
|
||||
C:\vcpkg\vcpkg.exe install sdl2-gfx:x64-windows-release
|
||||
cp C:\vcpkg\packages\sdl2-gfx_x64-windows-release\bin\SDL2_gfx.dll ./
|
||||
cp C:\vcpkg\packages\sdl2-gfx_x64-windows-release\lib\SDL2_gfx.lib ./
|
||||
|
||||
- name: Setup Rust (Windows)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ env.TARGET }}
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
- name: Prepare Archive
|
||||
run: |
|
||||
New-Item -Type Directory ./release/
|
||||
Move-Item -Path ./target/release/pacman.exe -Destination ./release/
|
||||
Move-Item -Path ./SDL2.dll, ./SDL2_image.dll, ./SDL2_ttf.dll, ./SDL2_mixer.dll, ./SDL2_gfx.dll -Destination ./release/
|
||||
New-Item -Type Directory ./release/assets/
|
||||
Move-Item -Path ./assets/TerminalVector.ttf, ./assets/fruit.png -Destination ./release/assets/
|
||||
|
||||
- name: Install Cargo Binstall
|
||||
uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Acquire Package Version
|
||||
run: |
|
||||
cargo binstall toml-cli -y
|
||||
PACKAGE_VERSION=$(toml get ./Cargo.toml package.version --raw)
|
||||
echo "PACKAGE_VERSION=${PACKAGE_VERSION}" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: "pacman-${{ env.PACKAGE_VERSION }}-${{ env.TARGET }}"
|
||||
path: ./release/
|
||||
retention-days: 7
|
||||
if-no-files-found: error
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,2 +1,7 @@
|
||||
/target
|
||||
.idea
|
||||
/dist
|
||||
.idea
|
||||
*.dll
|
||||
rust-sdl2-emscripten/
|
||||
assets/build.css
|
||||
emsdk/
|
||||
22
.pre-commit-config.yaml
Normal file
22
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: forbid-submodules
|
||||
- id: mixed-line-ending
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-fmt
|
||||
name: cargo fmt
|
||||
entry: cargo fmt --all --
|
||||
language: system
|
||||
types: [rust]
|
||||
pass_filenames: false
|
||||
7
BUILD.md
Normal file
7
BUILD.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Building Pac-Man
|
||||
|
||||
## GitHub Actions Workflow
|
||||
|
||||
1. Build workflow produces executables & WASM files for all platforms
|
||||
2. Uploaded as artifacts
|
||||
3. Deployment workflow downloads artifacts and uploads to GitHub Pages
|
||||
622
Cargo.lock
generated
622
Cargo.lock
generated
@@ -1,6 +1,27 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
@@ -8,6 +29,18 @@ version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "c_vec"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdd7a427adc0135366d99db65b36dae9237130997e560ed61118041fb72be6e8"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -15,32 +48,283 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
name = "deprecate-until"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
checksum = "7a3767f826efbbe5a5ae093920b58b43b01734202be697e1354914e862e8e704"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"semver",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "integer-sqrt"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "pacman"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pathfinding",
|
||||
"rand",
|
||||
"sdl2",
|
||||
"spin_sleep",
|
||||
"thiserror 1.0.69",
|
||||
"tracing",
|
||||
"tracing-error",
|
||||
"tracing-subscriber",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdl2"
|
||||
version = "0.35.2"
|
||||
name = "pathfinding"
|
||||
version = "4.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a"
|
||||
checksum = "59ac35caa284c08f3721fb33c2741b5f763decaf42d080c8a6a722154347017e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"deprecate-until",
|
||||
"indexmap",
|
||||
"integer-sqrt",
|
||||
"num-traits",
|
||||
"rustc-hash",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "sdl2"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"c_vec",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"sdl2-sys",
|
||||
@@ -48,17 +332,331 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sdl2-sys"
|
||||
version = "0.35.2"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0"
|
||||
checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"vcpkg",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
||||
|
||||
[[package]]
|
||||
name = "spin_sleep"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14ac0e4b54d028c2000a13895bcd84cd02a1d63c4f78e08e4ec5ec8f53efd4b9"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
40
Cargo.toml
40
Cargo.toml
@@ -6,5 +6,41 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
lazy_static = "1.4.0"
|
||||
sdl2 = { version = "0.35", features = ["image", "ttf", "mixer"] }
|
||||
tracing = { version = "0.1.40", features = ["max_level_debug", "release_max_level_debug"]}
|
||||
tracing-error = "0.2.0"
|
||||
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
|
||||
lazy_static = "1.5.0"
|
||||
sdl2 = { version = "0.38.0", features = ["image", "ttf"] }
|
||||
spin_sleep = "1.3.2"
|
||||
rand = "0.9.2"
|
||||
pathfinding = "4.14"
|
||||
once_cell = "1.21.3"
|
||||
thiserror = "1.0"
|
||||
anyhow = "1.0"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.winapi]
|
||||
version = "0.3"
|
||||
features = ["consoleapi", "fileapi", "handleapi", "processenv", "winbase", "wincon", "winnt", "winuser", "windef", "minwindef"]
|
||||
|
||||
|
||||
[target.'cfg(target_os = "emscripten")'.dependencies.sdl2]
|
||||
version = "0.38"
|
||||
default-features = false
|
||||
features = ["ttf","image","gfx","mixer"]
|
||||
|
||||
[target.'cfg(not(target_os = "emscripten"))'.dependencies.sdl2]
|
||||
version = "0.38"
|
||||
default-features = false
|
||||
features = ["ttf","image","gfx","mixer","static-link","use-vcpkg"]
|
||||
|
||||
[package.metadata.vcpkg]
|
||||
dependencies = ["sdl2", "sdl2-image[libjpeg-turbo,tiff,libwebp]", "sdl2-ttf", "sdl2-gfx", "sdl2-mixer"]
|
||||
git = "https://github.com/microsoft/vcpkg"
|
||||
rev = "2024.05.24" # release 2024.05.24 # to check for a new one, check https://github.com/microsoft/vcpkg/releases
|
||||
|
||||
[package.metadata.vcpkg.target]
|
||||
x86_64-pc-windows-msvc = { triplet = "x64-windows-static-md" }
|
||||
stable-x86_64-unknown-linux-gnu = { triplet = "x86_64-unknown-linux-gnu" }
|
||||
|
||||
[target.'cfg(target_os = "emscripten")'.dependencies]
|
||||
libc = "0.2.16"
|
||||
|
||||
13
README.md
13
README.md
@@ -23,6 +23,9 @@ at.
|
||||
- More than 4 ghosts
|
||||
- Custom Level Generation
|
||||
- Multi-map tunnelling
|
||||
- Online Scoreboard
|
||||
- WebAssembly build contains a special API key for communicating with server.
|
||||
- To prevent abuse, the server will only accept scores from the WebAssembly build.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -49,6 +52,16 @@ The latest releases can be found here:
|
||||
|
||||
Download each for your architecture, and locate the appropriately named DLL within. Move said DLL to root of this project.
|
||||
|
||||
In total, you should have the following DLLs in the root of the project:
|
||||
|
||||
- SDL2.dll
|
||||
- SDL2_mixer.dll
|
||||
- SDL2_ttf.dll
|
||||
- SDL2_image.dll
|
||||
- libpngX-X.dll
|
||||
- Not sure on what specific version is to be used, or if naming matters. `libpng16-16.dll` is what I had used.
|
||||
- zlib1.dll
|
||||
|
||||
## Building
|
||||
|
||||
To build the project, run the following command:
|
||||
|
||||
333
STORY.md
Normal file
333
STORY.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# Story
|
||||
|
||||
This is living document that describes the story of the project, from inspiration to solution.
|
||||
When a website is available, this document will help curate it's content.
|
||||
|
||||
## Inspiration
|
||||
|
||||
I initially got the idea for this project after finding a video about another Pac-Man clone on YouTube.
|
||||
|
||||
[![Code Review Thumbnail][code-review-thumbnail]][code-review-video]
|
||||
|
||||
This implementation was written in C++, used SDL2 for graphics, and was kinda weird - but it worked.
|
||||
|
||||
- I think it was weird because the way it linked files together is extremely non-standard.
|
||||
Essentially, it was a single file that included all the other files. This is not how C++ projects are typically structured.
|
||||
- This implementation was also extremely dependent on OOP; Rust has no real counterpart for OOP code, so writing my own implementation would be a challenge.
|
||||
|
||||
## Lifetimes
|
||||
|
||||
Rust's SDL2 implementation is a wrapper around the C library, so it's not as nice as the C++ implementation.
|
||||
Additionally, lifetimes in this library are a bit weird, making them quite difficult to deal with.
|
||||
|
||||
I found a whole blog post complaining about this ([1][fighting-lifetimes-1], [2][fighting-lifetimes-2], [3][fighting-lifetimes-3]), so I'm not alone in this.
|
||||
|
||||
## Emscripten & RuggRogue
|
||||
|
||||
One of the targets for this project is to build a web-accessible version of the game. If you were watching at all during
|
||||
the Rust hype, one of it's primary selling points was a growing community of Rust-based web applications, thanks to
|
||||
WebAssembly.
|
||||
|
||||
The problem is that much of this work was done for pure-Rust applications - and SDL is C++.
|
||||
This requires a C++ WebAssembly compiler such as Emscripten; and it's a pain to get working.
|
||||
|
||||
Luckily though, someone else has done this before, and they fully documented it - [RuggRouge][ruggrouge].
|
||||
- Built with Rust
|
||||
- Uses SDL2
|
||||
- Compiling for WebAssembly with Emscripten
|
||||
- Also compiles for Windows & Linux
|
||||
|
||||
This repository has been massively helpful in getting my WebAssembly builds working.
|
||||
|
||||
## Key Capturing Extensions in WASM Build
|
||||
|
||||
Some extensions I had installed were capturing keys.
|
||||
The issue presented with some keys never being sent to the application.
|
||||
To confirm, enter safe mode or switch to a different browser without said extensions.
|
||||
If the issue disappears, it's because of an extension in your browser stealing keys in a way that is incompatible with the batshit insanity of Emscripten.
|
||||
|
||||
|
||||
## A Long Break
|
||||
|
||||
After hitting a wall with an issue with Emscripten where the tab would freeze after switching tabs (making it into a background tab), I decided to take a break from the project. A couple months went by without anything going on.
|
||||
|
||||
## Revisiting
|
||||
|
||||
I decided to revisit the project because I didn't want to see this project die. It's actually a lot of fun, and has a very interesting stack, with a simple premise, and a lot of potential for expansion.
|
||||
|
||||
Unfortunately, the issue above still lingered. I did a lot of testing, and concluded that I needed to create a simple example with as much stripped away as possible. All I learned from this was that the freeze occurred the moment that the 'Hidden' event (for the Window) was fired. After that, the rendered would take 0 nanoseconds to render, and some script for Asyncify would keep spinning in the background.
|
||||
|
||||
I tried to ask around but didn't get anywhere, but one reply on my post gave me the idea to back away from Emscripten 1.39.20 (several years old at this point).
|
||||
|
||||
## Emscripten Callback Main Loop
|
||||
|
||||
I looked into as many examples online (not that many), and came across an Emscripten callback loop exposed in C. Some were basic and all over the place, some were advanced, but also imbibed extremely annoying static lifetime requirements.
|
||||
|
||||
- I tried my best to satisfy and work with these lifetimes, but it was a nightmare.
|
||||
- Instead, I tried to simplify and move away from this annoying Emscripten callback loop, but simpler ones had issues, crashing with `invalid renderer` errors.
|
||||
- This guy named Greg Buchholz apparently was the creator of this special Emscripten bindings with the static lifetimes, and it was done to solve this issue with `invalid renderer`.
|
||||
- [GitHub](https://github.com/gregbuchholz), [Repository](https://github.com/gregbuchholz/RuSDLem), [StackOverflow](https://stackoverflow.com/questions/69748049/rust-sdl2-emscripten-and-invalid-renderer-panic), [Forum Post](https://users.rust-lang.org/t/sdl2-emscripten-asmjs-and-invalid-renderer-panic/66567/2)
|
||||
|
||||
With this in mind, it seemed like I was at a dead end AGAIN; either I had to deal with the static lifetimes (I am not that good at Rust), or I had to deal with Asyncify.
|
||||
|
||||
But this did help me narrow my search even more for a good example. I needed to find a repository with Rust, SDL2, Emscripten, and `TextureCreator`.
|
||||
|
||||
`TextureCreator` was key, as the static lifetimes issue was most encumbering when dealing with borrows and lifetimes of `TextureCreator` inside the `main` loop closure.
|
||||
|
||||
## Return to Asyncify
|
||||
|
||||
I found [one such repository](https://github.com/KyleMiles/Rust-SDL-Emscripten-Template/), and interestingly, it used `latest` Emscripten (not a specific target like 1.39.20), and was new enough (2 years old, but still new enough) to be relevant.
|
||||
|
||||
Even more interesting, it didn't use the `main` loop closure, but instead used Emscripten's *Asyncify* feature to handle the main loop.
|
||||
|
||||
But, unlike my original project which called `std::thread::sleep` directly, it used bindings into Emscripten's functions like `emscripten_sleep`.
|
||||
|
||||
Even better, it had an example of script execution (JavaScript) bindings, which I could use to handle all sorts of things. I tested it out, and it worked.
|
||||
|
||||
## Instant::now() 32-bit Byte Cutoff
|
||||
|
||||
Unfortunately while trying to get basic FPS timings working, I got divide by zero errors when trying to calculate the time difference between two `Instant` times.
|
||||
|
||||
This was weird, and honestly, I'm confused as to why the 2-year old sample code 'worked' at the time, but not now.
|
||||
|
||||
After a bit of time, I noted that the `Instant` times were printing with only the whole seconds changing, and the nanoseconds were always 0.
|
||||
|
||||
```
|
||||
Instant { tv_sec: 0, tv_nsec: 0 }
|
||||
Instant { tv_sec: 1, tv_nsec: 0 }
|
||||
Instant { tv_sec: 2, tv_nsec: 0 }
|
||||
Instant { tv_sec: 3, tv_nsec: 0 }
|
||||
Instant { tv_sec: 4, tv_nsec: 0 }
|
||||
...
|
||||
```
|
||||
|
||||
This was super weird, but I stumbled upon [an issue on GitHub](https://github.com/rust-lang/rust/issues/113852) that mentioned the exact situation I was in, as well as providing a patch solution (`emscripten_get_now`).
|
||||
|
||||
## VSync Gotcha
|
||||
|
||||
After getting the timing working, I noticed that the rendering was extremely slow. I was getting 60 FPS, but I wasn't sleeping at all.
|
||||
|
||||
Normally when rendering occurs, you want to sleep for the remaining time so that your game calculations can occur at a consistent rate (60 FPS for example).
|
||||
|
||||
If your rendering time is less than the sleep time, you can just sleep for the remaining time. But if your rendering time is greater than the sleep time, you encounter lag, the FPS starts to drop.
|
||||
|
||||
This was a confusing issue as I knew it couldn't be a coincidence that the rendering time was exactly ~16ms (60 FPS) every time.
|
||||
|
||||
After a little bit though, I found the `present_vsync` function in the SDL2 render initialization. This was causing the rendering to try and time the canvas present() to the monitor's refresh rate (60 FPS).
|
||||
|
||||
Maybe I could have skipped my custom timing and just used this, but I don't know if it would be platform-independent, what would happen on 120 FPS displays, etc.
|
||||
|
||||
## Emscripten v.s. SDL2-TTF
|
||||
|
||||
While working on the next extension of SDL2 for my test repository, SDL2-TTF had some pretty annoying issues. It would build fine, but it would raise a runtime error: `indirect call to null`.
|
||||
|
||||
Luckily, I had a recently updated repository to copy off of, and the working fix was to lower the EMSDK version to `3.1.43`.
|
||||
|
||||
[Source](https://github.com/aelred/tetris/blob/0ad88153db1ca7962b42277504c0f7f9f3c675a9/tetris-sdl/src/main.rs#L34)
|
||||
```rust
|
||||
static FONT_DATA: &[u8] = include_bytes!("../assets/TerminalVector.ttf");
|
||||
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
fn ttf_context() -> ttf::Sdl2TtfContext {
|
||||
ttf::init().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
fn ttf_context() -> &'static ttf::Sdl2TtfContext {
|
||||
// Deliberately leak so we get a static lifetime
|
||||
Box::leak(Box::new(ttf::init().unwrap()))
|
||||
}
|
||||
|
||||
const FONT_MULTIPLE: u16 = 9;
|
||||
|
||||
// Funny division is done here to round to nearest multiple of FONT_MULTIPLE
|
||||
const FONT_SIZE: u16 = (WINDOW_HEIGHT / 32) as u16 / FONT_MULTIPLE * FONT_MULTIPLE;
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
let font_data = RWops::from_bytes(FONT_DATA).unwrap();
|
||||
let font_size = max(FONT_SIZE, FONT_MULTIPLE);
|
||||
let font = ttf_context
|
||||
.load_font_from_rwops(font_data, font_size)
|
||||
.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
I don't particularly understand why loading from memory is used, but it's a neat trick. I tested normal font loading afterwards, and it seems to be totally fine.
|
||||
|
||||
On to the Mixer extension, then.
|
||||
|
||||
## Mixer and GFX
|
||||
|
||||
Mixer was relatively easy, I don't remember anything special about it.
|
||||
|
||||
As it happens, neither was SDL GFX, except for me finding that getting it compiling on Windows would soon be difficult; `SDL2_gfx` is not currently being updated, nor is it managed by the SDL team. This meant that no releases of development libraries including DLLs or LIB files were going to be available.
|
||||
|
||||
When I added in GFX, I wanted to add some mouse interaction since that currently wasn't being done anywhere in the demo, but I also wanted the ability for the mouse to be hidden until used.
|
||||
|
||||
Detecting whether the mouse was focusing the window or not wasn't super easy, and I'm still not sure that it's working perfectly, but at the very least Emscripten seems to support what I'm trying to do. I should look into asynchronous Javascript callbacks, see what Emscripten supports.
|
||||
|
||||
## Styling with PostCSS + Tailwind
|
||||
|
||||
I'm big on using Tailwind, and while this project probably could have done without it, I didn't want to forego my favorite tool.
|
||||
|
||||
But I also didn't want to include some big framework on this, like Astro, so I looked for the smallest way to include Tailwind.
|
||||
|
||||
After fiddling and failing to find Hugo suitable, I stuck to plain HTML & the PostCSS method, which worked great. It's definitely not that fast for rapid development, but it works well enough.
|
||||
|
||||
The only thing I'm unsatisfied with is why `postcss-cli` wasn't working when executed from `pnpm`. It works just fine from `pnpx`, but it has to download and setup the whole package on *every single invocation*, which is super slow. And probably expensive, in the long run.
|
||||
|
||||
## Cross-platform Builds
|
||||
|
||||
With the next step of the demo project, I needed to get builds for every OS running, that's one down out of the four targets I'm gunning for.
|
||||
|
||||
Linux was the easiest, as usual, with `apt` providing access to all the development libraries of SDL & the associated extensions, including `SDL2_gfx`.
|
||||
|
||||
There's also no requirement for providing sidecar DLLs like Windows needs, so that worked well.
|
||||
The hardest part was figuring out the most satisfying way to zip and load all the assets together, but luckily the artifact uploader provides it's own zip implementation; albeit I may need to modify it to add further system hinting (`.tar.gz` for Linux, `.dmg` for MacOS, `.zip` for Windows).
|
||||
|
||||
## SDL2 on Windows
|
||||
|
||||
SDL2 on Windows has to be one of the least fun development cycles; setting up the environment is pretty painful as there's almost no guides for Rust users to figure out each requirement. You'll learn fast, and this knowledge is hands on experience that will probably be applicable later on in C++ development, but I'm sure a fair number of Rust users like myself have no idea why a DLL or LIB file is necessary at all.
|
||||
|
||||
To be honest, I still don't.
|
||||
|
||||
Regardless, SDL2 needs a LIB file for compliation to be available in the root directory, and each extension has there own.
|
||||
|
||||
Once the EXE is compiled, the working directory needs to contain a DLL file for execution, too. Each extension has it's own as well.
|
||||
|
||||
This sounds easy, but acquiring these DLLs and LIB files is not easy. At the very least, the SDL-supported extensions have releases available containing
|
||||
|
||||

|
||||
|
||||
So I got to creating a build step involving the download of each of these libraries. I'm no expert with `curl`, but I had it figured out eventually.
|
||||
|
||||
```yaml
|
||||
- name: Download SDL2 Libraries
|
||||
run: |
|
||||
curl -L "https://github.com/libsdl-org/SDL/releases/download/release-${{ env.SDL2 }}/SDL2-devel-${{ env.SDL2 }}-VC.zip" -o "sdl2_devel.zip"
|
||||
curl -L "https://github.com/libsdl-org/SDL_mixer/releases/download/release-${{ env.SDL2_MIXER }}/SDL2_mixer-devel-${{ env.SDL2_MIXER }}-VC.zip" -o "sdl2_mixer_devel.zip"
|
||||
curl -L "https://github.com/libsdl-org/SDL_ttf/releases/download/release-${{ env.SDL2_TTF }}/SDL2_ttf-devel-${{ env.SDL2_TTF }}-VC.zip" -o "sdl2_ttf_devel.zip"
|
||||
curl -L "https://github.com/libsdl-org/SDL_image/releases/download/release-${{ env.SDL2_IMAGE }}/SDL2_image-devel-${{ env.SDL2_IMAGE }}-VC.zip" -o "sdl2_image_devel.zip"
|
||||
```
|
||||
|
||||
I did take a lot of care in making sure that versions were specified externally in different variables, which took a couple tries while I learned how interpolation works with GitHub Actions.
|
||||
|
||||
Additionally, I realized that `LIB` files were required for compliation after this, so I had to painfully fix all the files to use the `-devel-` version. Speifically the one with `-VC` appended.
|
||||
|
||||
I still do not know what VC means here. Perhaps it is related to `vcpkg` somehow.
|
||||
|
||||
The next step was to extract the files I needed from the `.zip`s, but that proved quite hard. I'm a lover of precision and using tools to the best of my knowledge, so I wanted to finely take just the DLL and ZIP I needed from these archives, and nothing else.
|
||||
|
||||
While I was able to get working commands to do this on Linux, finely finding the exact DLL and placing it in `pwd`, I was not able to replicate it on the Windows-imaged GitHub Runner;
|
||||
|
||||
When specifying the `-o` flag meaning 'output directory here' like `-o./tmp` (yes, there is no space in between), it would always error with `Too short switch: -o`. I was unable to find meaningful discussions on Google.
|
||||
|
||||
My Linux machine did not complain, and I wasn't yet ready to switch OSes for an error like this, so I just extracted everything and then `mv`'d the items into `pwd`.
|
||||
|
||||
I knew what lay ahead with `SDL2_gfx`, so I tested whether the compilation error changed, and luckily, it was only erroring on the missing `SDL2_gfx.lib` at this point.
|
||||
|
||||
While reading discussions online, I came across [a reddit post](https://www.reddit.com/r/rust_gamedev/comments/am84q9/using_sdl2_gfx_on_windows/efk6uwq/) talking about `vcpkg`. I'd heard of it, but never used the program before. It seemed like it could provide `SDL2_gfx` for me without hassle.
|
||||
|
||||
And that was partly true.
|
||||
|
||||
The primary 'boon' of `vcpkg` here was that it setup and compiled `SDL2_gfx` without the hassle of messing with the compiler, options, or most importantly: dependencies.
|
||||
|
||||
I didn't know it at the time, but `SDL2_gfx` depended on `SDL2` directly, and so I'd have to setup and compile both projects, if I was hoping to do this 'manually'.
|
||||
|
||||
## VCPKG for SDL2_GFX
|
||||
|
||||
I tried to use the GitHub-provided environment variables relating to VCPKG's installation location, but nothing really worked here. I was on the correcti mage (`windows-latest` for Windows 2022 Enterprise on GitHub's Runner Images), but nothing seemed to work.
|
||||
|
||||
[This comment](https://github.com/actions/runner-images/issues/6376#issuecomment-1781269302) seemed to describe the exact same experience I was happening, several months ago.
|
||||
|
||||
Alas, I simply tried `C:\vcpkg\` and it worked, providing me the ability to install `SDL2_gfx`.
|
||||
|
||||
As it were though, the hard part wasn't going to be compiling, but locating the DLLs and LIB files for movement. No matter where I looked online or in the logs, nothing was obvious about the location of my files.
|
||||
|
||||
In retrospect, a recursive `Get-ChildItem` looking for `DLL` or `LIB` files probably would've worked well, but.. yeah...
|
||||
|
||||
After a couple attempts with various test commits, I couldn't find it, and just switched to Windows to install and compile it myself, so I could locate the file manually.
|
||||
|
||||
> Note: VCPKG is annoying to install, the executable provided by Visual Studio Community does not permit classic-mode usage, so you'll still need to clone and bootstrap VCPKG (instructions in the repository README).
|
||||
|
||||
As it happens, they were placed in
|
||||
- `$VCPKG_ROOT\packages\sdl2-gfx_x64-windows-release\bin\SDL2_gfx.dll` and
|
||||
- `$VCPKG_ROOT\packages\sdl2-gfx_x64-windows-release\lib\SDL2_gfx.lib` respectively.
|
||||
|
||||
This brings me to one issue, and one fix; while compiling you're required to specify that the build is for 64 bit systems manually, on each invocation of VCPKG (while in classic mode, which I am).
|
||||
|
||||
On top of that, they'll be built in debug mode (with extra symbols and such) by default, which I am not interested in.
|
||||
|
||||
To get the x64 Release build of a package, append `:x64-windows-release` to it, as in `sdl2-gfx:x64-windows-release` for `sdl2-gfx`.
|
||||
|
||||
After getting this sorted, I struggled a little bit in using the `mv` (`Move-Item`) command in Powershell, as I battled with the comma delimited files when moving multiple files to a given destination. Dumb.
|
||||
|
||||
This is also the point at which I renamed the executable from `pacman` to `spiritus` to differentiate the two projects. The name is just my play on the word 'sprite'.
|
||||
|
||||
## Console Window Hiding
|
||||
|
||||
When launching the demo app, I saw a console window pop up, even though I launched it from the File Explorer; this is not the behavior I was interested in.
|
||||
|
||||
I believe that apps launched from File Explorer shouldn't have a console window available unless...
|
||||
|
||||
- It's a CLI app by nature, and it uses the Console Window.
|
||||
- It has a specific debugging flag passed into it, perhaps by a Shortcut file.
|
||||
- The console window is required for the nature of the app, or it is the preferred method of log inspection.
|
||||
- It's a debug build.
|
||||
|
||||
But if it's launched from the console, then it should either
|
||||
|
||||
- Detach and relinquish control of the console back to the user.
|
||||
- Use the console actively in it's logging.
|
||||
|
||||
Most programs I know and use follow this general consensus. Naturally, mine must too.
|
||||
|
||||
But, when searching for a solution online, it seemed what I want doesn't really exist; I implemented the closest approximation.
|
||||
|
||||
If `stdout` is detected to be a `tty` (an active console), the console window won't be hidden. Otherwise, it will be hidden.
|
||||
|
||||
Unfortunately, this results in the millisecond flash of a black console window appearing.
|
||||
|
||||
## Updating Deprecated Actions
|
||||
|
||||
As it were, most of the actions I were using were deprecated in some way. It didn't feel like I was using super old actions, but I guess I was. Luckily, most of them were simply just updating the version (`2` or `3` to `4` or `5`).
|
||||
|
||||
`actions-rs@toolchain` was different though, and was officially deprecated, the GitHub repository archived. Couldn't find a good reason why, but the repository was untouched in 4 years, so maybe that's why...
|
||||
|
||||
I found `dtolnay@rust-toolchain` and switched, it more or less was perfect with no differences. I think it's sorta neat that the Rust version is specified using the version of the action. I'd be worried though of a changing feature set across different action versions...
|
||||
|
||||
I guess a well designed GitHub Action shouldn't change much, including a Rust toolchain action.
|
||||
|
||||
## Artifact Naming
|
||||
|
||||
Perhaps it's super unnecessary and won't be appreciated, but I wanted the artifact files produced by my script to have semantic meaning in it's version and target.
|
||||
|
||||
For each OS, I extracted the targets (`x86_64-unknown-linux-gnu`, `x86_64-apple-darwin`...) into an environment variable scoped at the job level (love that).
|
||||
|
||||
I looked into ways to get the package version, but nothing obvious jumped out at me. I did come across `toml-cli` though, a Rust-based CLI program analogous to `jq` (for JSON).
|
||||
|
||||
It worked great, but it was sorta slow; compiling `toml-cli` added an extra 20-60 seconds for each job. I'd heard of a Rust project to speed up builds by providing prebuilt executables though; it's called `binstall`.
|
||||
|
||||
Even cooler, it was had an action available to easily add it to my build script, and so I had `toml-cli` installing and available 10x faster!
|
||||
|
||||

|
||||
|
||||
Perhaps I could use some special bash commands to acquire the package version, but it'd be a lot of work and maintenance to get it working in both bash and Powershell, maintaining it across four jobs.
|
||||
|
||||
This is both cool, fast, and easy!
|
||||
|
||||

|
||||
|
||||
I was thinking of a github-pages artifact name that aligns with the others, but I think that'd be stupid AND overkill.
|
||||
|
||||
Perhaps at the least I'll look into a 32-bit build for Windows, just for demonstration purposes.
|
||||
|
||||
|
||||
[code-review-video]: https://www.youtube.com/watch?v=OKs_JewEeOo
|
||||
[code-review-thumbnail]: https://img.youtube.com/vi/OKs_JewEeOo/hqdefault.jpg
|
||||
[fighting-lifetimes-1]: https://devcry.heiho.net/html/2022/20220709-rust-and-sdl2-fighting-with-lifetimes.html
|
||||
[fighting-lifetimes-2]: https://devcry.heiho.net/html/2022/20220716-rust-and-sdl2-fighting-with-lifetimes-2.html
|
||||
[fighting-lifetimes-3]: https://devcry.heiho.net/html/2022/20220724-rust-and-sdl2-fighting-with-lifetimes-3.html
|
||||
[ruggrogue]: https://tung.github.io/ruggrogue/
|
||||
BIN
assets/TerminalVector.ttf
Normal file
BIN
assets/TerminalVector.ttf
Normal file
Binary file not shown.
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/font/konami.ttf
Normal file
BIN
assets/font/konami.ttf
Normal file
Binary file not shown.
65
assets/index.html
Normal file
65
assets/index.html
Normal file
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Pac-Man Arcade</title>
|
||||
<link rel="stylesheet" href="build.css" />
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-black text-yellow-400 text-center">
|
||||
<a
|
||||
href="https://github.com/Xevion/Pac-Man"
|
||||
class="absolute top-0 right-0"
|
||||
aria-label="View source on GitHub"
|
||||
>
|
||||
<svg
|
||||
width="80"
|
||||
height="80"
|
||||
viewBox="0 0 250 250"
|
||||
class="fill-yellow-400 text-black"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||
<path
|
||||
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
|
||||
class="octo-arm"
|
||||
></path>
|
||||
<path
|
||||
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
|
||||
class="octo-body"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="text-4xl mt-10 scaled-text">Pac-Man Arcade</h1>
|
||||
<p class="text-lg mt-5 scaled-text">
|
||||
Welcome to the Pac-Man Arcade! Use the controls below to play.
|
||||
</p>
|
||||
<canvas
|
||||
id="canvas"
|
||||
class="block mx-auto mt-5"
|
||||
width="800"
|
||||
height="600"
|
||||
></canvas>
|
||||
<div class="mt-10">
|
||||
<span
|
||||
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
|
||||
>← ↑ → ↓ Move</span
|
||||
>
|
||||
<span
|
||||
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
|
||||
>Space Change Sprite</span
|
||||
>
|
||||
<span
|
||||
class="inline-block mx-2 px-4 py-2 bg-yellow-400 text-black rounded scaled-text"
|
||||
>Shift + ↑↓ Change Volume</span
|
||||
>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var Module = {
|
||||
canvas: document.getElementById("canvas"),
|
||||
};
|
||||
</script>
|
||||
<script type="text/javascript" src="pacman.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
assets/styles.scss
Normal file
21
assets/styles.scss
Normal file
@@ -0,0 +1,21 @@
|
||||
@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;
|
||||
}
|
||||
BIN
assets/wav/1.ogg
Normal file
BIN
assets/wav/1.ogg
Normal file
Binary file not shown.
BIN
assets/wav/2.ogg
Normal file
BIN
assets/wav/2.ogg
Normal file
Binary file not shown.
BIN
assets/wav/3.ogg
Normal file
BIN
assets/wav/3.ogg
Normal file
Binary file not shown.
BIN
assets/wav/4.ogg
Normal file
BIN
assets/wav/4.ogg
Normal file
Binary file not shown.
BIN
assets/wav/eating.wav
Normal file
BIN
assets/wav/eating.wav
Normal file
Binary file not shown.
BIN
assets/wav/waka_ka.wav
Normal file
BIN
assets/wav/waka_ka.wav
Normal file
Binary file not shown.
BIN
assets/wav/waka_wa.wav
Normal file
BIN
assets/wav/waka_wa.wav
Normal file
Binary file not shown.
@@ -1,7 +0,0 @@
|
||||
& cargo build --target=wasm32-unknown-emscripten --release
|
||||
|
||||
mkdir -p dist -Force
|
||||
|
||||
cp ./target/wasm32-unknown-emscripten/release/Pac_Man.wasm ./dist
|
||||
cp ./target/wasm32-unknown-emscripten/release/Pac-Man.js ./dist
|
||||
cp index.html dist
|
||||
78
build.sh
Executable file → Normal file
78
build.sh
Executable file → Normal file
@@ -1,10 +1,76 @@
|
||||
#!/bin/sh
|
||||
set -eux
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
cargo build --target=wasm32-unknown-emscripten --release
|
||||
release='false'
|
||||
serve='false'
|
||||
skip_emsdk='false'
|
||||
clean='false'
|
||||
|
||||
print_usage() {
|
||||
printf "Usage: -erdsc\n"
|
||||
printf " -e: Skip EMSDK setup (GitHub workflow only)\n"
|
||||
printf " -r: Build in release mode\n"
|
||||
printf " -d: Build in debug mode\n"
|
||||
printf " -s: Serve the WASM files once built\n"
|
||||
printf " -c: Clean the target/dist directory\n"
|
||||
}
|
||||
|
||||
while getopts 'erdsc' flag; do
|
||||
case "${flag}" in
|
||||
e) skip_emsdk='true' ;;
|
||||
r) release='true' ;;
|
||||
d) release='false' ;; # doesn't actually do anything, but last flag wins
|
||||
s) serve='true' ;;
|
||||
c) clean='true' ;;
|
||||
*)
|
||||
print_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$clean" = 'true' ]; then
|
||||
echo "Cleaning target directory"
|
||||
cargo clean
|
||||
rm -rf ./dist/
|
||||
fi
|
||||
|
||||
if [ "$skip_emsdk" = 'false' ]; then
|
||||
echo "Activating Emscripten"
|
||||
# SDL2-TTF requires 3.1.43, fails to build on latest
|
||||
../emsdk/emsdk activate 3.1.43
|
||||
source ../emsdk/emsdk_env.sh
|
||||
fi
|
||||
|
||||
echo "Building WASM with Emscripten"
|
||||
build_type='debug'
|
||||
if [ "$release" = 'true' ]; then
|
||||
cargo build --target=wasm32-unknown-emscripten --release
|
||||
build_type='release'
|
||||
else
|
||||
cargo build --target=wasm32-unknown-emscripten
|
||||
fi
|
||||
|
||||
echo "Generating CSS"
|
||||
pnpx postcss-cli ./assets/styles.scss -o ./assets/build.css
|
||||
|
||||
echo "Copying WASM files"
|
||||
mkdir -p dist
|
||||
output_folder="target/wasm32-unknown-emscripten/$build_type"
|
||||
cp assets/index.html dist
|
||||
|
||||
cp target/wasm32-unknown-emscripten/release/Pac_Man.wasm dist
|
||||
cp target/wasm32-unknown-emscripten/release/Pac-Man.js dist
|
||||
cp index.html dist
|
||||
# cp assets/*.woff* dist
|
||||
cp assets/build.css dist
|
||||
cp assets/favicon.ico dist
|
||||
cp $output_folder/pacman.wasm dist
|
||||
cp $output_folder/pacman.js dist
|
||||
# only if .data file exists
|
||||
cp $output_folder/deps/pacman.data dist
|
||||
if [ -f $output_folder/pacman.wasm.map ]; then
|
||||
cp $output_folder/pacman.wasm.map dist
|
||||
fi
|
||||
|
||||
if [ "$serve" = 'true' ]; then
|
||||
echo "Serving WASM with Emscripten"
|
||||
python3 -m http.server -d ./dist/ 8080
|
||||
fi
|
||||
|
||||
194
build.ts
Normal file
194
build.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import { $ } from "bun";
|
||||
|
||||
// This is a bun script, run with `bun run build.ts`
|
||||
|
||||
import * as path from "path";
|
||||
import * as fs from "fs/promises";
|
||||
|
||||
async function clean() {
|
||||
console.log("Cleaning...");
|
||||
await $`cargo clean`;
|
||||
await $`rm -rf ./dist/`;
|
||||
console.log("Cleaned...");
|
||||
}
|
||||
|
||||
async function setupEmscripten() {
|
||||
const emsdkDir = "./emsdk";
|
||||
const emsdkExists = await fs
|
||||
.access(emsdkDir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!emsdkExists) {
|
||||
console.log("Cloning Emscripten SDK...");
|
||||
await $`git clone https://github.com/emscripten-core/emsdk.git`;
|
||||
} else {
|
||||
console.log("Emscripten SDK already exists, skipping clone.");
|
||||
}
|
||||
|
||||
const emscriptenToolchainPath = path.join(emsdkDir, "upstream", "emscripten");
|
||||
const toolchainInstalled = await fs
|
||||
.access(emscriptenToolchainPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!toolchainInstalled) {
|
||||
console.log("Installing Emscripten toolchain...");
|
||||
await $`./emsdk/emsdk install 3.1.43`;
|
||||
} else {
|
||||
console.log(
|
||||
"Emscripten toolchain 3.1.43 already installed, skipping install."
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Activating Emscripten...");
|
||||
await $`./emsdk/emsdk activate 3.1.43`;
|
||||
console.log("Emscripten activated.");
|
||||
|
||||
// Set EMSDK environment variable for subsequent commands
|
||||
process.env.EMSDK = path.resolve(emsdkDir);
|
||||
|
||||
const emsdkPython = path.join(path.resolve(emsdkDir), "python");
|
||||
const emsdkNode = path.join(path.resolve(emsdkDir), "node", "16.20.0_64bit"); // Adjust node version if needed
|
||||
const emsdkBin = path.join(path.resolve(emsdkDir), "upstream", "emscripten");
|
||||
process.env.PATH = `${emsdkPython}:${emsdkNode}:${emsdkBin}:${process.env.PATH}`;
|
||||
}
|
||||
|
||||
async function buildWeb(release: boolean) {
|
||||
console.log("Building WASM with Emscripten...");
|
||||
if (release) {
|
||||
await $`cargo build --target=wasm32-unknown-emscripten --release`;
|
||||
} else {
|
||||
await $`cargo build --target=wasm32-unknown-emscripten`;
|
||||
}
|
||||
|
||||
console.log("Generating CSS...");
|
||||
await $`pnpx postcss-cli ./assets/styles.scss -o ./assets/build.css`;
|
||||
|
||||
console.log("Copying WASM files...");
|
||||
const buildType = release ? "release" : "debug";
|
||||
const outputFolder = `target/wasm32-unknown-emscripten/${buildType}`;
|
||||
await $`mkdir -p dist`;
|
||||
await $`cp assets/index.html dist`;
|
||||
await $`cp assets/*.woff* dist`;
|
||||
await $`cp assets/build.css dist`;
|
||||
await $`cp assets/favicon.ico dist`;
|
||||
await $`cp ${outputFolder}/spiritus.wasm dist`;
|
||||
await $`cp ${outputFolder}/spiritus.js dist`;
|
||||
|
||||
// Check if .data file exists before copying
|
||||
try {
|
||||
await fs.access(`${outputFolder}/deps/spiritus.data`);
|
||||
await $`cp ${outputFolder}/deps/spiritus.data dist`;
|
||||
} catch (e) {
|
||||
console.log("No spiritus.data file found, skipping copy.");
|
||||
}
|
||||
|
||||
// Check if .map file exists before copying
|
||||
try {
|
||||
await fs.access(`${outputFolder}/spiritus.wasm.map`);
|
||||
await $`cp ${outputFolder}/spiritus.wasm.map dist`;
|
||||
} catch (e) {
|
||||
console.log("No spiritus.wasm.map file found, skipping copy.");
|
||||
}
|
||||
|
||||
console.log("WASM files copied.");
|
||||
}
|
||||
|
||||
async function serve() {
|
||||
console.log("Serving WASM with Emscripten...");
|
||||
await $`python3 -m http.server -d ./dist/ 8080`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
let release = false;
|
||||
let serveFiles = false;
|
||||
let skipEmscriptenSetup = false;
|
||||
let cleanProject = false;
|
||||
let target = "web"; // Default target
|
||||
|
||||
for (const arg of args) {
|
||||
switch (arg) {
|
||||
case "-r":
|
||||
release = true;
|
||||
break;
|
||||
case "-s":
|
||||
serveFiles = true;
|
||||
break;
|
||||
case "-e":
|
||||
skipEmscriptenSetup = true;
|
||||
break;
|
||||
case "-c":
|
||||
cleanProject = true;
|
||||
break;
|
||||
case "--target=linux":
|
||||
target = "linux";
|
||||
break;
|
||||
case "--target=windows":
|
||||
target = "windows";
|
||||
break;
|
||||
case "--target=web":
|
||||
target = "web";
|
||||
break;
|
||||
case "-h":
|
||||
case "--help":
|
||||
console.log(`
|
||||
Usage: ts-node build.ts [options]
|
||||
|
||||
Options:
|
||||
-r Build in release mode
|
||||
-s Serve the WASM files once built (for web target)
|
||||
-e Skip EMSDK setup (GitHub workflow only)
|
||||
-c Clean the target/dist directory
|
||||
--target=[web|linux|windows] Specify target platform (default: web)
|
||||
-h, --help Show this help message
|
||||
`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanProject) {
|
||||
await clean();
|
||||
}
|
||||
|
||||
if (!skipEmscriptenSetup && target === "web") {
|
||||
await setupEmscripten();
|
||||
}
|
||||
|
||||
switch (target) {
|
||||
case "web":
|
||||
await buildWeb(release);
|
||||
if (serveFiles) {
|
||||
await serve();
|
||||
}
|
||||
break;
|
||||
case "linux":
|
||||
console.log("Building for Linux...");
|
||||
if (release) {
|
||||
await $`cargo build --release`;
|
||||
} else {
|
||||
await $`cargo build`;
|
||||
}
|
||||
console.log("Linux build complete.");
|
||||
break;
|
||||
case "windows":
|
||||
console.log("Building for Windows...");
|
||||
if (release) {
|
||||
await $`cargo build --release --target=x86_64-pc-windows-msvc`; // Assuming MSVC toolchain
|
||||
} else {
|
||||
await $`cargo build --target=x86_64-pc-windows-msvc`;
|
||||
}
|
||||
console.log("Windows build complete.");
|
||||
break;
|
||||
default:
|
||||
console.error("Invalid target specified.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
21
index.html
21
index.html
@@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="text/javascript">
|
||||
let Module = {
|
||||
canvas: (function () {
|
||||
// this is how we provide a canvas to our sdl2
|
||||
return document.getElementById("canvas");
|
||||
})(),
|
||||
preRun: [function () {
|
||||
ENV.RUST_LOG = "info,wgpu=warn"
|
||||
}]
|
||||
};
|
||||
</script>
|
||||
<script src="Pac-Man.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
166
src/animation.rs
166
src/animation.rs
@@ -1,3 +1,4 @@
|
||||
//! This module provides a simple animation and atlas system for textures.
|
||||
use sdl2::{
|
||||
rect::Rect,
|
||||
render::{Canvas, Texture},
|
||||
@@ -6,31 +7,36 @@ use sdl2::{
|
||||
|
||||
use crate::direction::Direction;
|
||||
|
||||
pub struct AnimatedTexture<'a> {
|
||||
raw_texture: Texture<'a>,
|
||||
ticker: u32,
|
||||
reversed: bool,
|
||||
offset: (i32, i32),
|
||||
ticks_per_frame: u32,
|
||||
frame_count: u32,
|
||||
frame_width: u32,
|
||||
frame_height: u32,
|
||||
/// Trait for drawable atlas-based textures
|
||||
pub trait FrameDrawn {
|
||||
fn render(
|
||||
&self,
|
||||
canvas: &mut Canvas<Window>,
|
||||
position: (i32, i32),
|
||||
direction: Direction,
|
||||
frame: Option<u32>,
|
||||
);
|
||||
}
|
||||
|
||||
impl<'a> AnimatedTexture<'a> {
|
||||
/// A texture atlas abstraction for static (non-animated) rendering.
|
||||
pub struct AtlasTexture<'a> {
|
||||
pub raw_texture: Texture<'a>,
|
||||
pub offset: (i32, i32),
|
||||
pub frame_count: u32,
|
||||
pub frame_width: u32,
|
||||
pub frame_height: u32,
|
||||
}
|
||||
|
||||
impl<'a> AtlasTexture<'a> {
|
||||
pub fn new(
|
||||
texture: Texture<'a>,
|
||||
ticks_per_frame: u32,
|
||||
frame_count: u32,
|
||||
frame_width: u32,
|
||||
frame_height: u32,
|
||||
offset: Option<(i32, i32)>,
|
||||
) -> Self {
|
||||
AnimatedTexture {
|
||||
AtlasTexture {
|
||||
raw_texture: texture,
|
||||
ticker: 0,
|
||||
reversed: false,
|
||||
ticks_per_frame,
|
||||
frame_count,
|
||||
frame_width,
|
||||
frame_height,
|
||||
@@ -38,61 +44,127 @@ impl<'a> AnimatedTexture<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn current_frame(&self) -> u32 {
|
||||
self.ticker / self.ticks_per_frame
|
||||
}
|
||||
|
||||
fn next_frame(&mut self) {
|
||||
if self.reversed {
|
||||
self.ticker -= 1;
|
||||
|
||||
if self.ticker == 0 {
|
||||
self.reversed = !self.reversed;
|
||||
}
|
||||
} else {
|
||||
self.ticker += 1;
|
||||
|
||||
if self.ticker + 1 == self.ticks_per_frame * self.frame_count {
|
||||
self.reversed = !self.reversed;
|
||||
}
|
||||
pub fn get_frame_rect(&self, frame: u32) -> Option<Rect> {
|
||||
if frame >= self.frame_count {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_frame_rect(&self) -> Rect {
|
||||
Rect::new(
|
||||
self.current_frame() as i32 * self.frame_width as i32,
|
||||
Some(Rect::new(
|
||||
frame as i32 * self.frame_width as i32,
|
||||
0,
|
||||
self.frame_width,
|
||||
self.frame_height,
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
|
||||
self.raw_texture.set_color_mod(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FrameDrawn for AtlasTexture<'a> {
|
||||
fn render(
|
||||
&self,
|
||||
canvas: &mut Canvas<Window>,
|
||||
position: (i32, i32),
|
||||
direction: Direction,
|
||||
frame: Option<u32>,
|
||||
) {
|
||||
let frame_rect = self.get_frame_rect();
|
||||
let position_rect = Rect::new(
|
||||
let texture_source_frame_rect = self.get_frame_rect(frame.unwrap_or(0));
|
||||
let canvas_destination_rect = Rect::new(
|
||||
position.0 + self.offset.0,
|
||||
position.1 + self.offset.1,
|
||||
self.frame_width,
|
||||
self.frame_height,
|
||||
);
|
||||
|
||||
canvas
|
||||
.copy_ex(
|
||||
&self.raw_texture,
|
||||
Some(frame_rect),
|
||||
Some(position_rect),
|
||||
texture_source_frame_rect,
|
||||
Some(canvas_destination_rect),
|
||||
direction.angle(),
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.expect("Could not render texture on canvas");
|
||||
|
||||
self.next_frame();
|
||||
}
|
||||
}
|
||||
|
||||
/// An animated texture using a texture atlas.
|
||||
pub struct AnimatedAtlasTexture<'a> {
|
||||
pub atlas: AtlasTexture<'a>,
|
||||
pub ticks_per_frame: u32,
|
||||
pub ticker: u32,
|
||||
pub reversed: bool,
|
||||
pub paused: bool,
|
||||
}
|
||||
|
||||
impl<'a> AnimatedAtlasTexture<'a> {
|
||||
pub fn new(
|
||||
texture: Texture<'a>,
|
||||
ticks_per_frame: u32,
|
||||
frame_count: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
offset: Option<(i32, i32)>,
|
||||
) -> Self {
|
||||
AnimatedAtlasTexture {
|
||||
atlas: AtlasTexture::new(texture, frame_count, width, height, offset),
|
||||
ticks_per_frame,
|
||||
ticker: 0,
|
||||
reversed: false,
|
||||
paused: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_frame(&self) -> u32 {
|
||||
self.ticker / self.ticks_per_frame
|
||||
}
|
||||
|
||||
/// Advances the animation by one tick, unless paused.
|
||||
pub fn tick(&mut self) {
|
||||
if self.paused {
|
||||
return;
|
||||
}
|
||||
if self.reversed {
|
||||
if self.ticker > 0 {
|
||||
self.ticker -= 1;
|
||||
}
|
||||
if self.ticker == 0 {
|
||||
self.reversed = !self.reversed;
|
||||
}
|
||||
} else {
|
||||
self.ticker += 1;
|
||||
if self.ticker + 1 == self.ticks_per_frame * self.atlas.frame_count {
|
||||
self.reversed = !self.reversed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pause(&mut self) {
|
||||
self.paused = true;
|
||||
}
|
||||
pub fn resume(&mut self) {
|
||||
self.paused = false;
|
||||
}
|
||||
pub fn is_paused(&self) -> bool {
|
||||
self.paused
|
||||
}
|
||||
|
||||
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
|
||||
self.atlas.set_color_modulation(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FrameDrawn for AnimatedAtlasTexture<'a> {
|
||||
fn render(
|
||||
&self,
|
||||
canvas: &mut Canvas<Window>,
|
||||
position: (i32, i32),
|
||||
direction: Direction,
|
||||
frame: Option<u32>,
|
||||
) {
|
||||
let frame = frame.unwrap_or_else(|| self.current_frame());
|
||||
self.atlas.render(canvas, position, direction, Some(frame));
|
||||
}
|
||||
}
|
||||
|
||||
74
src/asset.rs
Normal file
74
src/asset.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! Cross-platform asset loading abstraction.
|
||||
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AssetError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Asset not found: {0}")]
|
||||
NotFound(String),
|
||||
#[error("Invalid asset format: {0}")]
|
||||
InvalidFormat(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Asset {
|
||||
Wav1,
|
||||
Wav2,
|
||||
Wav3,
|
||||
Wav4,
|
||||
Pacman,
|
||||
Pellet,
|
||||
Energizer,
|
||||
Map,
|
||||
FontKonami,
|
||||
GhostBody,
|
||||
GhostEyes,
|
||||
// Add more as needed
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
mod imp {
|
||||
use super::*;
|
||||
macro_rules! asset_bytes_enum {
|
||||
( $asset:expr ) => {
|
||||
match $asset {
|
||||
Asset::Wav1 => Cow::Borrowed(include_bytes!("../assets/wav/1.ogg")),
|
||||
Asset::Wav2 => Cow::Borrowed(include_bytes!("../assets/wav/2.ogg")),
|
||||
Asset::Wav3 => Cow::Borrowed(include_bytes!("../assets/wav/3.ogg")),
|
||||
Asset::Wav4 => Cow::Borrowed(include_bytes!("../assets/wav/4.ogg")),
|
||||
Asset::Pacman => Cow::Borrowed(include_bytes!("../assets/32/pacman.png")),
|
||||
Asset::Pellet => Cow::Borrowed(include_bytes!("../assets/24/pellet.png")),
|
||||
Asset::Energizer => Cow::Borrowed(include_bytes!("../assets/24/energizer.png")),
|
||||
Asset::Map => Cow::Borrowed(include_bytes!("../assets/map.png")),
|
||||
Asset::FontKonami => Cow::Borrowed(include_bytes!("../assets/font/konami.ttf")),
|
||||
Asset::GhostBody => Cow::Borrowed(include_bytes!("../assets/32/ghost_body.png")),
|
||||
Asset::GhostEyes => Cow::Borrowed(include_bytes!("../assets/32/ghost_eyes.png")),
|
||||
}
|
||||
};
|
||||
}
|
||||
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
|
||||
Ok(asset_bytes_enum!(asset))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
mod imp {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
|
||||
let path = Path::new("assets").join(asset.path());
|
||||
if !path.exists() {
|
||||
return Err(AssetError::NotFound(asset.path().to_string()));
|
||||
}
|
||||
let bytes = fs::read(&path)?;
|
||||
Ok(Cow::Owned(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
pub use imp::get_asset_bytes;
|
||||
77
src/audio.rs
Normal file
77
src/audio.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! This module handles the audio playback for the game.
|
||||
use crate::asset::{get_asset_bytes, Asset};
|
||||
use sdl2::{
|
||||
mixer::{self, Chunk, InitFlag, LoaderRWops, DEFAULT_FORMAT},
|
||||
rwops::RWops,
|
||||
};
|
||||
|
||||
const SOUND_ASSETS: [Asset; 4] = [Asset::Wav1, Asset::Wav2, Asset::Wav3, Asset::Wav4];
|
||||
|
||||
/// The audio system for the game.
|
||||
///
|
||||
/// This struct is responsible for initializing the audio device, loading sounds,
|
||||
/// and playing them.
|
||||
pub struct Audio {
|
||||
_mixer_context: mixer::Sdl2MixerContext,
|
||||
sounds: Vec<Chunk>,
|
||||
next_sound_index: usize,
|
||||
}
|
||||
|
||||
impl Audio {
|
||||
/// Creates a new `Audio` instance.
|
||||
pub fn new() -> Self {
|
||||
let frequency = 44100;
|
||||
let format = DEFAULT_FORMAT;
|
||||
let channels = 4;
|
||||
let chunk_size = 128;
|
||||
mixer::open_audio(frequency, format, 1, chunk_size).expect("Failed to open audio");
|
||||
mixer::allocate_channels(channels);
|
||||
|
||||
// set channel volume
|
||||
for i in 0..channels {
|
||||
mixer::Channel(i).set_volume(32);
|
||||
}
|
||||
|
||||
let mixer_context = mixer::init(InitFlag::OGG).expect("Failed to initialize SDL2_mixer");
|
||||
|
||||
let sounds: Vec<Chunk> = SOUND_ASSETS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, asset)| {
|
||||
let data = get_asset_bytes(*asset).expect("Failed to load sound asset");
|
||||
let rwops = RWops::from_bytes(&data)
|
||||
.unwrap_or_else(|_| panic!("Failed to create RWops for sound {}", i + 1));
|
||||
rwops
|
||||
.load_wav()
|
||||
.unwrap_or_else(|_| panic!("Failed to load sound {} from asset API", i + 1))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Audio {
|
||||
_mixer_context: mixer_context,
|
||||
sounds,
|
||||
next_sound_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Plays the "eat" sound effect.
|
||||
///
|
||||
/// This function also logs the time since the last sound effect was played.
|
||||
pub fn eat(&mut self) {
|
||||
if let Some(chunk) = self.sounds.get(self.next_sound_index) {
|
||||
match mixer::Channel(0).play(chunk, 0) {
|
||||
Ok(channel) => {
|
||||
tracing::info!(
|
||||
"Playing sound #{} on channel {:?}",
|
||||
self.next_sound_index + 1,
|
||||
channel
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Could not play sound #{}: {}", self.next_sound_index + 1, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.next_sound_index = (self.next_sound_index + 1) % self.sounds.len();
|
||||
}
|
||||
}
|
||||
129
src/constants.rs
129
src/constants.rs
@@ -1,25 +1,95 @@
|
||||
use lazy_static::lazy_static;
|
||||
//! This module contains all the constants used in the game.
|
||||
|
||||
/// The width of the game board, in cells.
|
||||
pub const BOARD_WIDTH: u32 = 28;
|
||||
pub const BOARD_HEIGHT: u32 = 37; // Adjusted to fit map texture?
|
||||
/// The height of the game board, in cells.
|
||||
pub const BOARD_HEIGHT: u32 = 31;
|
||||
/// The size of each cell, in pixels.
|
||||
pub const CELL_SIZE: u32 = 24;
|
||||
|
||||
pub const WINDOW_WIDTH: u32 = CELL_SIZE * BOARD_WIDTH;
|
||||
pub const WINDOW_HEIGHT: u32 = CELL_SIZE * BOARD_HEIGHT;
|
||||
/// The offset of the game board from the top-left corner of the window, in
|
||||
/// cells.
|
||||
pub const BOARD_OFFSET: (u32, u32) = (0, 3);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
/// The width of the window, in pixels.
|
||||
pub const WINDOW_WIDTH: u32 = CELL_SIZE * BOARD_WIDTH;
|
||||
/// The height of the window, in pixels.
|
||||
///
|
||||
/// The map texture is 6 cells taller than the grid (3 above, 3 below), so we
|
||||
/// add 6 to the board height to get the window height.
|
||||
pub const WINDOW_HEIGHT: u32 = CELL_SIZE * (BOARD_HEIGHT + 6);
|
||||
|
||||
/// An enum representing the different types of tiles on the map.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum MapTile {
|
||||
/// An empty tile.
|
||||
Empty,
|
||||
/// A wall tile.
|
||||
Wall,
|
||||
/// A regular pellet.
|
||||
Pellet,
|
||||
/// A power pellet.
|
||||
PowerPellet,
|
||||
/// A starting position for an entity.
|
||||
StartingPosition(u8),
|
||||
/// A tunnel tile.
|
||||
Tunnel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(u8)]
|
||||
pub enum FruitType {
|
||||
Cherry,
|
||||
Strawberry,
|
||||
Orange,
|
||||
Apple,
|
||||
Melon,
|
||||
Galaxian,
|
||||
Bell,
|
||||
Key,
|
||||
}
|
||||
|
||||
impl FruitType {
|
||||
pub const ALL: [FruitType; 8] = [
|
||||
FruitType::Cherry,
|
||||
FruitType::Strawberry,
|
||||
FruitType::Orange,
|
||||
FruitType::Apple,
|
||||
FruitType::Melon,
|
||||
FruitType::Galaxian,
|
||||
FruitType::Bell,
|
||||
FruitType::Key,
|
||||
];
|
||||
|
||||
pub fn score(self) -> u32 {
|
||||
match self {
|
||||
FruitType::Cherry => 100,
|
||||
FruitType::Strawberry => 300,
|
||||
FruitType::Orange => 500,
|
||||
FruitType::Apple => 700,
|
||||
FruitType::Melon => 1000,
|
||||
FruitType::Galaxian => 2000,
|
||||
FruitType::Bell => 3000,
|
||||
FruitType::Key => 5000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(self) -> usize {
|
||||
match self {
|
||||
FruitType::Cherry => 0,
|
||||
FruitType::Strawberry => 1,
|
||||
FruitType::Orange => 2,
|
||||
FruitType::Apple => 3,
|
||||
FruitType::Melon => 4,
|
||||
FruitType::Galaxian => 5,
|
||||
FruitType::Bell => 6,
|
||||
FruitType::Key => 7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw layout of the game board, as a 2D array of characters.
|
||||
pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"############################",
|
||||
"#............##............#",
|
||||
"#.####.#####.##.#####.####.#",
|
||||
@@ -34,7 +104,7 @@ pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
|
||||
" #.## 1 ##.# ",
|
||||
" #.## ###==### ##.# ",
|
||||
"######.## # # ##.######",
|
||||
" . #2 3 4 # . ",
|
||||
"T . #2 3 4 # . T",
|
||||
"######.## # # ##.######",
|
||||
" #.## ######## ##.# ",
|
||||
" #.## ##.# ",
|
||||
@@ -51,45 +121,4 @@ pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
|
||||
"#.##########.##.##########.#",
|
||||
"#..........................#",
|
||||
"############################",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
];
|
||||
|
||||
lazy_static! {
|
||||
pub static ref BOARD: [[MapTile; BOARD_HEIGHT as usize]; BOARD_HEIGHT as usize] = {
|
||||
let mut board = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_HEIGHT as usize];
|
||||
|
||||
for y in 0..BOARD_HEIGHT as usize {
|
||||
let line = RAW_BOARD[y];
|
||||
|
||||
for x in 0..BOARD_WIDTH as usize {
|
||||
if x >= line.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let i = (y * (BOARD_WIDTH as usize) + x) as usize;
|
||||
let character = line
|
||||
.chars()
|
||||
.nth(x as usize)
|
||||
.unwrap_or_else(|| panic!("Could not get character at {} = ({}, {})", i, x, y));
|
||||
|
||||
let tile = match character {
|
||||
'#' => MapTile::Wall,
|
||||
'.' => MapTile::Pellet,
|
||||
'o' => MapTile::PowerPellet,
|
||||
' ' => MapTile::Empty,
|
||||
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => {
|
||||
MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)
|
||||
},
|
||||
'=' => MapTile::Empty,
|
||||
_ => panic!("Unknown character in board: {}", character),
|
||||
};
|
||||
|
||||
board[x as usize][y as usize] = tile;
|
||||
}
|
||||
}
|
||||
|
||||
board
|
||||
};
|
||||
}
|
||||
|
||||
74
src/debug.rs
Normal file
74
src/debug.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
//! Debug rendering utilities for Pac-Man.
|
||||
use crate::{
|
||||
constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH},
|
||||
ghosts::blinky::Blinky,
|
||||
map::Map,
|
||||
};
|
||||
use sdl2::{pixels::Color, render::Canvas, video::Window};
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum DebugMode {
|
||||
None,
|
||||
Grid,
|
||||
Pathfinding,
|
||||
ValidPositions,
|
||||
}
|
||||
|
||||
pub struct DebugRenderer;
|
||||
|
||||
impl DebugRenderer {
|
||||
pub fn draw_cell(canvas: &mut Canvas<Window>, _map: &Map, cell: (u32, u32), color: Color) {
|
||||
let position = Map::cell_to_pixel(cell);
|
||||
canvas.set_draw_color(color);
|
||||
canvas
|
||||
.draw_rect(sdl2::rect::Rect::new(position.0, position.1, 24, 24))
|
||||
.expect("Could not draw rectangle");
|
||||
}
|
||||
|
||||
pub fn draw_debug_grid(canvas: &mut Canvas<Window>, map: &Map, pacman_cell: (u32, u32)) {
|
||||
for x in 0..BOARD_WIDTH {
|
||||
for y in 0..BOARD_HEIGHT {
|
||||
let tile = map.get_tile((x as i32, y as i32)).unwrap_or(MapTile::Empty);
|
||||
let mut color = None;
|
||||
if (x, y) == pacman_cell {
|
||||
Self::draw_cell(canvas, map, (x, y), Color::CYAN);
|
||||
} else {
|
||||
color = match tile {
|
||||
MapTile::Empty => None,
|
||||
MapTile::Wall => Some(Color::BLUE),
|
||||
MapTile::Pellet => Some(Color::RED),
|
||||
MapTile::PowerPellet => Some(Color::MAGENTA),
|
||||
MapTile::StartingPosition(_) => Some(Color::GREEN),
|
||||
MapTile::Tunnel => Some(Color::CYAN),
|
||||
};
|
||||
}
|
||||
if let Some(color) = color {
|
||||
Self::draw_cell(canvas, map, (x, y), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_next_cell(canvas: &mut Canvas<Window>, map: &Map, next_cell: (u32, u32)) {
|
||||
Self::draw_cell(canvas, map, next_cell, Color::YELLOW);
|
||||
}
|
||||
|
||||
pub fn draw_valid_positions(canvas: &mut Canvas<Window>, map: &mut Map) {
|
||||
let valid_positions_vec = map.get_valid_playable_positions().clone();
|
||||
for &pos in &valid_positions_vec {
|
||||
Self::draw_cell(canvas, map, (pos.x, pos.y), Color::RGB(255, 140, 0));
|
||||
// ORANGE
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_pathfinding(canvas: &mut Canvas<Window>, blinky: &Blinky, map: &Map) {
|
||||
if let Some((path, _)) = blinky.get_path_to_target({
|
||||
let (tx, ty) = blinky.get_target_tile();
|
||||
(tx as u32, ty as u32)
|
||||
}) {
|
||||
for &(x, y) in &path {
|
||||
Self::draw_cell(canvas, map, (x, y), Color::YELLOW);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
//! This module defines the `Direction` enum, which is used to represent the
|
||||
//! direction of an entity.
|
||||
use sdl2::keyboard::Keycode;
|
||||
|
||||
/// An enum representing the direction of an entity.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum Direction {
|
||||
Up,
|
||||
@@ -7,6 +12,7 @@ pub enum Direction {
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
/// Returns the angle of the direction in degrees.
|
||||
pub fn angle(&self) -> f64 {
|
||||
match self {
|
||||
Direction::Right => 0f64,
|
||||
@@ -15,4 +21,39 @@ impl Direction {
|
||||
Direction::Up => 270f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the offset of the direction as a tuple of (x, y).
|
||||
pub fn offset(&self) -> (i32, i32) {
|
||||
match self {
|
||||
Direction::Right => (1, 0),
|
||||
Direction::Down => (0, 1),
|
||||
Direction::Left => (-1, 0),
|
||||
Direction::Up => (0, -1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the opposite direction.
|
||||
pub fn opposite(&self) -> Direction {
|
||||
match self {
|
||||
Direction::Right => Direction::Left,
|
||||
Direction::Down => Direction::Up,
|
||||
Direction::Left => Direction::Right,
|
||||
Direction::Up => Direction::Down,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `Direction` from a `Keycode`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keycode` - The keycode to convert.
|
||||
pub fn from_keycode(keycode: Keycode) -> Option<Direction> {
|
||||
match keycode {
|
||||
Keycode::D | Keycode::Right => Some(Direction::Right),
|
||||
Keycode::A | Keycode::Left => Some(Direction::Left),
|
||||
Keycode::W | Keycode::Up => Some(Direction::Up),
|
||||
Keycode::S | Keycode::Down => Some(Direction::Down),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
86
src/edible.rs
Normal file
86
src/edible.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! Edible entity for Pac-Man: pellets, power pellets, and fruits.
|
||||
use crate::animation::{AtlasTexture, FrameDrawn};
|
||||
use crate::constants::{FruitType, MapTile, BOARD_HEIGHT, BOARD_WIDTH};
|
||||
use crate::direction::Direction;
|
||||
use crate::entity::{Entity, Renderable, StaticEntity};
|
||||
use crate::map::Map;
|
||||
use sdl2::{render::Canvas, video::Window};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EdibleKind {
|
||||
Pellet,
|
||||
PowerPellet,
|
||||
Fruit(FruitType),
|
||||
}
|
||||
|
||||
pub struct Edible<'a> {
|
||||
pub base: StaticEntity,
|
||||
pub kind: EdibleKind,
|
||||
pub sprite: Rc<AtlasTexture<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Edible<'a> {
|
||||
pub fn new(kind: EdibleKind, cell_position: (u32, u32), sprite: Rc<AtlasTexture<'a>>) -> Self {
|
||||
let pixel_position = Map::cell_to_pixel(cell_position);
|
||||
Edible {
|
||||
base: StaticEntity::new(pixel_position, cell_position),
|
||||
kind,
|
||||
sprite,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks collision with Pac-Man (or any entity)
|
||||
pub fn collide(&self, pacman: &dyn Entity) -> bool {
|
||||
self.base.is_colliding(pacman)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Entity for Edible<'a> {
|
||||
fn base(&self) -> &StaticEntity {
|
||||
&self.base
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Renderable for Edible<'a> {
|
||||
fn render(&self, canvas: &mut Canvas<Window>) {
|
||||
self.sprite
|
||||
.render(canvas, self.base.pixel_position, Direction::Right, Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Reconstruct all edibles from the original map layout
|
||||
pub fn reconstruct_edibles<'a>(
|
||||
map: Rc<RefCell<Map>>,
|
||||
pellet_sprite: Rc<AtlasTexture<'a>>,
|
||||
power_pellet_sprite: Rc<AtlasTexture<'a>>,
|
||||
_fruit_sprite: Rc<AtlasTexture<'a>>,
|
||||
) -> Vec<Edible<'a>> {
|
||||
let mut edibles = Vec::new();
|
||||
for x in 0..BOARD_WIDTH {
|
||||
for y in 0..BOARD_HEIGHT {
|
||||
let tile = map.borrow().get_tile((x as i32, y as i32));
|
||||
let cell = (x, y);
|
||||
match tile {
|
||||
Some(MapTile::Pellet) => {
|
||||
edibles.push(Edible::new(
|
||||
EdibleKind::Pellet,
|
||||
cell,
|
||||
Rc::clone(&pellet_sprite),
|
||||
));
|
||||
}
|
||||
Some(MapTile::PowerPellet) => {
|
||||
edibles.push(Edible::new(
|
||||
EdibleKind::PowerPellet,
|
||||
cell,
|
||||
Rc::clone(&power_pellet_sprite),
|
||||
));
|
||||
}
|
||||
// Fruits can be added here if you have fruit positions
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
edibles
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// taken from https://github.com/Gigoteur/PX8/blob/master/src/px8/emscripten.rs
|
||||
#[cfg(target_os = "emscripten")]
|
||||
pub mod emscripten {
|
||||
use std::cell::RefCell;
|
||||
use std::ptr::null_mut;
|
||||
use std::os::raw::{c_int, c_void, c_char, c_float};
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
type em_callback_func = unsafe extern "C" fn();
|
||||
|
||||
extern "C" {
|
||||
// void emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop)
|
||||
pub fn emscripten_set_main_loop(func: em_callback_func,
|
||||
fps: c_int,
|
||||
simulate_infinite_loop: c_int);
|
||||
|
||||
pub fn emscripten_cancel_main_loop();
|
||||
pub fn emscripten_pause_main_loop();
|
||||
pub fn emscripten_get_now() -> c_float;
|
||||
}
|
||||
|
||||
thread_local!(static MAIN_LOOP_CALLBACK: RefCell<*mut c_void> = RefCell::new(null_mut()));
|
||||
|
||||
pub fn set_main_loop_callback<F>(callback: F)
|
||||
where F: FnMut()
|
||||
{
|
||||
MAIN_LOOP_CALLBACK
|
||||
.with(|log| { *log.borrow_mut() = &callback as *const _ as *mut c_void; });
|
||||
|
||||
unsafe {
|
||||
emscripten_set_main_loop(wrapper::<F>, -1, 1);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn wrapper<F>()
|
||||
where F: FnMut()
|
||||
{
|
||||
MAIN_LOOP_CALLBACK.with(|z| {
|
||||
let closure = *z.borrow_mut() as *mut F;
|
||||
(*closure)();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/entity.rs
182
src/entity.rs
@@ -1,10 +1,174 @@
|
||||
use crate::{
|
||||
constants::{MapTile, BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE},
|
||||
direction::Direction,
|
||||
map::Map,
|
||||
modulation::SimpleTickModulator,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// A trait for game objects that can be moved and rendered.
|
||||
pub trait Entity {
|
||||
// Returns true if the entity is colliding with the other entity
|
||||
fn is_colliding(&self, other: &dyn Entity) -> bool;
|
||||
// Returns the absolute position of the entity
|
||||
fn position(&self) -> (i32, i32);
|
||||
// Returns the cell position of the entity (XY position within the grid)
|
||||
fn cell_position(&self) -> (u32, u32);
|
||||
// Tick the entity (move it, perform collision checks, etc)
|
||||
fn tick(&mut self);
|
||||
}
|
||||
/// Returns a reference to the base entity (position, etc).
|
||||
fn base(&self) -> &StaticEntity;
|
||||
|
||||
/// Returns true if the entity is colliding with the other entity.
|
||||
fn is_colliding(&self, other: &dyn Entity) -> bool {
|
||||
let (x, y) = self.base().pixel_position;
|
||||
let (other_x, other_y) = other.base().pixel_position;
|
||||
x == other_x && y == other_y
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for entities that can move and interact with the map.
|
||||
pub trait Moving {
|
||||
fn move_forward(&mut self);
|
||||
fn update_cell_position(&mut self);
|
||||
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32);
|
||||
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool;
|
||||
fn handle_tunnel(&mut self) -> bool;
|
||||
fn is_grid_aligned(&self) -> bool;
|
||||
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool;
|
||||
}
|
||||
|
||||
/// A struct for static (non-moving) entities with position only.
|
||||
pub struct StaticEntity {
|
||||
pub pixel_position: (i32, i32),
|
||||
pub cell_position: (u32, u32),
|
||||
}
|
||||
|
||||
impl StaticEntity {
|
||||
pub fn new(pixel_position: (i32, i32), cell_position: (u32, u32)) -> Self {
|
||||
Self {
|
||||
pixel_position,
|
||||
cell_position,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct for movable game entities with position, direction, speed, and modulation.
|
||||
pub struct MovableEntity {
|
||||
pub base: StaticEntity,
|
||||
pub direction: Direction,
|
||||
pub speed: u32,
|
||||
pub modulation: SimpleTickModulator,
|
||||
pub in_tunnel: bool,
|
||||
pub map: Rc<RefCell<Map>>,
|
||||
}
|
||||
|
||||
impl MovableEntity {
|
||||
pub fn new(
|
||||
pixel_position: (i32, i32),
|
||||
cell_position: (u32, u32),
|
||||
direction: Direction,
|
||||
speed: u32,
|
||||
modulation: SimpleTickModulator,
|
||||
map: Rc<RefCell<Map>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
base: StaticEntity::new(pixel_position, cell_position),
|
||||
direction,
|
||||
speed,
|
||||
modulation,
|
||||
in_tunnel: false,
|
||||
map,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the position within the current cell, in pixels.
|
||||
pub fn internal_position(&self) -> (u32, u32) {
|
||||
(
|
||||
self.base.pixel_position.0 as u32 % CELL_SIZE,
|
||||
self.base.pixel_position.1 as u32 % CELL_SIZE,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for MovableEntity {
|
||||
fn base(&self) -> &StaticEntity {
|
||||
&self.base
|
||||
}
|
||||
}
|
||||
|
||||
impl Moving for MovableEntity {
|
||||
fn move_forward(&mut self) {
|
||||
let speed = self.speed as i32;
|
||||
match self.direction {
|
||||
Direction::Right => self.base.pixel_position.0 += speed,
|
||||
Direction::Left => self.base.pixel_position.0 -= speed,
|
||||
Direction::Up => self.base.pixel_position.1 -= speed,
|
||||
Direction::Down => self.base.pixel_position.1 += speed,
|
||||
}
|
||||
}
|
||||
fn update_cell_position(&mut self) {
|
||||
self.base.cell_position = (
|
||||
(self.base.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0,
|
||||
(self.base.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1,
|
||||
);
|
||||
}
|
||||
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
|
||||
let (x, y) = direction.unwrap_or(self.direction).offset();
|
||||
(
|
||||
self.base.cell_position.0 as i32 + x,
|
||||
self.base.cell_position.1 as i32 + y,
|
||||
)
|
||||
}
|
||||
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
|
||||
let next_cell = self.next_cell(direction);
|
||||
matches!(self.map.borrow().get_tile(next_cell), Some(MapTile::Wall))
|
||||
}
|
||||
fn handle_tunnel(&mut self) -> bool {
|
||||
if !self.in_tunnel {
|
||||
let current_tile = self.map.borrow().get_tile((
|
||||
self.base.cell_position.0 as i32,
|
||||
self.base.cell_position.1 as i32,
|
||||
));
|
||||
if matches!(current_tile, Some(MapTile::Tunnel)) {
|
||||
self.in_tunnel = true;
|
||||
}
|
||||
}
|
||||
if self.in_tunnel {
|
||||
if self.base.cell_position.0 == 0 {
|
||||
self.base.cell_position.0 = BOARD_WIDTH - 2;
|
||||
self.base.pixel_position =
|
||||
Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1));
|
||||
self.in_tunnel = false;
|
||||
true
|
||||
} else if self.base.cell_position.0 == BOARD_WIDTH - 1 {
|
||||
self.base.cell_position.0 = 1;
|
||||
self.base.pixel_position =
|
||||
Map::cell_to_pixel((self.base.cell_position.0, self.base.cell_position.1));
|
||||
self.in_tunnel = false;
|
||||
true
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
fn is_grid_aligned(&self) -> bool {
|
||||
self.internal_position() == (0, 0)
|
||||
}
|
||||
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
|
||||
if new_direction == self.direction {
|
||||
return false;
|
||||
}
|
||||
if self.is_wall_ahead(Some(new_direction)) {
|
||||
return false;
|
||||
}
|
||||
self.direction = new_direction;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for StaticEntity {
|
||||
fn base(&self) -> &StaticEntity {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for entities that can be rendered to the screen.
|
||||
pub trait Renderable {
|
||||
fn render(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>);
|
||||
}
|
||||
|
||||
394
src/game.rs
394
src/game.rs
@@ -1,99 +1,379 @@
|
||||
//! This module contains the main game logic and state.
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use rand::seq::IteratorRandom;
|
||||
use sdl2::image::LoadTexture;
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::render::{TextureCreator, Texture};
|
||||
use sdl2::render::{Texture, TextureCreator};
|
||||
use sdl2::rwops::RWops;
|
||||
use sdl2::ttf::Font;
|
||||
use sdl2::video::WindowContext;
|
||||
use sdl2::{pixels::Color, render::Canvas, video::Window};
|
||||
|
||||
use crate::constants::{MapTile, BOARD, BOARD_HEIGHT, BOARD_WIDTH};
|
||||
use crate::animation::AtlasTexture;
|
||||
use crate::asset::{get_asset_bytes, Asset};
|
||||
use crate::audio::Audio;
|
||||
use crate::constants::RAW_BOARD;
|
||||
use crate::debug::{DebugMode, DebugRenderer};
|
||||
use crate::direction::Direction;
|
||||
use crate::entity::Entity;
|
||||
use crate::pacman::{Pacman};
|
||||
use crate::edible::{reconstruct_edibles, Edible, EdibleKind};
|
||||
use crate::entity::Renderable;
|
||||
use crate::ghosts::blinky::Blinky;
|
||||
use crate::map::Map;
|
||||
use crate::pacman::Pacman;
|
||||
|
||||
/// The main game state.
|
||||
///
|
||||
/// This struct contains all the information necessary to run the game, including
|
||||
/// the canvas, textures, fonts, game objects, and the current score.
|
||||
pub struct Game<'a> {
|
||||
canvas: &'a mut Canvas<Window>,
|
||||
map_texture: Texture<'a>,
|
||||
pacman: Pacman<'a>,
|
||||
debug: bool,
|
||||
pellet_texture: Rc<AtlasTexture<'a>>,
|
||||
power_pellet_texture: Rc<AtlasTexture<'a>>,
|
||||
font: Font<'a, 'static>,
|
||||
pacman: Rc<RefCell<Pacman<'a>>>,
|
||||
map: Rc<RefCell<Map>>,
|
||||
debug_mode: DebugMode,
|
||||
score: u32,
|
||||
audio: Audio,
|
||||
blinky: Blinky<'a>,
|
||||
edibles: Vec<Edible<'a>>,
|
||||
}
|
||||
|
||||
impl Game<'_> {
|
||||
pub fn new<'a>(
|
||||
impl<'a> Game<'a> {
|
||||
/// Creates a new `Game` instance.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `canvas` - The SDL canvas to render to.
|
||||
/// * `texture_creator` - The SDL texture creator.
|
||||
/// * `ttf_context` - The SDL TTF context.
|
||||
/// * `_audio_subsystem` - The SDL audio subsystem (currently unused).
|
||||
pub fn new(
|
||||
canvas: &'a mut Canvas<Window>,
|
||||
texture_creator: &'a TextureCreator<WindowContext>,
|
||||
ttf_context: &'a sdl2::ttf::Sdl2TtfContext,
|
||||
_audio_subsystem: &'a sdl2::AudioSubsystem,
|
||||
) -> Game<'a> {
|
||||
let map = Rc::new(RefCell::new(Map::new(RAW_BOARD)));
|
||||
|
||||
// Load Pacman texture from asset API
|
||||
let pacman_bytes = get_asset_bytes(Asset::Pacman).expect("Failed to load asset");
|
||||
let pacman_atlas = texture_creator
|
||||
.load_texture("assets/32/pacman.png")
|
||||
.expect("Could not load pacman texture");
|
||||
let pacman = Pacman::new(None, pacman_atlas);
|
||||
.load_texture_bytes(&pacman_bytes)
|
||||
.expect("Could not load pacman texture from asset API");
|
||||
let pacman = Rc::new(RefCell::new(Pacman::new(
|
||||
(1, 1),
|
||||
pacman_atlas,
|
||||
Rc::clone(&map),
|
||||
)));
|
||||
|
||||
// Load ghost textures
|
||||
let ghost_body_bytes = get_asset_bytes(Asset::GhostBody).expect("Failed to load asset");
|
||||
let ghost_body = texture_creator
|
||||
.load_texture_bytes(&ghost_body_bytes)
|
||||
.expect("Could not load ghost body texture from asset API");
|
||||
let ghost_eyes_bytes = get_asset_bytes(Asset::GhostEyes).expect("Failed to load asset");
|
||||
let ghost_eyes = texture_creator
|
||||
.load_texture_bytes(&ghost_eyes_bytes)
|
||||
.expect("Could not load ghost eyes texture from asset API");
|
||||
|
||||
// Create Blinky
|
||||
let blinky = Blinky::new(
|
||||
(13, 11), // Starting position just above ghost house
|
||||
ghost_body,
|
||||
ghost_eyes,
|
||||
Rc::clone(&map),
|
||||
Rc::clone(&pacman),
|
||||
);
|
||||
|
||||
// Load pellet texture from asset API
|
||||
let pellet_bytes = get_asset_bytes(Asset::Pellet).expect("Failed to load asset");
|
||||
let pellet_texture = Rc::new(AtlasTexture::new(
|
||||
texture_creator
|
||||
.load_texture_bytes(&pellet_bytes)
|
||||
.expect("Could not load pellet texture from asset API"),
|
||||
1,
|
||||
24,
|
||||
24,
|
||||
None,
|
||||
));
|
||||
let power_pellet_bytes = get_asset_bytes(Asset::Energizer).expect("Failed to load asset");
|
||||
let power_pellet_texture = Rc::new(AtlasTexture::new(
|
||||
texture_creator
|
||||
.load_texture_bytes(&power_pellet_bytes)
|
||||
.expect("Could not load power pellet texture from asset API"),
|
||||
1,
|
||||
24,
|
||||
24,
|
||||
None,
|
||||
));
|
||||
|
||||
// Load font from asset API
|
||||
let font = {
|
||||
let font_bytes = get_asset_bytes(Asset::FontKonami)
|
||||
.expect("Failed to load asset")
|
||||
.into_owned();
|
||||
let font_bytes_static: &'static [u8] = Box::leak(font_bytes.into_boxed_slice());
|
||||
let font_rwops =
|
||||
RWops::from_bytes(font_bytes_static).expect("Failed to create RWops for font");
|
||||
ttf_context
|
||||
.load_font_from_rwops(font_rwops, 24)
|
||||
.expect("Could not load font from asset API")
|
||||
};
|
||||
|
||||
let audio = Audio::new();
|
||||
|
||||
// Load map texture from asset API
|
||||
let map_bytes = get_asset_bytes(Asset::Map).expect("Failed to load asset");
|
||||
let mut map_texture = texture_creator
|
||||
.load_texture_bytes(&map_bytes)
|
||||
.expect("Could not load map texture from asset API");
|
||||
map_texture.set_color_mod(0, 0, 255);
|
||||
|
||||
let edibles = reconstruct_edibles(
|
||||
Rc::clone(&map),
|
||||
Rc::clone(&pellet_texture),
|
||||
Rc::clone(&power_pellet_texture),
|
||||
Rc::clone(&pellet_texture), // placeholder for fruit sprite
|
||||
);
|
||||
|
||||
Game {
|
||||
canvas,
|
||||
pacman: pacman,
|
||||
debug: false,
|
||||
map_texture: texture_creator
|
||||
.load_texture("assets/map.png")
|
||||
.expect("Could not load pacman texture"),
|
||||
pacman,
|
||||
debug_mode: DebugMode::None,
|
||||
map,
|
||||
map_texture,
|
||||
pellet_texture,
|
||||
power_pellet_texture,
|
||||
font,
|
||||
score: 0,
|
||||
audio,
|
||||
blinky,
|
||||
edibles,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a keyboard event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keycode` - The keycode of the key that was pressed.
|
||||
pub fn keyboard_event(&mut self, keycode: Keycode) {
|
||||
match keycode {
|
||||
Keycode::D => {
|
||||
self.pacman.direction = Direction::Right;
|
||||
}
|
||||
Keycode::A => {
|
||||
self.pacman.direction = Direction::Left;
|
||||
}
|
||||
Keycode::W => {
|
||||
self.pacman.direction = Direction::Up;
|
||||
}
|
||||
Keycode::S => {
|
||||
self.pacman.direction = Direction::Down;
|
||||
}
|
||||
Keycode::Space => {
|
||||
self.debug = !self.debug;
|
||||
}
|
||||
_ => {}
|
||||
// Change direction
|
||||
let direction = Direction::from_keycode(keycode);
|
||||
if direction.is_some() {
|
||||
self.pacman.borrow_mut().next_direction = direction;
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle debug mode
|
||||
if keycode == Keycode::Space {
|
||||
self.debug_mode = match self.debug_mode {
|
||||
DebugMode::None => DebugMode::Grid,
|
||||
DebugMode::Grid => DebugMode::Pathfinding,
|
||||
DebugMode::Pathfinding => DebugMode::ValidPositions,
|
||||
DebugMode::ValidPositions => DebugMode::None,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset game
|
||||
if keycode == Keycode::R {
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
self.pacman.tick();
|
||||
/// Adds points to the score.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `points` - The number of points to add.
|
||||
pub fn add_score(&mut self, points: u32) {
|
||||
self.score += points;
|
||||
}
|
||||
|
||||
/// Resets the game to its initial state.
|
||||
pub fn reset(&mut self) {
|
||||
// Reset the map to restore all pellets
|
||||
{
|
||||
let mut map = self.map.borrow_mut();
|
||||
map.reset();
|
||||
}
|
||||
|
||||
// Reset the score
|
||||
self.score = 0;
|
||||
|
||||
// Get valid positions from the cached flood fill and randomize positions in a single block
|
||||
{
|
||||
let mut map = self.map.borrow_mut();
|
||||
let valid_positions = map.get_valid_playable_positions();
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// Randomize Pac-Man position
|
||||
if let Some(pos) = valid_positions.iter().choose(&mut rng) {
|
||||
let mut pacman = self.pacman.borrow_mut();
|
||||
pacman.base.base.pixel_position = Map::cell_to_pixel((pos.x, pos.y));
|
||||
pacman.base.base.cell_position = (pos.x, pos.y);
|
||||
pacman.base.in_tunnel = false;
|
||||
pacman.base.direction = Direction::Right;
|
||||
pacman.next_direction = None;
|
||||
pacman.stopped = false;
|
||||
}
|
||||
|
||||
// Randomize ghost position
|
||||
if let Some(pos) = valid_positions.iter().choose(&mut rng) {
|
||||
self.blinky.base.base.pixel_position = Map::cell_to_pixel((pos.x, pos.y));
|
||||
self.blinky.base.base.cell_position = (pos.x, pos.y);
|
||||
self.blinky.base.in_tunnel = false;
|
||||
self.blinky.base.direction = Direction::Left;
|
||||
self.blinky.mode = crate::ghost::GhostMode::Chase;
|
||||
}
|
||||
}
|
||||
|
||||
self.edibles = reconstruct_edibles(
|
||||
Rc::clone(&self.map),
|
||||
Rc::clone(&self.pellet_texture),
|
||||
Rc::clone(&self.power_pellet_texture),
|
||||
Rc::clone(&self.pellet_texture), // placeholder for fruit sprite
|
||||
);
|
||||
}
|
||||
|
||||
/// Advances the game by one tick.
|
||||
pub fn tick(&mut self) {
|
||||
// Advance animation frames for Pacman and Blinky
|
||||
self.pacman.borrow_mut().sprite.tick();
|
||||
self.blinky.body_sprite.tick();
|
||||
self.blinky.eyes_sprite.tick();
|
||||
|
||||
let pacman = self.pacman.borrow();
|
||||
let mut eaten_indices = vec![];
|
||||
for (i, edible) in self.edibles.iter().enumerate() {
|
||||
if edible.collide(&*pacman) {
|
||||
eaten_indices.push(i);
|
||||
}
|
||||
}
|
||||
drop(pacman); // Release immutable borrow before mutably borrowing self
|
||||
for &i in eaten_indices.iter().rev() {
|
||||
let edible = &self.edibles[i];
|
||||
match edible.kind {
|
||||
EdibleKind::Pellet => {
|
||||
self.add_score(10);
|
||||
self.audio.eat();
|
||||
}
|
||||
EdibleKind::PowerPellet => {
|
||||
self.add_score(50);
|
||||
self.audio.eat();
|
||||
}
|
||||
EdibleKind::Fruit(_fruit) => {
|
||||
self.add_score(100);
|
||||
self.audio.eat();
|
||||
}
|
||||
}
|
||||
self.edibles.remove(i);
|
||||
}
|
||||
self.pacman.borrow_mut().tick();
|
||||
self.blinky.tick();
|
||||
}
|
||||
|
||||
/// Draws the entire game to the canvas.
|
||||
pub fn draw(&mut self) {
|
||||
// Clear the screen (black)
|
||||
self.canvas.set_draw_color(Color::RGB(0, 0, 0));
|
||||
self.canvas.clear();
|
||||
|
||||
|
||||
// Render the map
|
||||
self.canvas
|
||||
.copy(&self.map_texture, None, None)
|
||||
.expect("Could not render texture on canvas");
|
||||
|
||||
// Render the pacman
|
||||
self.pacman.render(self.canvas);
|
||||
// Render all edibles
|
||||
for edible in &self.edibles {
|
||||
edible.render(self.canvas);
|
||||
}
|
||||
|
||||
// Draw a grid
|
||||
if self.debug {
|
||||
for x in 0..BOARD_WIDTH {
|
||||
for y in 0..BOARD_HEIGHT {
|
||||
let tile = BOARD[x as usize][y as usize];
|
||||
let color = match tile {
|
||||
MapTile::Empty => None,
|
||||
MapTile::Wall => Some(Color::BLUE),
|
||||
MapTile::Pellet => Some(Color::RED),
|
||||
MapTile::PowerPellet => Some(Color::MAGENTA),
|
||||
MapTile::StartingPosition(_) => Some(Color::GREEN),
|
||||
};
|
||||
// Render Pac-Man
|
||||
self.pacman.borrow().render(self.canvas);
|
||||
|
||||
if let Some(color) = color {
|
||||
self.canvas.set_draw_color(color);
|
||||
self.canvas
|
||||
.draw_rect(sdl2::rect::Rect::new(x as i32 * 24, y as i32 * 24, 24, 24))
|
||||
.expect("Could not draw rectangle");
|
||||
}
|
||||
// Render ghost
|
||||
self.blinky.render(self.canvas);
|
||||
|
||||
// Render score
|
||||
self.render_ui();
|
||||
|
||||
// Draw the debug grid
|
||||
match self.debug_mode {
|
||||
DebugMode::Grid => {
|
||||
DebugRenderer::draw_debug_grid(
|
||||
self.canvas,
|
||||
&self.map.borrow(),
|
||||
self.pacman.borrow().base.base.cell_position,
|
||||
);
|
||||
let next_cell =
|
||||
<Pacman as crate::entity::Moving>::next_cell(&*self.pacman.borrow(), None);
|
||||
DebugRenderer::draw_next_cell(
|
||||
self.canvas,
|
||||
&self.map.borrow(),
|
||||
(next_cell.0 as u32, next_cell.1 as u32),
|
||||
);
|
||||
}
|
||||
DebugMode::ValidPositions => {
|
||||
DebugRenderer::draw_valid_positions(self.canvas, &mut self.map.borrow_mut());
|
||||
}
|
||||
DebugMode::Pathfinding => {
|
||||
DebugRenderer::draw_pathfinding(self.canvas, &self.blinky, &self.map.borrow());
|
||||
}
|
||||
DebugMode::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Present the canvas
|
||||
self.canvas.present();
|
||||
}
|
||||
|
||||
/// Renders the user interface, including the score and lives.
|
||||
fn render_ui(&mut self) {
|
||||
let lives = 3;
|
||||
let score_text = format!("{:02}", self.score);
|
||||
|
||||
let x_offset = 12;
|
||||
let y_offset = 2;
|
||||
let lives_offset = 3;
|
||||
let score_offset = 7 - (score_text.len() as i32);
|
||||
let gap_offset = 6;
|
||||
|
||||
// Render the score and high score
|
||||
self.render_text(
|
||||
&format!("{lives}UP HIGH SCORE "),
|
||||
(24 * lives_offset + x_offset, y_offset),
|
||||
Color::WHITE,
|
||||
);
|
||||
self.render_text(
|
||||
&score_text,
|
||||
(24 * score_offset + x_offset, 24 + y_offset + gap_offset),
|
||||
Color::WHITE,
|
||||
);
|
||||
}
|
||||
|
||||
/// Renders text to the screen at the given position.
|
||||
fn render_text(&mut self, text: &str, position: (i32, i32), color: Color) {
|
||||
let surface = self
|
||||
.font
|
||||
.render(text)
|
||||
.blended(color)
|
||||
.expect("Could not render text surface");
|
||||
|
||||
let texture_creator = self.canvas.texture_creator();
|
||||
let texture = texture_creator
|
||||
.create_texture_from_surface(&surface)
|
||||
.expect("Could not create texture from surface");
|
||||
let query = texture.query();
|
||||
|
||||
let dst_rect = sdl2::rect::Rect::new(position.0, position.1, query.width, query.height);
|
||||
|
||||
self.canvas
|
||||
.copy(&texture, None, Some(dst_rect))
|
||||
.expect("Could not render text texture");
|
||||
}
|
||||
}
|
||||
|
||||
324
src/ghost.rs
Normal file
324
src/ghost.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use pathfinding::prelude::dijkstra;
|
||||
use rand::Rng;
|
||||
|
||||
use crate::animation::{AnimatedAtlasTexture, FrameDrawn};
|
||||
use crate::constants::{MapTile, BOARD_WIDTH};
|
||||
use crate::direction::Direction;
|
||||
use crate::entity::{Entity, MovableEntity, Moving, Renderable};
|
||||
use crate::map::Map;
|
||||
use crate::modulation::{SimpleTickModulator, TickModulator};
|
||||
use crate::pacman::Pacman;
|
||||
use sdl2::pixels::Color;
|
||||
use sdl2::render::Texture;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// The different modes a ghost can be in
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum GhostMode {
|
||||
/// Chase mode - ghost actively pursues Pac-Man using its unique strategy
|
||||
Chase,
|
||||
/// Scatter mode - ghost heads to its home corner
|
||||
Scatter,
|
||||
/// Frightened mode - ghost moves randomly and can be eaten
|
||||
Frightened,
|
||||
/// Eyes mode - ghost returns to the ghost house after being eaten
|
||||
Eyes,
|
||||
/// House mode - ghost is in the ghost house, waiting to exit
|
||||
House,
|
||||
}
|
||||
|
||||
/// The different ghost personalities
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum GhostType {
|
||||
Blinky, // Red - Shadow
|
||||
Pinky, // Pink - Speedy
|
||||
Inky, // Cyan - Bashful
|
||||
Clyde, // Orange - Pokey
|
||||
}
|
||||
|
||||
impl GhostType {
|
||||
/// Returns the color of the ghost.
|
||||
pub fn color(&self) -> Color {
|
||||
match self {
|
||||
GhostType::Blinky => Color::RGB(255, 0, 0),
|
||||
GhostType::Pinky => Color::RGB(255, 184, 255),
|
||||
GhostType::Inky => Color::RGB(0, 255, 255),
|
||||
GhostType::Clyde => Color::RGB(255, 184, 82),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base ghost struct that contains common functionality
|
||||
pub struct Ghost<'a> {
|
||||
/// Shared movement and position fields.
|
||||
pub base: MovableEntity,
|
||||
/// The current mode of the ghost
|
||||
pub mode: GhostMode,
|
||||
/// The type/personality of this ghost
|
||||
pub ghost_type: GhostType,
|
||||
/// Reference to Pac-Man for targeting
|
||||
pub pacman: Rc<RefCell<Pacman<'a>>>,
|
||||
pub body_sprite: AnimatedAtlasTexture<'a>,
|
||||
pub eyes_sprite: AnimatedAtlasTexture<'a>,
|
||||
}
|
||||
|
||||
impl Ghost<'_> {
|
||||
/// Creates a new ghost instance
|
||||
pub fn new<'a>(
|
||||
ghost_type: GhostType,
|
||||
starting_position: (u32, u32),
|
||||
body_texture: Texture<'a>,
|
||||
eyes_texture: Texture<'a>,
|
||||
map: Rc<RefCell<Map>>,
|
||||
pacman: Rc<RefCell<Pacman<'a>>>,
|
||||
) -> Ghost<'a> {
|
||||
let color = ghost_type.color();
|
||||
let mut body_sprite = AnimatedAtlasTexture::new(body_texture, 8, 2, 32, 32, Some((-4, -4)));
|
||||
body_sprite.set_color_modulation(color.r, color.g, color.b);
|
||||
let pixel_position = Map::cell_to_pixel(starting_position);
|
||||
Ghost {
|
||||
base: MovableEntity::new(
|
||||
pixel_position,
|
||||
starting_position,
|
||||
Direction::Left,
|
||||
3,
|
||||
SimpleTickModulator::new(1.0),
|
||||
map,
|
||||
),
|
||||
mode: GhostMode::Chase,
|
||||
ghost_type,
|
||||
pacman,
|
||||
body_sprite,
|
||||
eyes_sprite: AnimatedAtlasTexture::new(eyes_texture, 1, 4, 32, 32, Some((-4, -4))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the target tile for this ghost based on its current mode
|
||||
pub fn get_target_tile(&self) -> (i32, i32) {
|
||||
match self.mode {
|
||||
GhostMode::Scatter => self.get_scatter_target(),
|
||||
GhostMode::Chase => self.get_chase_target(),
|
||||
GhostMode::Frightened => self.get_random_target(),
|
||||
GhostMode::Eyes => self.get_house_target(),
|
||||
GhostMode::House => self.get_house_exit_target(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets this ghost's home corner target for scatter mode
|
||||
fn get_scatter_target(&self) -> (i32, i32) {
|
||||
match self.ghost_type {
|
||||
GhostType::Blinky => (25, 0), // Top right
|
||||
GhostType::Pinky => (2, 0), // Top left
|
||||
GhostType::Inky => (27, 35), // Bottom right
|
||||
GhostType::Clyde => (0, 35), // Bottom left
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a random adjacent tile for frightened mode
|
||||
fn get_random_target(&self) -> (i32, i32) {
|
||||
let mut rng = rand::rng();
|
||||
let mut possible_moves = Vec::new();
|
||||
|
||||
// Check all four directions
|
||||
for dir in &[
|
||||
Direction::Up,
|
||||
Direction::Down,
|
||||
Direction::Left,
|
||||
Direction::Right,
|
||||
] {
|
||||
// Don't allow reversing direction
|
||||
if *dir == self.base.direction.opposite() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let next_cell = self.base.next_cell(Some(*dir));
|
||||
if !matches!(
|
||||
self.base.map.borrow().get_tile(next_cell),
|
||||
Some(MapTile::Wall)
|
||||
) {
|
||||
possible_moves.push(next_cell);
|
||||
}
|
||||
}
|
||||
|
||||
if possible_moves.is_empty() {
|
||||
// No valid moves, must reverse
|
||||
self.base.next_cell(Some(self.base.direction.opposite()))
|
||||
} else {
|
||||
// Choose a random valid move
|
||||
possible_moves[rng.random_range(0..possible_moves.len())]
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the ghost house target for returning eyes
|
||||
fn get_house_target(&self) -> (i32, i32) {
|
||||
(13, 14) // Center of ghost house
|
||||
}
|
||||
|
||||
/// Gets the exit point target when leaving house
|
||||
fn get_house_exit_target(&self) -> (i32, i32) {
|
||||
(13, 11) // Just above ghost house
|
||||
}
|
||||
|
||||
/// Gets this ghost's chase mode target (to be implemented by each ghost type)
|
||||
fn get_chase_target(&self) -> (i32, i32) {
|
||||
let pacman = self.pacman.borrow();
|
||||
let cell = pacman.base().cell_position;
|
||||
(cell.0 as i32, cell.1 as i32)
|
||||
}
|
||||
|
||||
/// Calculates the path to the target tile using the A* algorithm.
|
||||
pub fn get_path_to_target(&self, target: (u32, u32)) -> Option<(Vec<(u32, u32)>, u32)> {
|
||||
let start = self.base.base.cell_position;
|
||||
let map = self.base.map.borrow();
|
||||
|
||||
dijkstra(
|
||||
&start,
|
||||
|&p| {
|
||||
let mut successors = vec![];
|
||||
let tile = map.get_tile((p.0 as i32, p.1 as i32));
|
||||
// Tunnel wrap: if currently in a tunnel, add the opposite exit as a neighbor
|
||||
if let Some(MapTile::Tunnel) = tile {
|
||||
if p.0 == 0 {
|
||||
successors.push(((BOARD_WIDTH - 2, p.1), 1));
|
||||
} else if p.0 == BOARD_WIDTH - 1 {
|
||||
successors.push(((1, p.1), 1));
|
||||
}
|
||||
}
|
||||
for dir in &[
|
||||
Direction::Up,
|
||||
Direction::Down,
|
||||
Direction::Left,
|
||||
Direction::Right,
|
||||
] {
|
||||
let (dx, dy) = dir.offset();
|
||||
let next_p = (p.0 as i32 + dx, p.1 as i32 + dy);
|
||||
if let Some(tile) = map.get_tile(next_p) {
|
||||
if tile == MapTile::Wall {
|
||||
continue;
|
||||
}
|
||||
successors.push(((next_p.0 as u32, next_p.1 as u32), 1));
|
||||
}
|
||||
}
|
||||
successors
|
||||
},
|
||||
|&p| p == target,
|
||||
)
|
||||
}
|
||||
|
||||
/// Changes the ghost's mode and handles direction reversal
|
||||
pub fn set_mode(&mut self, new_mode: GhostMode) {
|
||||
// Don't reverse if going to/from frightened or if in house
|
||||
let should_reverse = self.mode != GhostMode::House
|
||||
&& new_mode != GhostMode::Frightened
|
||||
&& self.mode != GhostMode::Frightened;
|
||||
|
||||
self.mode = new_mode;
|
||||
|
||||
self.base.speed = match new_mode {
|
||||
GhostMode::Chase => 3,
|
||||
GhostMode::Scatter => 2,
|
||||
GhostMode::Frightened => 2,
|
||||
GhostMode::Eyes => 7,
|
||||
GhostMode::House => 0,
|
||||
};
|
||||
|
||||
if should_reverse {
|
||||
self.base
|
||||
.set_direction_if_valid(self.base.direction.opposite());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
if self.mode == GhostMode::House {
|
||||
// For now, do nothing in the house
|
||||
return;
|
||||
}
|
||||
|
||||
if self.base.is_grid_aligned() {
|
||||
self.base.update_cell_position();
|
||||
|
||||
if !self.base.handle_tunnel() {
|
||||
// Pathfinding logic (only if not in tunnel)
|
||||
let target_tile = self.get_target_tile();
|
||||
if let Some((path, _)) =
|
||||
self.get_path_to_target((target_tile.0 as u32, target_tile.1 as u32))
|
||||
{
|
||||
if path.len() > 1 {
|
||||
let next_move = path[1];
|
||||
let (x, y) = self.base.base.cell_position;
|
||||
let dx = next_move.0 as i32 - x as i32;
|
||||
let dy = next_move.1 as i32 - y as i32;
|
||||
let new_direction = if dx > 0 {
|
||||
Direction::Right
|
||||
} else if dx < 0 {
|
||||
Direction::Left
|
||||
} else if dy > 0 {
|
||||
Direction::Down
|
||||
} else {
|
||||
Direction::Up
|
||||
};
|
||||
self.base.set_direction_if_valid(new_direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't move if the next tile is a wall
|
||||
if self.base.is_wall_ahead(None) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if self.base.modulation.next() {
|
||||
self.base.move_forward();
|
||||
|
||||
if self.base.is_grid_aligned() {
|
||||
self.base.update_cell_position();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Moving for Ghost<'a> {
|
||||
fn move_forward(&mut self) {
|
||||
self.base.move_forward();
|
||||
}
|
||||
fn update_cell_position(&mut self) {
|
||||
self.base.update_cell_position();
|
||||
}
|
||||
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
|
||||
self.base.next_cell(direction)
|
||||
}
|
||||
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
|
||||
self.base.is_wall_ahead(direction)
|
||||
}
|
||||
fn handle_tunnel(&mut self) -> bool {
|
||||
self.base.handle_tunnel()
|
||||
}
|
||||
fn is_grid_aligned(&self) -> bool {
|
||||
self.base.is_grid_aligned()
|
||||
}
|
||||
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
|
||||
self.base.set_direction_if_valid(new_direction)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Renderable for Ghost<'a> {
|
||||
fn render(&self, canvas: &mut sdl2::render::Canvas<sdl2::video::Window>) {
|
||||
let pos = self.base.base.pixel_position;
|
||||
self.body_sprite.render(canvas, pos, Direction::Right, None);
|
||||
// Inline the eye_frame logic here
|
||||
let eye_frame = if self.mode == GhostMode::Frightened {
|
||||
4 // Frightened frame
|
||||
} else {
|
||||
match self.base.direction {
|
||||
Direction::Right => 0,
|
||||
Direction::Up => 1,
|
||||
Direction::Left => 2,
|
||||
Direction::Down => 3,
|
||||
}
|
||||
};
|
||||
self.eyes_sprite
|
||||
.render(canvas, pos, Direction::Right, Some(eye_frame));
|
||||
}
|
||||
}
|
||||
102
src/ghosts/blinky.rs
Normal file
102
src/ghosts/blinky.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use sdl2::render::{Canvas, Texture};
|
||||
use sdl2::video::Window;
|
||||
|
||||
use crate::direction::Direction;
|
||||
use crate::entity::{Entity, Moving, Renderable, StaticEntity};
|
||||
use crate::ghost::{Ghost, GhostMode, GhostType};
|
||||
use crate::map::Map;
|
||||
use crate::pacman::Pacman;
|
||||
|
||||
pub struct Blinky<'a> {
|
||||
ghost: Ghost<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Blinky<'a> {
|
||||
pub fn new(
|
||||
starting_position: (u32, u32),
|
||||
body_texture: Texture<'a>,
|
||||
eyes_texture: Texture<'a>,
|
||||
map: Rc<RefCell<Map>>,
|
||||
pacman: Rc<RefCell<Pacman<'a>>>,
|
||||
) -> Blinky<'a> {
|
||||
Blinky {
|
||||
ghost: Ghost::new(
|
||||
GhostType::Blinky,
|
||||
starting_position,
|
||||
body_texture,
|
||||
eyes_texture,
|
||||
map,
|
||||
pacman,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets Blinky's chase target - directly targets Pac-Man's current position
|
||||
fn get_chase_target(&self) -> (i32, i32) {
|
||||
let pacman = self.ghost.pacman.borrow();
|
||||
let cell = pacman.base().cell_position;
|
||||
(cell.0 as i32, cell.1 as i32)
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: GhostMode) {
|
||||
self.ghost.set_mode(mode);
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
self.ghost.tick();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Entity for Blinky<'a> {
|
||||
fn base(&self) -> &StaticEntity {
|
||||
self.ghost.base.base()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Renderable for Blinky<'a> {
|
||||
fn render(&self, canvas: &mut Canvas<Window>) {
|
||||
self.ghost.render(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Moving for Blinky<'a> {
|
||||
fn move_forward(&mut self) {
|
||||
self.ghost.move_forward();
|
||||
}
|
||||
fn update_cell_position(&mut self) {
|
||||
self.ghost.update_cell_position();
|
||||
}
|
||||
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
|
||||
self.ghost.next_cell(direction)
|
||||
}
|
||||
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
|
||||
self.ghost.is_wall_ahead(direction)
|
||||
}
|
||||
fn handle_tunnel(&mut self) -> bool {
|
||||
self.ghost.handle_tunnel()
|
||||
}
|
||||
fn is_grid_aligned(&self) -> bool {
|
||||
self.ghost.is_grid_aligned()
|
||||
}
|
||||
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
|
||||
self.ghost.set_direction_if_valid(new_direction)
|
||||
}
|
||||
}
|
||||
|
||||
// Allow direct access to ghost fields
|
||||
impl<'a> std::ops::Deref for Blinky<'a> {
|
||||
type Target = Ghost<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.ghost
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::ops::DerefMut for Blinky<'a> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.ghost
|
||||
}
|
||||
}
|
||||
1
src/ghosts/mod.rs
Normal file
1
src/ghosts/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod blinky;
|
||||
107
src/helper.rs
Normal file
107
src/helper.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
//! This module contains helper functions that are used throughout the game.
|
||||
|
||||
/// Checks if two grid positions are adjacent to each other
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `a` - First position as (x, y) coordinates
|
||||
/// * `b` - Second position as (x, y) coordinates
|
||||
/// * `diagonal` - Whether to consider diagonal adjacency (true) or only orthogonal (false)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `true` if positions are adjacent according to the diagonal parameter
|
||||
/// * `false` otherwise
|
||||
pub fn is_adjacent(a: (u32, u32), b: (u32, u32), diagonal: bool) -> bool {
|
||||
let (ax, ay) = a;
|
||||
let (bx, by) = b;
|
||||
let dx = ax.abs_diff(bx);
|
||||
let dy = ay.abs_diff(by);
|
||||
if diagonal {
|
||||
dx <= 1 && dy <= 1 && (dx != 0 || dy != 0)
|
||||
} else {
|
||||
(dx == 1 && dy == 0) || (dx == 0 && dy == 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_orthogonal_adjacency() {
|
||||
// Test orthogonal adjacency (diagonal = false)
|
||||
|
||||
// Same position should not be adjacent
|
||||
assert!(!is_adjacent((0, 0), (0, 0), false));
|
||||
|
||||
// Adjacent positions should be true
|
||||
assert!(is_adjacent((0, 0), (1, 0), false)); // Right
|
||||
assert!(is_adjacent((0, 0), (0, 1), false)); // Down
|
||||
assert!(is_adjacent((1, 1), (0, 1), false)); // Left
|
||||
assert!(is_adjacent((1, 1), (1, 0), false)); // Up
|
||||
|
||||
// Diagonal positions should be false
|
||||
assert!(!is_adjacent((0, 0), (1, 1), false));
|
||||
assert!(!is_adjacent((0, 1), (1, 0), false));
|
||||
|
||||
// Positions more than 1 step away should be false
|
||||
assert!(!is_adjacent((0, 0), (2, 0), false));
|
||||
assert!(!is_adjacent((0, 0), (0, 2), false));
|
||||
assert!(!is_adjacent((0, 0), (2, 2), false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diagonal_adjacency() {
|
||||
// Test diagonal adjacency (diagonal = true)
|
||||
|
||||
// Same position should not be adjacent
|
||||
assert!(!is_adjacent((0, 0), (0, 0), true));
|
||||
|
||||
// Orthogonal adjacent positions should be true
|
||||
assert!(is_adjacent((0, 0), (1, 0), true)); // Right
|
||||
assert!(is_adjacent((0, 0), (0, 1), true)); // Down
|
||||
assert!(is_adjacent((1, 1), (0, 1), true)); // Left
|
||||
assert!(is_adjacent((1, 1), (1, 0), true)); // Up
|
||||
|
||||
// Diagonal adjacent positions should be true
|
||||
assert!(is_adjacent((0, 0), (1, 1), true)); // Down-right
|
||||
assert!(is_adjacent((1, 0), (0, 1), true)); // Down-left
|
||||
assert!(is_adjacent((0, 1), (1, 0), true)); // Up-right
|
||||
assert!(is_adjacent((1, 1), (0, 0), true)); // Up-left
|
||||
|
||||
// Positions more than 1 step away should be false
|
||||
assert!(!is_adjacent((0, 0), (2, 0), true));
|
||||
assert!(!is_adjacent((0, 0), (0, 2), true));
|
||||
assert!(!is_adjacent((0, 0), (2, 2), true));
|
||||
assert!(!is_adjacent((0, 0), (1, 2), true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_cases() {
|
||||
// Test with larger coordinates
|
||||
assert!(is_adjacent((100, 100), (101, 100), false));
|
||||
assert!(is_adjacent((100, 100), (100, 101), false));
|
||||
assert!(!is_adjacent((100, 100), (102, 100), false));
|
||||
|
||||
assert!(is_adjacent((100, 100), (101, 101), true));
|
||||
assert!(!is_adjacent((100, 100), (102, 102), true));
|
||||
|
||||
// Test with zero coordinates
|
||||
assert!(is_adjacent((0, 0), (1, 0), false));
|
||||
assert!(is_adjacent((0, 0), (0, 1), false));
|
||||
assert!(is_adjacent((0, 0), (1, 1), true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commutative_property() {
|
||||
// The function should work the same regardless of parameter order
|
||||
assert_eq!(
|
||||
is_adjacent((1, 2), (2, 2), false),
|
||||
is_adjacent((2, 2), (1, 2), false)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
is_adjacent((1, 2), (2, 3), true),
|
||||
is_adjacent((2, 3), (1, 2), true)
|
||||
);
|
||||
}
|
||||
}
|
||||
181
src/main.rs
181
src/main.rs
@@ -1,24 +1,97 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
|
||||
use crate::game::Game;
|
||||
use sdl2::event::{Event};
|
||||
use sdl2::event::{Event, WindowEvent};
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::pixels::Color;
|
||||
use sdl2::render::{Canvas, Texture};
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::event;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
pub mod emscripten;
|
||||
#[cfg(windows)]
|
||||
use winapi::{
|
||||
shared::ntdef::NULL,
|
||||
um::{
|
||||
fileapi::{CreateFileA, OPEN_EXISTING},
|
||||
handleapi::INVALID_HANDLE_VALUE,
|
||||
processenv::SetStdHandle,
|
||||
winbase::{STD_ERROR_HANDLE, STD_OUTPUT_HANDLE},
|
||||
wincon::{AttachConsole, GetConsoleWindow},
|
||||
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
|
||||
},
|
||||
};
|
||||
|
||||
/// Attaches the process to the parent console on Windows.
|
||||
///
|
||||
/// This allows the application to print to the console when run from a terminal,
|
||||
/// which is useful for debugging purposes. If the application is not run from a
|
||||
/// terminal, this function does nothing.
|
||||
#[cfg(windows)]
|
||||
unsafe fn attach_console() {
|
||||
if !std::ptr::eq(GetConsoleWindow(), std::ptr::null_mut()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS) != 0 {
|
||||
let handle = CreateFileA(
|
||||
c"CONOUT$".as_ptr(),
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
std::ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
NULL,
|
||||
);
|
||||
|
||||
if handle != INVALID_HANDLE_VALUE {
|
||||
SetStdHandle(STD_OUTPUT_HANDLE, handle);
|
||||
SetStdHandle(STD_ERROR_HANDLE, handle);
|
||||
}
|
||||
}
|
||||
// Do NOT call AllocConsole here - we don't want a console when launched from Explorer
|
||||
}
|
||||
|
||||
mod constants;
|
||||
mod direction;
|
||||
mod game;
|
||||
mod pacman;
|
||||
mod entity;
|
||||
mod animation;
|
||||
mod audio;
|
||||
mod constants;
|
||||
mod debug;
|
||||
mod direction;
|
||||
mod edible;
|
||||
mod entity;
|
||||
mod game;
|
||||
mod ghost;
|
||||
mod ghosts;
|
||||
mod helper;
|
||||
mod map;
|
||||
mod modulation;
|
||||
mod pacman;
|
||||
mod asset;
|
||||
|
||||
/// The main entry point of the application.
|
||||
///
|
||||
/// This function initializes SDL, the window, the game state, and then enters
|
||||
/// the main game loop.
|
||||
pub fn main() {
|
||||
// Attaches the console on Windows for debugging purposes.
|
||||
#[cfg(windows)]
|
||||
unsafe {
|
||||
attach_console();
|
||||
}
|
||||
|
||||
let sdl_context = sdl2::init().unwrap();
|
||||
let video_subsystem = sdl_context.video().unwrap();
|
||||
let audio_subsystem = sdl_context.audio().unwrap();
|
||||
let ttf_context = sdl2::ttf::init().unwrap();
|
||||
|
||||
// Setup tracing
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_ansi(cfg!(not(target_os = "emscripten")))
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.finish()
|
||||
.with(ErrorLayer::default());
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).expect("Could not set global default");
|
||||
|
||||
let window = video_subsystem
|
||||
.window("Pac-Man", WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||
@@ -28,7 +101,6 @@ pub fn main() {
|
||||
|
||||
let mut canvas = window
|
||||
.into_canvas()
|
||||
.accelerated()
|
||||
.build()
|
||||
.expect("Could not build canvas");
|
||||
|
||||
@@ -37,54 +109,109 @@ pub fn main() {
|
||||
.expect("Could not set logical size");
|
||||
|
||||
let texture_creator = canvas.texture_creator();
|
||||
let mut game = Game::new(&mut canvas, &texture_creator);
|
||||
let mut game = Game::new(
|
||||
&mut canvas,
|
||||
&texture_creator,
|
||||
&ttf_context,
|
||||
&audio_subsystem,
|
||||
);
|
||||
|
||||
let mut event_pump = sdl_context
|
||||
.event_pump()
|
||||
.expect("Could not get SDL EventPump");
|
||||
|
||||
// Initial draw and tick
|
||||
game.draw();
|
||||
game.tick();
|
||||
|
||||
let loop_time = Duration::from_millis(1000 / 60);
|
||||
// The target time for each frame of the game loop (60 FPS).
|
||||
let loop_time = Duration::from_secs(1) / 60;
|
||||
|
||||
let mut paused = false;
|
||||
// Whether the window is currently shown.
|
||||
let mut shown = false;
|
||||
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
"Starting game loop ({:.3}ms)",
|
||||
loop_time.as_secs_f32() * 1000.0
|
||||
);
|
||||
let mut main_loop = || {
|
||||
let start = Instant::now();
|
||||
|
||||
// TODO: Fix key repeat delay issues by using a queue for keyboard events.
|
||||
// This would allow for instant key repeat without being affected by the
|
||||
// main loop's tick rate.
|
||||
for event in event_pump.poll_iter() {
|
||||
match event {
|
||||
Event::Window { win_event, .. } => match win_event {
|
||||
WindowEvent::Hidden => {
|
||||
event!(tracing::Level::DEBUG, "Window hidden");
|
||||
shown = false;
|
||||
}
|
||||
WindowEvent::Shown => {
|
||||
event!(tracing::Level::DEBUG, "Window shown");
|
||||
shown = true;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
// Handle quitting keys or window close
|
||||
Event::Quit { .. }
|
||||
| Event::KeyDown {
|
||||
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
|
||||
..
|
||||
} => return false,
|
||||
Event::KeyDown { keycode , .. } => {
|
||||
} => {
|
||||
event!(tracing::Level::INFO, "Exit requested. Exiting...");
|
||||
return false;
|
||||
}
|
||||
Event::KeyDown {
|
||||
keycode: Some(Keycode::P),
|
||||
..
|
||||
} => {
|
||||
paused = !paused;
|
||||
event!(
|
||||
tracing::Level::INFO,
|
||||
"{}",
|
||||
if paused { "Paused" } else { "Unpaused" }
|
||||
);
|
||||
}
|
||||
Event::KeyDown { keycode, .. } => {
|
||||
game.keyboard_event(keycode.unwrap());
|
||||
},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
game.tick();
|
||||
game.draw();
|
||||
// TODO: Implement a proper pausing mechanism that does not interfere with
|
||||
// statistic gathering and other background tasks.
|
||||
if !paused {
|
||||
game.tick();
|
||||
game.draw();
|
||||
}
|
||||
|
||||
if start.elapsed() < loop_time {
|
||||
::std::thread::sleep(loop_time - start.elapsed());
|
||||
let time = loop_time.saturating_sub(start.elapsed());
|
||||
if time != Duration::ZERO {
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
{
|
||||
spin_sleep::sleep(time);
|
||||
}
|
||||
#[cfg(target_os = "emscripten")]
|
||||
{
|
||||
std::thread::sleep(time);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Game loop behind schedule by: {:?}", start.elapsed() - loop_time);
|
||||
event!(
|
||||
tracing::Level::WARN,
|
||||
"Game loop behind schedule by: {:?}",
|
||||
start.elapsed() - loop_time
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
};
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
use emscripten::emscripten;
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
emscripten::set_main_loop_callback(main_loop);
|
||||
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
loop {
|
||||
if !main_loop() {
|
||||
break;
|
||||
|
||||
216
src/map.rs
Normal file
216
src/map.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
//! This module defines the game map and provides functions for interacting with it.
|
||||
use rand::seq::IteratorRandom;
|
||||
|
||||
use crate::constants::{MapTile, BOARD_OFFSET, CELL_SIZE};
|
||||
use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::ops::Add;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct SignedPosition {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Position {
|
||||
pub x: u32,
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
impl Add<SignedPosition> for Position {
|
||||
type Output = Position;
|
||||
fn add(self, rhs: SignedPosition) -> Self::Output {
|
||||
Position {
|
||||
x: (self.x as i32 + rhs.x) as u32,
|
||||
y: (self.y as i32 + rhs.y) as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<(u32, u32)> for Position {
|
||||
fn eq(&self, other: &(u32, u32)) -> bool {
|
||||
self.x == other.0 && self.y == other.1
|
||||
}
|
||||
}
|
||||
|
||||
impl Position {
|
||||
pub fn as_i32(&self) -> (i32, i32) {
|
||||
(self.x as i32, self.y as i32)
|
||||
}
|
||||
pub fn wrapping_add(&self, dx: i32, dy: i32) -> Position {
|
||||
Position {
|
||||
x: (self.x as i32 + dx) as u32,
|
||||
y: (self.y as i32 + dy) as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The game map.
|
||||
///
|
||||
/// The map is represented as a 2D array of `MapTile`s. It also stores a copy of
|
||||
/// the original map, which can be used to reset the map to its initial state.
|
||||
pub struct Map {
|
||||
/// The current state of the map.
|
||||
current: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize],
|
||||
/// The default state of the map.
|
||||
default: [[MapTile; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize],
|
||||
}
|
||||
|
||||
impl Map {
|
||||
/// Creates a new `Map` instance from a raw board layout.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `raw_board` - A 2D array of characters representing the board layout.
|
||||
pub fn new(raw_board: [&str; BOARD_HEIGHT as usize]) -> Map {
|
||||
let mut map = [[MapTile::Empty; BOARD_HEIGHT as usize]; BOARD_WIDTH as usize];
|
||||
|
||||
for (y, line) in raw_board.iter().enumerate().take(BOARD_HEIGHT as usize) {
|
||||
for (x, character) in line.chars().enumerate().take(BOARD_WIDTH as usize) {
|
||||
let tile = match character {
|
||||
'#' => MapTile::Wall,
|
||||
'.' => MapTile::Pellet,
|
||||
'o' => MapTile::PowerPellet,
|
||||
' ' => MapTile::Empty,
|
||||
'T' => MapTile::Tunnel,
|
||||
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => {
|
||||
MapTile::StartingPosition(c.to_digit(10).unwrap() as u8)
|
||||
}
|
||||
'=' => MapTile::Empty,
|
||||
_ => panic!("Unknown character in board: {character}"),
|
||||
};
|
||||
map[x][y] = tile;
|
||||
}
|
||||
}
|
||||
|
||||
Map {
|
||||
current: map,
|
||||
default: map,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the map to its original state.
|
||||
pub fn reset(&mut self) {
|
||||
// Restore the map to its original state
|
||||
for (x, col) in self
|
||||
.current
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.take(BOARD_WIDTH as usize)
|
||||
{
|
||||
for (y, cell) in col.iter_mut().enumerate().take(BOARD_HEIGHT as usize) {
|
||||
*cell = self.default[x][y];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the tile at the given cell coordinates.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cell` - The cell coordinates, in grid coordinates.
|
||||
pub fn get_tile(&self, cell: (i32, i32)) -> Option<MapTile> {
|
||||
let x = cell.0 as usize;
|
||||
let y = cell.1 as usize;
|
||||
|
||||
if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(self.current[x][y])
|
||||
}
|
||||
|
||||
/// Sets the tile at the given cell coordinates.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cell` - The cell coordinates, in grid coordinates.
|
||||
/// * `tile` - The tile to set.
|
||||
pub fn set_tile(&mut self, cell: (i32, i32), tile: MapTile) -> bool {
|
||||
let x = cell.0 as usize;
|
||||
let y = cell.1 as usize;
|
||||
|
||||
if x >= BOARD_WIDTH as usize || y >= BOARD_HEIGHT as usize {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.current[x][y] = tile;
|
||||
true
|
||||
}
|
||||
|
||||
/// Converts cell coordinates to pixel coordinates.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cell` - The cell coordinates, in grid coordinates.
|
||||
pub fn cell_to_pixel(cell: (u32, u32)) -> (i32, i32) {
|
||||
(
|
||||
(cell.0 * CELL_SIZE) as i32,
|
||||
((cell.1 + BOARD_OFFSET.1) * CELL_SIZE) as i32,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a reference to a cached vector of all valid playable positions in the maze.
|
||||
/// This is computed once using a flood fill from a random pellet, and then cached.
|
||||
pub fn get_valid_playable_positions(&mut self) -> &Vec<Position> {
|
||||
use MapTile::*;
|
||||
static CACHE: OnceCell<Vec<Position>> = OnceCell::new();
|
||||
if let Some(cached) = CACHE.get() {
|
||||
return cached;
|
||||
}
|
||||
// Find a random starting pellet
|
||||
let mut pellet_positions = vec![];
|
||||
for (x, col) in self.current.iter().enumerate().take(BOARD_WIDTH as usize) {
|
||||
for (y, &cell) in col.iter().enumerate().take(BOARD_HEIGHT as usize) {
|
||||
match cell {
|
||||
Pellet | PowerPellet => pellet_positions.push(Position {
|
||||
x: x as u32,
|
||||
y: y as u32,
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut rng = rand::rng();
|
||||
let &start = pellet_positions
|
||||
.iter()
|
||||
.choose(&mut rng)
|
||||
.expect("No pellet found for flood fill");
|
||||
// Flood fill
|
||||
let mut visited = HashSet::new();
|
||||
let mut queue = VecDeque::new();
|
||||
|
||||
queue.push_back(start);
|
||||
while let Some(pos) = queue.pop_front() {
|
||||
if !visited.insert(pos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match self.current[pos.x as usize][pos.y as usize] {
|
||||
Empty | Pellet | PowerPellet => {
|
||||
for offset in [
|
||||
SignedPosition { x: -1, y: 0 },
|
||||
SignedPosition { x: 1, y: 0 },
|
||||
SignedPosition { x: 0, y: -1 },
|
||||
SignedPosition { x: 0, y: 1 },
|
||||
] {
|
||||
let neighbor = pos + offset;
|
||||
if neighbor.x < BOARD_WIDTH && neighbor.y < BOARD_HEIGHT {
|
||||
let neighbor_tile =
|
||||
self.current[neighbor.x as usize][neighbor.y as usize];
|
||||
if matches!(neighbor_tile, Empty | Pellet | PowerPellet) {
|
||||
queue.push_back(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StartingPosition(_) | Wall | Tunnel => {}
|
||||
}
|
||||
}
|
||||
let mut result: Vec<Position> = visited.into_iter().collect();
|
||||
result.sort_unstable();
|
||||
CACHE.get_or_init(|| result)
|
||||
}
|
||||
}
|
||||
55
src/modulation.rs
Normal file
55
src/modulation.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
//! This module provides a tick modulator, which can be used to slow down
|
||||
//! operations by a percentage.
|
||||
/// A tick modulator allows you to slow down operations by a percentage.
|
||||
///
|
||||
/// Unfortunately, switching to floating point numbers for entities can induce floating point errors, slow down calculations
|
||||
/// and make the game less deterministic. This is why we use a speed modulator instead.
|
||||
/// Additionally, with small integers, lowering the speed by a percentage is not possible. For example, if we have a speed of 2,
|
||||
/// and we want to slow it down by 10%, we would need to slow it down by 0.2. However, since we are using integers, we can't.
|
||||
/// The only amount you can slow it down by is 1, which is 50% of the speed.
|
||||
///
|
||||
/// The basic principle of the Speed Modulator is to instead 'skip' movement ticks every now and then.
|
||||
/// At 60 ticks per second, skips could happen several times per second, or once every few seconds.
|
||||
/// Whatever it be, as long as the tick rate is high enough, the human eye will not be able to tell the difference.
|
||||
///
|
||||
/// For example, if we want to slow down the speed by 10%, we would need to skip every 10th tick.
|
||||
pub trait TickModulator {
|
||||
/// Creates a new tick modulator.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `percent` - The percentage to slow down by, from 0.0 to 1.0.
|
||||
fn new(percent: f32) -> Self;
|
||||
/// Returns whether or not the operation should be performed on this tick.
|
||||
fn next(&mut self) -> bool;
|
||||
}
|
||||
|
||||
/// A simple tick modulator that skips every Nth tick.
|
||||
pub struct SimpleTickModulator {
|
||||
tick_count: u32,
|
||||
ticks_left: u32,
|
||||
}
|
||||
|
||||
// TODO: Add tests for the tick modulator to ensure that it is working correctly.
|
||||
// TODO: Look into average precision and binary code modulation strategies to see
|
||||
// if they would be a better fit for this use case.
|
||||
impl TickModulator for SimpleTickModulator {
|
||||
fn new(percent: f32) -> Self {
|
||||
let ticks_required: u32 = (1f32 / (1f32 - percent)).round() as u32;
|
||||
|
||||
SimpleTickModulator {
|
||||
tick_count: ticks_required,
|
||||
ticks_left: ticks_required,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) -> bool {
|
||||
if self.ticks_left == 0 {
|
||||
self.ticks_left = self.tick_count;
|
||||
return false;
|
||||
}
|
||||
|
||||
self.ticks_left -= 1;
|
||||
true
|
||||
}
|
||||
}
|
||||
158
src/pacman.rs
158
src/pacman.rs
@@ -1,64 +1,134 @@
|
||||
//! This module defines the Pac-Man entity, including its behavior and rendering.
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use sdl2::{
|
||||
render::{Canvas, Texture},
|
||||
video::Window,
|
||||
};
|
||||
|
||||
use crate::{animation::AnimatedTexture, direction::Direction, entity::Entity};
|
||||
use crate::{
|
||||
animation::{AnimatedAtlasTexture, FrameDrawn},
|
||||
direction::Direction,
|
||||
entity::{Entity, MovableEntity, Moving, Renderable, StaticEntity},
|
||||
map::Map,
|
||||
modulation::{SimpleTickModulator, TickModulator},
|
||||
};
|
||||
|
||||
/// The Pac-Man entity.
|
||||
pub struct Pacman<'a> {
|
||||
// Absolute position on the board (precise)
|
||||
pub position: (i32, i32),
|
||||
pub direction: Direction,
|
||||
speed: u32,
|
||||
sprite: AnimatedTexture<'a>,
|
||||
/// Shared movement and position fields.
|
||||
pub base: MovableEntity,
|
||||
/// The next direction of Pac-Man, which will be applied when Pac-Man is next aligned with the grid.
|
||||
pub next_direction: Option<Direction>,
|
||||
/// Whether Pac-Man is currently stopped.
|
||||
pub stopped: bool,
|
||||
pub sprite: AnimatedAtlasTexture<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Entity for Pacman<'a> {
|
||||
fn base(&self) -> &StaticEntity {
|
||||
&self.base.base
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Moving for Pacman<'a> {
|
||||
fn move_forward(&mut self) {
|
||||
self.base.move_forward();
|
||||
}
|
||||
fn update_cell_position(&mut self) {
|
||||
self.base.update_cell_position();
|
||||
}
|
||||
fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
|
||||
self.base.next_cell(direction)
|
||||
}
|
||||
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
|
||||
self.base.is_wall_ahead(direction)
|
||||
}
|
||||
fn handle_tunnel(&mut self) -> bool {
|
||||
self.base.handle_tunnel()
|
||||
}
|
||||
fn is_grid_aligned(&self) -> bool {
|
||||
self.base.is_grid_aligned()
|
||||
}
|
||||
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
|
||||
self.base.set_direction_if_valid(new_direction)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pacman<'_> {
|
||||
pub fn new<'a>(starting_position: Option<(i32, i32)>, atlas: Texture<'a>) -> Pacman<'a> {
|
||||
/// Creates a new `Pacman` instance.
|
||||
pub fn new<'a>(
|
||||
starting_position: (u32, u32),
|
||||
atlas: Texture<'a>,
|
||||
map: Rc<RefCell<Map>>,
|
||||
) -> Pacman<'a> {
|
||||
let pixel_position = Map::cell_to_pixel(starting_position);
|
||||
Pacman {
|
||||
position: starting_position.unwrap_or((0i32, 0i32)),
|
||||
direction: Direction::Right,
|
||||
speed: 2,
|
||||
sprite: AnimatedTexture::new(atlas, 4, 3, 32, 32),
|
||||
base: MovableEntity::new(
|
||||
pixel_position,
|
||||
starting_position,
|
||||
Direction::Right,
|
||||
3,
|
||||
SimpleTickModulator::new(1.0),
|
||||
map,
|
||||
),
|
||||
next_direction: None,
|
||||
stopped: false,
|
||||
sprite: AnimatedAtlasTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, canvas: &mut Canvas<Window>) {
|
||||
self.sprite.render(canvas, self.position, self.direction);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Pacman<'_> {
|
||||
fn is_colliding(&self, other: &dyn Entity) -> bool {
|
||||
let (x, y) = self.position();
|
||||
let (other_x, other_y) = other.position();
|
||||
x == other_x && y == other_y
|
||||
}
|
||||
|
||||
fn position(&self) -> (i32, i32) {
|
||||
self.position
|
||||
}
|
||||
|
||||
fn cell_position(&self) -> (u32, u32) {
|
||||
let (x, y) = self.position();
|
||||
(x as u32 / 24, y as u32 / 24)
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
let speed = self.speed as i32;
|
||||
match self.direction {
|
||||
Direction::Right => {
|
||||
self.position.0 += speed;
|
||||
/// Handles a requested direction change.
|
||||
fn handle_direction_change(&mut self) -> bool {
|
||||
match self.next_direction {
|
||||
None => return false,
|
||||
Some(next_direction) => {
|
||||
if <Pacman as Moving>::set_direction_if_valid(self, next_direction) {
|
||||
self.next_direction = None;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Direction::Left => {
|
||||
self.position.0 -= speed;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns the internal position of Pac-Man, rounded down to the nearest even number.
|
||||
fn internal_position_even(&self) -> (u32, u32) {
|
||||
let (x, y) = self.base.internal_position();
|
||||
((x / 2u32) * 2u32, (y / 2u32) * 2u32)
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
let can_change = self.internal_position_even() == (0, 0);
|
||||
if can_change {
|
||||
<Pacman as Moving>::update_cell_position(self);
|
||||
if !<Pacman as Moving>::handle_tunnel(self) {
|
||||
self.handle_direction_change();
|
||||
if !self.stopped && <Pacman as Moving>::is_wall_ahead(self, None) {
|
||||
self.stopped = true;
|
||||
} else if self.stopped && !<Pacman as Moving>::is_wall_ahead(self, None) {
|
||||
self.stopped = false;
|
||||
}
|
||||
}
|
||||
Direction::Up => {
|
||||
self.position.1 -= speed;
|
||||
}
|
||||
Direction::Down => {
|
||||
self.position.1 += speed;
|
||||
}
|
||||
if !self.stopped && self.base.modulation.next() {
|
||||
<Pacman as Moving>::move_forward(self);
|
||||
if self.internal_position_even() == (0, 0) {
|
||||
<Pacman as Moving>::update_cell_position(self);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderable for Pacman<'_> {
|
||||
fn render(&self, canvas: &mut Canvas<Window>) {
|
||||
let pos = self.base.base.pixel_position;
|
||||
let dir = self.base.direction;
|
||||
if self.stopped {
|
||||
self.sprite.render(canvas, pos, dir, Some(2));
|
||||
} else {
|
||||
self.sprite.render(canvas, pos, dir, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user