Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 0120abe806 | |||
| e61930c08a | |||
| f7ff9f5290 | |||
| de29dc6711 | |||
| c90f221c73 | |||
| 841943e121 | |||
| 83d665123c | |||
| ffc21c8622 | |||
| b46a51bc76 | |||
| 443afb1223 | |||
| 724878dc17 | |||
| 274404b9ea | |||
| 2214a5541f | |||
| 64de5fb732 | |||
| 4a4e6e40a9 | |||
| ccde1b6538 | |||
| d90785a61a | |||
| 227c603ffe | |||
| 361ff95f7f | |||
| f3fe1b783b | |||
| 570cee0f62 | |||
| 5b6fd34a6f | |||
| 973fa08ac9 | |||
| 4a1f565f72 | |||
| 2ddf2611d9 | |||
| b3a3664578 | |||
| 2b667bb6a2 | |||
| 9a88e71202 |
12
.cargo/config.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[target.'cfg(target_os = "emscripten")']
|
||||||
|
# TODO: Document what the fuck this is.
|
||||||
|
rustflags = [
|
||||||
|
# "-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/",
|
||||||
|
]
|
||||||
225
.github/workflows/deploy.yaml
vendored
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
wasm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- 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:
|
||||||
|
target: wasm32-unknown-emscripten
|
||||||
|
|
||||||
|
- name: Rust Cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v3
|
||||||
|
with:
|
||||||
|
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
|
||||||
6
.gitignore
vendored
@@ -1 +1,7 @@
|
|||||||
/target
|
/target
|
||||||
|
/dist
|
||||||
|
.idea
|
||||||
|
*.dll
|
||||||
|
rust-sdl2-emscripten/
|
||||||
|
assets/build.css
|
||||||
|
emsdk/
|
||||||
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
|
||||||
598
Cargo.lock
generated
@@ -1,20 +1,40 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "Pac-Man"
|
name = "aho-corasick"
|
||||||
version = "0.1.0"
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sdl2",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
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]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -22,24 +42,280 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "deprecate-until"
|
||||||
version = "1.4.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.147"
|
version = "0.2.174"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[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 = [
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"pathfinding",
|
||||||
|
"rand",
|
||||||
|
"sdl2",
|
||||||
|
"spin_sleep",
|
||||||
|
"tracing",
|
||||||
|
"tracing-error",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathfinding"
|
||||||
|
version = "4.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ac35caa284c08f3721fb33c2741b5f763decaf42d080c8a6a722154347017e"
|
||||||
|
dependencies = [
|
||||||
|
"deprecate-until",
|
||||||
|
"indexmap",
|
||||||
|
"integer-sqrt",
|
||||||
|
"num-traits",
|
||||||
|
"rustc-hash",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "sdl2"
|
name = "sdl2"
|
||||||
version = "0.35.2"
|
version = "0.38.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a"
|
checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags 1.3.2",
|
||||||
|
"c_vec",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"sdl2-sys",
|
"sdl2-sys",
|
||||||
@@ -47,17 +323,311 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sdl2-sys"
|
name = "sdl2-sys"
|
||||||
version = "0.35.2"
|
version = "0.38.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0"
|
checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
|
"vcpkg",
|
||||||
"version-compare",
|
"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 = "2.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
|
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",
|
||||||
|
]
|
||||||
|
|||||||
38
Cargo.toml
@@ -1,9 +1,43 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "Pac-Man"
|
name = "pacman"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
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"
|
||||||
|
|
||||||
|
[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"
|
||||||
|
|||||||
35
IMPLEMENTATION.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Implementation
|
||||||
|
|
||||||
|
A document detailing the implementation the project from rendering, to game logic, to build systems.
|
||||||
|
|
||||||
|
## Rendering
|
||||||
|
|
||||||
|
1. Map
|
||||||
|
- May require procedural text generation later on (cacheable?)
|
||||||
|
2. Pacman
|
||||||
|
3. Ghosts
|
||||||
|
- Requires colors
|
||||||
|
4. Items
|
||||||
|
5. Interface
|
||||||
|
- Requires fonts
|
||||||
|
|
||||||
|
## Grid System
|
||||||
|
|
||||||
|
1. How does the grid system work?
|
||||||
|
|
||||||
|
The grid is 28 x 36 (although, the map texture is 28 x 37), and each cell is 24x24 (pixels).
|
||||||
|
Many of the walls in the map texture only occupy a portion of the cell, so some items are able to render across multiple cells.
|
||||||
|
24x24 assets include pellets, the energizer, and the map itself ()
|
||||||
|
|
||||||
|
2. What constraints must be enforced on Ghosts and PacMan?
|
||||||
|
|
||||||
|
3. How do movement transitions work?
|
||||||
|
|
||||||
|
All entities store a precise position, and a direction. This position is only used for animation, rendering, and collision purposes. Otherwise, a separate 'cell position' (which is 24 times less precise, owing to the fact that it is based on the entity's position within the grid).
|
||||||
|
|
||||||
|
When an entity is transitioning between cells, movement directions are acknowledged, but won't take effect until the next cell has been entered completely.
|
||||||
|
|
||||||
|
4. Between transitions, how does collision detection work?
|
||||||
|
|
||||||
|
It appears the original implementation used cell-level detection.
|
||||||
|
I worry this may be prone to division errors. Make sure to use rounding (50% >=).
|
||||||
69
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
If the title doesn't clue you in, I'm remaking Pac-Man with SDL and Rust.
|
If the title doesn't clue you in, I'm remaking Pac-Man with SDL and Rust.
|
||||||
|
|
||||||
The project is *extremely* early in development, but check back in a week, and maybe I'll have something cool to look
|
The project is _extremely_ early in development, but check back in a week, and maybe I'll have something cool to look
|
||||||
at.
|
at.
|
||||||
|
|
||||||
## Feature Targets
|
## Feature Targets
|
||||||
@@ -10,3 +10,70 @@ at.
|
|||||||
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors.
|
- Near-perfect replication of logic, scoring, graphics, sound, and behaviors.
|
||||||
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly.
|
- Written in Rust, buildable on Windows, Linux, Mac and WebAssembly.
|
||||||
- Online demo, playable in a browser.
|
- Online demo, playable in a browser.
|
||||||
|
- Automatic build system, with releases for Windows, Linux, and Mac & Web-Assembly.
|
||||||
|
- Debug tooling
|
||||||
|
- Game state visualization
|
||||||
|
- Game speed controls + pausing
|
||||||
|
- Log tracing
|
||||||
|
- Performance details
|
||||||
|
|
||||||
|
## Experimental Ideas
|
||||||
|
|
||||||
|
- Perfected Ghost Algorithms
|
||||||
|
- 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
|
||||||
|
|
||||||
|
Besides SDL2, the following extensions are required: Image, Mixer, and TTF.
|
||||||
|
|
||||||
|
### Ubuntu
|
||||||
|
|
||||||
|
On Ubuntu, you can install the required packages with the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
On Windows, installation requires either building from source (not covered), or downloading the pre-built binaries.
|
||||||
|
|
||||||
|
The latest releases can be found here:
|
||||||
|
|
||||||
|
- [SDL2](https://github.com/libsdl-org/SDL/releases/latest/)
|
||||||
|
- [SDL2_image](https://github.com/libsdl-org/SDL_image/releases/latest/)
|
||||||
|
- [SDL2_mixer](https://github.com/libsdl-org/SDL_mixer/releases/latest/)
|
||||||
|
- [SDL2_ttf](https://github.com/libsdl-org/SDL_ttf/releases/latest/)
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
During development, you can easily run the project with:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run
|
||||||
|
cargo run -q # Quiet mode, no logging
|
||||||
|
cargo run --release # Release mode, optimized
|
||||||
|
```
|
||||||
BIN
SDL2_image.dll
BIN
SDL2_mixer.dll
BIN
SDL2_ttf.dll
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/24/energizer.png
Normal file
|
After Width: | Height: | Size: 174 B |
BIN
assets/24/pellet.png
Normal file
|
After Width: | Height: | Size: 158 B |
BIN
assets/32/fruit.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/32/game_over.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/32/ghost_body.png
Normal file
|
After Width: | Height: | Size: 528 B |
BIN
assets/32/ghost_eyes.png
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
assets/32/life.png
Normal file
|
After Width: | Height: | Size: 228 B |
BIN
assets/32/pacman.png
Normal file
|
After Width: | Height: | Size: 370 B |
BIN
assets/TerminalVector.ttf
Normal file
BIN
assets/door.png
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/font/konami.ttf
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
@@ -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/2.ogg
Normal file
BIN
assets/wav/3.ogg
Normal file
BIN
assets/wav/4.ogg
Normal file
BIN
assets/wav/eating.wav
Normal file
BIN
assets/wav/waka_ka.wav
Normal file
BIN
assets/wav/waka_wa.wav
Normal file
76
build.sh
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
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 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
@@ -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);
|
||||||
|
});
|
||||||
165
src/animation.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
//! This module provides a simple animation system for textures.
|
||||||
|
use sdl2::{
|
||||||
|
rect::Rect,
|
||||||
|
render::{Canvas, Texture},
|
||||||
|
video::Window,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::direction::Direction;
|
||||||
|
|
||||||
|
/// An animated texture, which is a texture that is rendered as a series of
|
||||||
|
/// frames.
|
||||||
|
///
|
||||||
|
/// This struct manages the state of an animated texture, including the current
|
||||||
|
/// frame and the number of frames in the animation.
|
||||||
|
pub struct AnimatedTexture<'a> {
|
||||||
|
// Parameters
|
||||||
|
raw_texture: Texture<'a>,
|
||||||
|
offset: (i32, i32),
|
||||||
|
ticks_per_frame: u32,
|
||||||
|
frame_count: u32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
// State
|
||||||
|
ticker: u32,
|
||||||
|
reversed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> AnimatedTexture<'a> {
|
||||||
|
pub fn new(
|
||||||
|
texture: Texture<'a>,
|
||||||
|
ticks_per_frame: u32,
|
||||||
|
frame_count: u32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
offset: Option<(i32, i32)>,
|
||||||
|
) -> Self {
|
||||||
|
AnimatedTexture {
|
||||||
|
raw_texture: texture,
|
||||||
|
ticker: 0,
|
||||||
|
reversed: false,
|
||||||
|
ticks_per_frame,
|
||||||
|
frame_count,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
offset: offset.unwrap_or((0, 0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_frame(&self) -> u32 {
|
||||||
|
self.ticker / self.ticks_per_frame
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances the animation by one tick.
|
||||||
|
///
|
||||||
|
/// This method updates the internal ticker that tracks the current frame
|
||||||
|
/// of the animation. The animation automatically reverses direction when
|
||||||
|
/// it reaches the end, creating a ping-pong effect.
|
||||||
|
///
|
||||||
|
/// When `reversed` is `false`, the ticker increments until it reaches
|
||||||
|
/// the total number of ticks for all frames, then reverses direction.
|
||||||
|
/// When `reversed` is `true`, the ticker decrements until it reaches 0,
|
||||||
|
/// then reverses direction again.
|
||||||
|
pub fn tick(&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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the source rectangle for a specific frame of the animated texture.
|
||||||
|
///
|
||||||
|
/// This method calculates the position and dimensions of a frame within the
|
||||||
|
/// texture atlas. Frames are arranged horizontally in a single row, so the
|
||||||
|
/// rectangle's x-coordinate is calculated by multiplying the frame index
|
||||||
|
/// by the frame width.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `frame` - The frame index to get the rectangle for (0-based)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A `Rect` representing the source rectangle for the specified frame
|
||||||
|
fn get_frame_rect(&self, frame: u32) -> Option<Rect> {
|
||||||
|
if frame >= self.frame_count {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Rect::new(
|
||||||
|
frame as i32 * self.width as i32,
|
||||||
|
0,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(
|
||||||
|
&mut self,
|
||||||
|
canvas: &mut Canvas<Window>,
|
||||||
|
position: (i32, i32),
|
||||||
|
direction: Direction,
|
||||||
|
) {
|
||||||
|
self.render_static(canvas, position, direction, Some(self.current_frame()));
|
||||||
|
self.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a specific frame of the animated texture to the canvas.
|
||||||
|
///
|
||||||
|
/// This method renders a static frame without advancing the animation ticker.
|
||||||
|
/// It's useful for displaying a specific frame, such as when an entity is stopped
|
||||||
|
/// or when you want to manually control which frame is displayed.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `canvas` - The SDL canvas to render to
|
||||||
|
/// * `position` - The pixel position where the texture should be rendered
|
||||||
|
/// * `direction` - The direction to rotate the texture based on entity facing
|
||||||
|
/// * `frame` - Optional specific frame to render. If `None`, uses the current frame
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// Panics if the specified frame is out of bounds for this texture.
|
||||||
|
pub fn render_static(
|
||||||
|
&mut self,
|
||||||
|
canvas: &mut Canvas<Window>,
|
||||||
|
position: (i32, i32),
|
||||||
|
direction: Direction,
|
||||||
|
frame: Option<u32>,
|
||||||
|
) {
|
||||||
|
let texture_source_frame_rect =
|
||||||
|
self.get_frame_rect(frame.unwrap_or_else(|| self.current_frame()));
|
||||||
|
let canvas_destination_rect = Rect::new(
|
||||||
|
position.0 + self.offset.0,
|
||||||
|
position.1 + self.offset.1,
|
||||||
|
self.width,
|
||||||
|
self.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas
|
||||||
|
.copy_ex(
|
||||||
|
&self.raw_texture,
|
||||||
|
texture_source_frame_rect,
|
||||||
|
Some(canvas_destination_rect),
|
||||||
|
direction.angle(),
|
||||||
|
None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("Could not render texture on canvas");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the color modulation for the texture.
|
||||||
|
pub fn set_color_modulation(&mut self, r: u8, g: u8, b: u8) {
|
||||||
|
self.raw_texture.set_color_mod(r, g, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/audio.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
//! This module handles the audio playback for the game.
|
||||||
|
use sdl2::{
|
||||||
|
mixer::{self, Chunk, InitFlag, LoaderRWops, DEFAULT_FORMAT},
|
||||||
|
rwops::RWops,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SOUND_1_DATA: &[u8] = include_bytes!("../assets/wav/1.ogg");
|
||||||
|
const SOUND_2_DATA: &[u8] = include_bytes!("../assets/wav/2.ogg");
|
||||||
|
const SOUND_3_DATA: &[u8] = include_bytes!("../assets/wav/3.ogg");
|
||||||
|
const SOUND_4_DATA: &[u8] = include_bytes!("../assets/wav/4.ogg");
|
||||||
|
|
||||||
|
/// An array of all the sound effect data.
|
||||||
|
const SOUND_DATA: [&[u8]; 4] = [SOUND_1_DATA, SOUND_2_DATA, SOUND_3_DATA, SOUND_4_DATA];
|
||||||
|
|
||||||
|
/// 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 as i32).set_volume(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mixer_context = mixer::init(InitFlag::OGG).expect("Failed to initialize SDL2_mixer");
|
||||||
|
|
||||||
|
let sounds: Vec<Chunk> = SOUND_DATA
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, data)| {
|
||||||
|
let rwops = RWops::from_bytes(data)
|
||||||
|
.expect(&format!("Failed to create RWops for sound {}", i + 1));
|
||||||
|
rwops.load_wav().expect(&format!(
|
||||||
|
"Failed to load sound {} from embedded data",
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/constants.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
//! This module contains all the constants used in the game.
|
||||||
|
|
||||||
|
/// The width of the game board, in cells.
|
||||||
|
pub const BOARD_WIDTH: u32 = 28;
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
/// The offset of the game board from the top-left corner of the window, in
|
||||||
|
/// cells.
|
||||||
|
pub const BOARD_OFFSET: (u32, u32) = (0, 3);
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The raw layout of the game board, as a 2D array of characters.
|
||||||
|
pub const RAW_BOARD: [&str; BOARD_HEIGHT as usize] = [
|
||||||
|
"############################",
|
||||||
|
"#............##............#",
|
||||||
|
"#.####.#####.##.#####.####.#",
|
||||||
|
"#o####.#####.##.#####.####o#",
|
||||||
|
"#.####.#####.##.#####.####.#",
|
||||||
|
"#..........................#",
|
||||||
|
"#.####.##.########.##.####.#",
|
||||||
|
"#.####.##.########.##.####.#",
|
||||||
|
"#......##....##....##......#",
|
||||||
|
"######.##### ## #####.######",
|
||||||
|
" #.##### ## #####.# ",
|
||||||
|
" #.## 1 ##.# ",
|
||||||
|
" #.## ###==### ##.# ",
|
||||||
|
"######.## # # ##.######",
|
||||||
|
"T . #2 3 4 # . T",
|
||||||
|
"######.## # # ##.######",
|
||||||
|
" #.## ######## ##.# ",
|
||||||
|
" #.## ##.# ",
|
||||||
|
" #.## ######## ##.# ",
|
||||||
|
"######.## ######## ##.######",
|
||||||
|
"#............##............#",
|
||||||
|
"#.####.#####.##.#####.####.#",
|
||||||
|
"#.####.#####.##.#####.####.#",
|
||||||
|
"#o..##.......0 .......##..o#",
|
||||||
|
"###.##.##.########.##.##.###",
|
||||||
|
"###.##.##.########.##.##.###",
|
||||||
|
"#......##....##....##......#",
|
||||||
|
"#.##########.##.##########.#",
|
||||||
|
"#.##########.##.##########.#",
|
||||||
|
"#..........................#",
|
||||||
|
"############################",
|
||||||
|
];
|
||||||
59
src/direction.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
//! 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,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Direction {
|
||||||
|
/// Returns the angle of the direction in degrees.
|
||||||
|
pub fn angle(&self) -> f64 {
|
||||||
|
match self {
|
||||||
|
Direction::Right => 0f64,
|
||||||
|
Direction::Down => 90f64,
|
||||||
|
Direction::Left => 180f64,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/entity.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//! This module defines the `Entity` trait, which is implemented by all game
|
||||||
|
//! objects that can be moved and rendered.
|
||||||
|
|
||||||
|
/// 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, in pixels.
|
||||||
|
fn position(&self) -> (i32, i32);
|
||||||
|
/// Returns the cell position of the entity, in grid coordinates.
|
||||||
|
fn cell_position(&self) -> (u32, u32);
|
||||||
|
/// Returns the position of the entity within its current cell, in pixels.
|
||||||
|
fn internal_position(&self) -> (u32, u32);
|
||||||
|
/// Ticks the entity, which updates its state and position.
|
||||||
|
fn tick(&mut self);
|
||||||
|
}
|
||||||
427
src/game.rs
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
//! 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::{Texture, TextureCreator};
|
||||||
|
use sdl2::rwops::RWops;
|
||||||
|
use sdl2::ttf::Font;
|
||||||
|
use sdl2::video::WindowContext;
|
||||||
|
use sdl2::{pixels::Color, render::Canvas, video::Window};
|
||||||
|
use tracing::event;
|
||||||
|
|
||||||
|
use crate::audio::Audio;
|
||||||
|
use crate::constants::{MapTile, BOARD_HEIGHT, BOARD_WIDTH, RAW_BOARD};
|
||||||
|
use crate::direction::Direction;
|
||||||
|
use crate::entity::Entity;
|
||||||
|
use crate::ghosts::Blinky;
|
||||||
|
use crate::map::Map;
|
||||||
|
use crate::pacman::Pacman;
|
||||||
|
|
||||||
|
// Embed texture data directly into the executable
|
||||||
|
static PACMAN_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/pacman.png");
|
||||||
|
static PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/pellet.png");
|
||||||
|
static POWER_PELLET_TEXTURE_DATA: &[u8] = include_bytes!("../assets/24/energizer.png");
|
||||||
|
static MAP_TEXTURE_DATA: &[u8] = include_bytes!("../assets/map.png");
|
||||||
|
static FONT_DATA: &[u8] = include_bytes!("../assets/font/konami.ttf");
|
||||||
|
|
||||||
|
// Add ghost texture data
|
||||||
|
static GHOST_BODY_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/ghost_body.png");
|
||||||
|
static GHOST_EYES_TEXTURE_DATA: &[u8] = include_bytes!("../assets/32/ghost_eyes.png");
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum DebugMode {
|
||||||
|
None,
|
||||||
|
Grid,
|
||||||
|
Pathfinding,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Game<'a> {
|
||||||
|
canvas: &'a mut Canvas<Window>,
|
||||||
|
map_texture: Texture<'a>,
|
||||||
|
pellet_texture: Texture<'a>,
|
||||||
|
power_pellet_texture: Texture<'a>,
|
||||||
|
font: Font<'a, 'static>,
|
||||||
|
pacman: Rc<RefCell<Pacman<'a>>>,
|
||||||
|
map: Rc<std::cell::RefCell<Map>>,
|
||||||
|
debug_mode: DebugMode,
|
||||||
|
score: u32,
|
||||||
|
audio: Audio,
|
||||||
|
// Add ghost
|
||||||
|
blinky: Blinky<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game<'_> {
|
||||||
|
/// 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<'a>(
|
||||||
|
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(std::cell::RefCell::new(Map::new(RAW_BOARD)));
|
||||||
|
|
||||||
|
// Load Pacman texture from embedded data
|
||||||
|
let pacman_atlas = texture_creator
|
||||||
|
.load_texture_bytes(PACMAN_TEXTURE_DATA)
|
||||||
|
.expect("Could not load pacman texture from embedded data");
|
||||||
|
let pacman = Rc::new(std::cell::RefCell::new(Pacman::new(
|
||||||
|
(1, 1),
|
||||||
|
pacman_atlas,
|
||||||
|
Rc::clone(&map),
|
||||||
|
)));
|
||||||
|
|
||||||
|
// Load ghost textures
|
||||||
|
let ghost_body = texture_creator
|
||||||
|
.load_texture_bytes(GHOST_BODY_TEXTURE_DATA)
|
||||||
|
.expect("Could not load ghost body texture from embedded data");
|
||||||
|
let ghost_eyes = texture_creator
|
||||||
|
.load_texture_bytes(GHOST_EYES_TEXTURE_DATA)
|
||||||
|
.expect("Could not load ghost eyes texture from embedded data");
|
||||||
|
|
||||||
|
// 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 embedded data
|
||||||
|
let pellet_texture = texture_creator
|
||||||
|
.load_texture_bytes(PELLET_TEXTURE_DATA)
|
||||||
|
.expect("Could not load pellet texture from embedded data");
|
||||||
|
|
||||||
|
// Load power pellet texture from embedded data
|
||||||
|
let power_pellet_texture = texture_creator
|
||||||
|
.load_texture_bytes(POWER_PELLET_TEXTURE_DATA)
|
||||||
|
.expect("Could not load power pellet texture from embedded data");
|
||||||
|
|
||||||
|
// Load font from embedded data
|
||||||
|
let font_rwops = RWops::from_bytes(FONT_DATA).expect("Failed to create RWops for font");
|
||||||
|
let font = ttf_context
|
||||||
|
.load_font_from_rwops(font_rwops, 24)
|
||||||
|
.expect("Could not load font from embedded data");
|
||||||
|
|
||||||
|
let audio = Audio::new();
|
||||||
|
|
||||||
|
// Load map texture from embedded data
|
||||||
|
let mut map_texture = texture_creator
|
||||||
|
.load_texture_bytes(MAP_TEXTURE_DATA)
|
||||||
|
.expect("Could not load map texture from embedded data");
|
||||||
|
map_texture.set_color_mod(0, 0, 255);
|
||||||
|
|
||||||
|
Game {
|
||||||
|
canvas,
|
||||||
|
pacman,
|
||||||
|
debug_mode: DebugMode::None,
|
||||||
|
map,
|
||||||
|
map_texture,
|
||||||
|
pellet_texture,
|
||||||
|
power_pellet_texture,
|
||||||
|
font,
|
||||||
|
score: 0,
|
||||||
|
audio,
|
||||||
|
blinky,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles a keyboard event.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `keycode` - The keycode of the key that was pressed.
|
||||||
|
pub fn keyboard_event(&mut self, keycode: Keycode) {
|
||||||
|
// Change direction
|
||||||
|
let direction = Direction::from_keycode(keycode);
|
||||||
|
self.pacman.borrow_mut().next_direction = direction;
|
||||||
|
|
||||||
|
// 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::None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset game
|
||||||
|
if keycode == Keycode::R {
|
||||||
|
self.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
// Reset Pacman position
|
||||||
|
let mut pacman = self.pacman.borrow_mut();
|
||||||
|
pacman.pixel_position = Map::cell_to_pixel((1, 1));
|
||||||
|
pacman.cell_position = (1, 1);
|
||||||
|
pacman.in_tunnel = false;
|
||||||
|
pacman.direction = Direction::Right;
|
||||||
|
pacman.next_direction = None;
|
||||||
|
pacman.stopped = false;
|
||||||
|
|
||||||
|
// Reset ghost positions and mode
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let map = self.map.borrow();
|
||||||
|
let mut valid_positions = vec![];
|
||||||
|
for x in 1..(crate::constants::BOARD_WIDTH - 1) {
|
||||||
|
for y in 1..(crate::constants::BOARD_HEIGHT - 1) {
|
||||||
|
let tile_option = map.get_tile((x as i32, y as i32));
|
||||||
|
|
||||||
|
if let Some(tile) = tile_option {
|
||||||
|
match tile {
|
||||||
|
MapTile::Empty | MapTile::Pellet | MapTile::PowerPellet => {
|
||||||
|
valid_positions.push((x, y));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(&(gx, gy)) = valid_positions.iter().choose(&mut rng) {
|
||||||
|
self.blinky.pixel_position = Map::cell_to_pixel((gx, gy));
|
||||||
|
self.blinky.cell_position = (gx, gy);
|
||||||
|
self.blinky.in_tunnel = false;
|
||||||
|
self.blinky.direction = Direction::Left;
|
||||||
|
self.blinky.mode = crate::ghost::GhostMode::Chase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances the game by one tick.
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
self.check_pellet_eating();
|
||||||
|
self.pacman.borrow_mut().tick();
|
||||||
|
self.blinky.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if Pac-Man is currently eating a pellet and updates the game state
|
||||||
|
/// accordingly.
|
||||||
|
fn check_pellet_eating(&mut self) {
|
||||||
|
let cell_pos = self.pacman.borrow().cell_position();
|
||||||
|
|
||||||
|
// Check if there's a pellet at the current position
|
||||||
|
let tile = {
|
||||||
|
let map = self.map.borrow();
|
||||||
|
map.get_tile((cell_pos.0 as i32, cell_pos.1 as i32))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(tile) = tile {
|
||||||
|
let pellet_value = match tile {
|
||||||
|
MapTile::Pellet => Some(10),
|
||||||
|
MapTile::PowerPellet => Some(50),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(value) = pellet_value {
|
||||||
|
{
|
||||||
|
let mut map = self.map.borrow_mut();
|
||||||
|
map.set_tile((cell_pos.0 as i32, cell_pos.1 as i32), MapTile::Empty);
|
||||||
|
}
|
||||||
|
self.add_score(value);
|
||||||
|
self.audio.eat();
|
||||||
|
event!(
|
||||||
|
tracing::Level::DEBUG,
|
||||||
|
"Pellet eaten at ({}, {})",
|
||||||
|
cell_pos.0,
|
||||||
|
cell_pos.1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 pellets
|
||||||
|
for x in 0..BOARD_WIDTH {
|
||||||
|
for y in 0..BOARD_HEIGHT {
|
||||||
|
let tile = self
|
||||||
|
.map
|
||||||
|
.borrow()
|
||||||
|
.get_tile((x as i32, y as i32))
|
||||||
|
.unwrap_or(MapTile::Empty);
|
||||||
|
|
||||||
|
let texture = match tile {
|
||||||
|
MapTile::Pellet => Some(&self.pellet_texture),
|
||||||
|
MapTile::PowerPellet => Some(&self.power_pellet_texture),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(texture) = texture {
|
||||||
|
let position = Map::cell_to_pixel((x, y));
|
||||||
|
let dst_rect = sdl2::rect::Rect::new(position.0, position.1, 24, 24);
|
||||||
|
self.canvas
|
||||||
|
.copy(texture, None, Some(dst_rect))
|
||||||
|
.expect("Could not render pellet");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Pac-Man
|
||||||
|
self.pacman.borrow_mut().render(self.canvas);
|
||||||
|
|
||||||
|
// Render ghost
|
||||||
|
self.blinky.render(self.canvas);
|
||||||
|
|
||||||
|
// Render score
|
||||||
|
self.render_ui();
|
||||||
|
|
||||||
|
// Draw the debug grid
|
||||||
|
if self.debug_mode == DebugMode::Grid {
|
||||||
|
for x in 0..BOARD_WIDTH {
|
||||||
|
for y in 0..BOARD_HEIGHT {
|
||||||
|
let tile = self
|
||||||
|
.map
|
||||||
|
.borrow()
|
||||||
|
.get_tile((x as i32, y as i32))
|
||||||
|
.unwrap_or(MapTile::Empty);
|
||||||
|
let mut color = None;
|
||||||
|
|
||||||
|
if (x, y) == self.pacman.borrow().cell_position() {
|
||||||
|
self.draw_cell((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((x, y), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the next cell
|
||||||
|
let next_cell = self.pacman.borrow().next_cell(None);
|
||||||
|
self.draw_cell((next_cell.0 as u32, next_cell.1 as u32), Color::YELLOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pathfinding debug mode
|
||||||
|
if self.debug_mode == DebugMode::Pathfinding {
|
||||||
|
// Show the current path for Blinky
|
||||||
|
if let Some((path, _)) = self.blinky.get_path_to_target({
|
||||||
|
let (tx, ty) = self.blinky.get_target_tile();
|
||||||
|
(tx as u32, ty as u32)
|
||||||
|
}) {
|
||||||
|
for &(x, y) in &path {
|
||||||
|
self.draw_cell((x, y), Color::YELLOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the canvas
|
||||||
|
self.canvas.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a single cell to the canvas with the given color.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `cell` - The cell to draw, in grid coordinates.
|
||||||
|
/// * `color` - The color to draw the cell with.
|
||||||
|
fn draw_cell(&mut self, cell: (u32, u32), color: Color) {
|
||||||
|
let position = Map::cell_to_pixel(cell);
|
||||||
|
|
||||||
|
self.canvas.set_draw_color(color);
|
||||||
|
self.canvas
|
||||||
|
.draw_rect(sdl2::rect::Rect::new(
|
||||||
|
position.0 as i32,
|
||||||
|
position.1 as i32,
|
||||||
|
24,
|
||||||
|
24,
|
||||||
|
))
|
||||||
|
.expect("Could not draw rectangle");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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!("{}UP HIGH SCORE ", lives),
|
||||||
|
(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");
|
||||||
|
}
|
||||||
|
}
|
||||||
402
src/ghost.rs
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
use pathfinding::prelude::dijkstra;
|
||||||
|
use sdl2::{
|
||||||
|
pixels::Color,
|
||||||
|
render::{Canvas, Texture},
|
||||||
|
video::Window,
|
||||||
|
};
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
animation::AnimatedTexture,
|
||||||
|
constants::{MapTile, BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE},
|
||||||
|
direction::Direction,
|
||||||
|
entity::Entity,
|
||||||
|
map::Map,
|
||||||
|
modulation::{SimpleTickModulator, TickModulator},
|
||||||
|
pacman::Pacman,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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> {
|
||||||
|
/// The absolute position of the ghost on the board, in pixels
|
||||||
|
pub pixel_position: (i32, i32),
|
||||||
|
/// The position of the ghost on the board, in grid coordinates
|
||||||
|
pub cell_position: (u32, u32),
|
||||||
|
/// The current direction of the ghost
|
||||||
|
pub direction: Direction,
|
||||||
|
/// The current mode of the ghost
|
||||||
|
pub mode: GhostMode,
|
||||||
|
/// The type/personality of this ghost
|
||||||
|
pub ghost_type: GhostType,
|
||||||
|
/// Reference to the game map
|
||||||
|
pub map: Rc<RefCell<Map>>,
|
||||||
|
/// Reference to Pac-Man for targeting
|
||||||
|
pub pacman: Rc<RefCell<Pacman<'a>>>,
|
||||||
|
/// Movement speed
|
||||||
|
speed: u32,
|
||||||
|
/// Movement modulator
|
||||||
|
modulation: SimpleTickModulator,
|
||||||
|
/// Ghost body sprite
|
||||||
|
body_sprite: AnimatedTexture<'a>,
|
||||||
|
/// Ghost eyes sprite
|
||||||
|
eyes_sprite: AnimatedTexture<'a>,
|
||||||
|
/// Whether the ghost is currently in a tunnel
|
||||||
|
pub in_tunnel: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = AnimatedTexture::new(body_texture, 8, 2, 32, 32, Some((-4, -4)));
|
||||||
|
body_sprite.set_color_modulation(color.r, color.g, color.b);
|
||||||
|
|
||||||
|
Ghost {
|
||||||
|
pixel_position: Map::cell_to_pixel(starting_position),
|
||||||
|
cell_position: starting_position,
|
||||||
|
direction: Direction::Left,
|
||||||
|
mode: GhostMode::Chase,
|
||||||
|
ghost_type,
|
||||||
|
map,
|
||||||
|
pacman,
|
||||||
|
speed: 3,
|
||||||
|
modulation: SimpleTickModulator::new(1.0),
|
||||||
|
body_sprite,
|
||||||
|
eyes_sprite: AnimatedTexture::new(eyes_texture, 1, 4, 32, 32, Some((-4, -4))),
|
||||||
|
in_tunnel: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the ghost to the canvas
|
||||||
|
pub fn render(&mut self, canvas: &mut Canvas<Window>) {
|
||||||
|
// Render body
|
||||||
|
if self.mode != GhostMode::Eyes {
|
||||||
|
let color = if self.mode == GhostMode::Frightened {
|
||||||
|
Color::RGB(0, 0, 255)
|
||||||
|
} else {
|
||||||
|
self.ghost_type.color()
|
||||||
|
};
|
||||||
|
|
||||||
|
self.body_sprite
|
||||||
|
.set_color_modulation(color.r, color.g, color.b);
|
||||||
|
self.body_sprite
|
||||||
|
.render(canvas, self.pixel_position, Direction::Right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always render eyes on top
|
||||||
|
let eye_frame = if self.mode == GhostMode::Frightened {
|
||||||
|
4 // Frightened frame
|
||||||
|
} else {
|
||||||
|
match self.direction {
|
||||||
|
Direction::Right => 0,
|
||||||
|
Direction::Up => 1,
|
||||||
|
Direction::Left => 2,
|
||||||
|
Direction::Down => 3,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.eyes_sprite.render_static(
|
||||||
|
canvas,
|
||||||
|
self.pixel_position,
|
||||||
|
Direction::Right,
|
||||||
|
Some(eye_frame),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.cell_position;
|
||||||
|
let map = self.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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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::thread_rng();
|
||||||
|
let (x, y) = self.cell_position;
|
||||||
|
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.direction.opposite() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (dx, dy) = dir.offset();
|
||||||
|
let next_cell = (x as i32 + dx, y as i32 + dy);
|
||||||
|
let tile = self.map.borrow().get_tile(next_cell);
|
||||||
|
if let Some(MapTile::Wall) = tile {
|
||||||
|
// It's a wall, not a valid move
|
||||||
|
} else {
|
||||||
|
possible_moves.push(next_cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if possible_moves.is_empty() {
|
||||||
|
// No valid moves, must reverse
|
||||||
|
let (dx, dy) = self.direction.opposite().offset();
|
||||||
|
return (x as i32 + dx, y as i32 + dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose a random valid move
|
||||||
|
possible_moves[rng.gen_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) {
|
||||||
|
// Default implementation just targets Pac-Man directly
|
||||||
|
let pacman = self.pacman.borrow();
|
||||||
|
(pacman.cell_position.0 as i32, pacman.cell_position.1 as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
if should_reverse {
|
||||||
|
self.direction = self.direction.opposite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for Ghost<'_> {
|
||||||
|
fn position(&self) -> (i32, i32) {
|
||||||
|
self.pixel_position
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_position(&self) -> (u32, u32) {
|
||||||
|
self.cell_position
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internal_position(&self) -> (u32, u32) {
|
||||||
|
let (x, y) = self.position();
|
||||||
|
(x as u32 % CELL_SIZE, y as u32 % CELL_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 tick(&mut self) {
|
||||||
|
if self.mode == GhostMode::House {
|
||||||
|
// For now, do nothing in the house
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.internal_position() == (0, 0) {
|
||||||
|
self.cell_position = (
|
||||||
|
(self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0,
|
||||||
|
(self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
let current_tile = self
|
||||||
|
.map
|
||||||
|
.borrow()
|
||||||
|
.get_tile((self.cell_position.0 as i32, self.cell_position.1 as i32))
|
||||||
|
.unwrap_or(MapTile::Empty);
|
||||||
|
if current_tile == MapTile::Tunnel {
|
||||||
|
self.in_tunnel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tunnel logic: if in tunnel, force movement and prevent direction change
|
||||||
|
if self.in_tunnel {
|
||||||
|
// If out of bounds, teleport to the opposite side and exit tunnel
|
||||||
|
if self.cell_position.0 == 0 {
|
||||||
|
self.cell_position.0 = BOARD_WIDTH - 2;
|
||||||
|
self.pixel_position =
|
||||||
|
Map::cell_to_pixel((self.cell_position.0, self.cell_position.1));
|
||||||
|
self.in_tunnel = false;
|
||||||
|
} else if self.cell_position.0 == BOARD_WIDTH - 1 {
|
||||||
|
self.cell_position.0 = 1;
|
||||||
|
self.pixel_position =
|
||||||
|
Map::cell_to_pixel((self.cell_position.0, self.cell_position.1));
|
||||||
|
self.in_tunnel = false;
|
||||||
|
} else {
|
||||||
|
// While in tunnel, do not allow direction change
|
||||||
|
// and always move in the current direction
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 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.cell_position;
|
||||||
|
let dx = next_move.0 as i32 - x as i32;
|
||||||
|
let dy = next_move.1 as i32 - y as i32;
|
||||||
|
self.direction = if dx > 0 {
|
||||||
|
Direction::Right
|
||||||
|
} else if dx < 0 {
|
||||||
|
Direction::Left
|
||||||
|
} else if dy > 0 {
|
||||||
|
Direction::Down
|
||||||
|
} else {
|
||||||
|
Direction::Up
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the next tile in the current direction is a wall
|
||||||
|
let (dx, dy) = self.direction.offset();
|
||||||
|
let next_cell = (
|
||||||
|
self.cell_position.0 as i32 + dx,
|
||||||
|
self.cell_position.1 as i32 + dy,
|
||||||
|
);
|
||||||
|
let next_tile = self
|
||||||
|
.map
|
||||||
|
.borrow()
|
||||||
|
.get_tile(next_cell)
|
||||||
|
.unwrap_or(MapTile::Empty);
|
||||||
|
if next_tile == MapTile::Wall {
|
||||||
|
// Don't move if the next tile is a wall
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.modulation.next() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position based on current direction and speed
|
||||||
|
let speed = self.speed as i32;
|
||||||
|
match self.direction {
|
||||||
|
Direction::Right => self.pixel_position.0 += speed,
|
||||||
|
Direction::Left => self.pixel_position.0 -= speed,
|
||||||
|
Direction::Up => self.pixel_position.1 -= speed,
|
||||||
|
Direction::Down => self.pixel_position.1 += speed,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cell position when aligned with grid
|
||||||
|
if self.internal_position() == (0, 0) {
|
||||||
|
self.cell_position = (
|
||||||
|
(self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0,
|
||||||
|
(self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/ghosts/blinky.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use sdl2::render::{Canvas, Texture};
|
||||||
|
use sdl2::video::Window;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entity::Entity,
|
||||||
|
ghost::{Ghost, GhostMode, GhostType},
|
||||||
|
map::Map,
|
||||||
|
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();
|
||||||
|
(pacman.cell_position.0 as i32, pacman.cell_position.1 as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mode(&mut self, mode: GhostMode) {
|
||||||
|
self.ghost.set_mode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, canvas: &mut Canvas<Window>) {
|
||||||
|
self.ghost.render(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Entity for Blinky<'a> {
|
||||||
|
fn position(&self) -> (i32, i32) {
|
||||||
|
self.ghost.position()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_position(&self) -> (u32, u32) {
|
||||||
|
self.ghost.cell_position()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internal_position(&self) -> (u32, u32) {
|
||||||
|
self.ghost.internal_position()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_colliding(&self, other: &dyn Entity) -> bool {
|
||||||
|
self.ghost.is_colliding(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&mut self) {
|
||||||
|
self.ghost.tick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/ghosts/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod blinky;
|
||||||
|
|
||||||
|
pub use blinky::Blinky;
|
||||||
112
src/helper.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
//! 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;
|
||||||
|
|
||||||
|
// Calculate absolute differences between coordinates
|
||||||
|
let dx = if ax > bx { ax - bx } else { bx - ax };
|
||||||
|
let dy = if ay > by { ay - by } else { by - ay };
|
||||||
|
|
||||||
|
if diagonal {
|
||||||
|
// For diagonal adjacency: both differences must be ≤ 1 and at least one > 0
|
||||||
|
dx <= 1 && dy <= 1 && (dx + dy) > 0
|
||||||
|
} else {
|
||||||
|
// For orthogonal adjacency: exactly one difference must be 1, the other 0
|
||||||
|
(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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
234
src/main.rs
@@ -1,45 +1,237 @@
|
|||||||
use sdl2::pixels::Color;
|
#![windows_subsystem = "windows"]
|
||||||
use sdl2::event::Event;
|
|
||||||
use sdl2::image::LoadTexture;
|
|
||||||
use sdl2::keyboard::{Keycode, Mod};
|
|
||||||
use std::time::Duration;
|
|
||||||
use crate::constants::{WINDOW_WIDTH, WINDOW_HEIGHT};
|
|
||||||
|
|
||||||
|
use crate::constants::{WINDOW_HEIGHT, WINDOW_WIDTH};
|
||||||
|
use crate::game::Game;
|
||||||
|
use sdl2::event::{Event, WindowEvent};
|
||||||
|
use sdl2::keyboard::Keycode;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use tracing::event;
|
||||||
|
use tracing_error::ErrorLayer;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use winapi::{
|
||||||
|
shared::{ntdef::NULL, windef::HWND},
|
||||||
|
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 GetConsoleWindow() != std::ptr::null_mut() as HWND {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS) != 0 {
|
||||||
|
let handle = CreateFileA(
|
||||||
|
"CONOUT$\0".as_ptr() as *const i8,
|
||||||
|
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 animation;
|
||||||
|
mod audio;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod board;
|
mod direction;
|
||||||
|
mod entity;
|
||||||
mod game;
|
mod game;
|
||||||
|
mod ghost;
|
||||||
|
mod ghosts;
|
||||||
|
mod helper;
|
||||||
|
mod map;
|
||||||
|
mod modulation;
|
||||||
|
mod pacman;
|
||||||
|
|
||||||
|
/// 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() {
|
pub fn main() {
|
||||||
|
// Attaches the console on Windows for debugging purposes.
|
||||||
|
#[cfg(windows)]
|
||||||
|
unsafe {
|
||||||
|
attach_console();
|
||||||
|
}
|
||||||
|
|
||||||
let sdl_context = sdl2::init().unwrap();
|
let sdl_context = sdl2::init().unwrap();
|
||||||
let video_subsystem = sdl_context.video().unwrap();
|
let video_subsystem = sdl_context.video().unwrap();
|
||||||
|
let audio_subsystem = sdl_context.audio().unwrap();
|
||||||
|
let ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
|
||||||
let window = video_subsystem.window("Pac-Man", WINDOW_WIDTH, WINDOW_HEIGHT)
|
// 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)
|
||||||
.position_centered()
|
.position_centered()
|
||||||
.build()
|
.build()
|
||||||
.expect("Could not initialize window");
|
.expect("Could not initialize window");
|
||||||
|
|
||||||
let mut canvas = window.into_canvas().build().expect("Could not build canvas");
|
let mut canvas = window
|
||||||
let texture_creator= canvas.texture_creator();
|
.into_canvas()
|
||||||
|
.build()
|
||||||
|
.expect("Could not build canvas");
|
||||||
|
|
||||||
let map_texture = texture_creator.load_texture("assets/map.png").expect("Could not load pacman texture");
|
canvas
|
||||||
canvas.copy(&map_texture, None, None).expect("Could not render texture on canvas");
|
.set_logical_size(WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||||
|
.expect("Could not set logical size");
|
||||||
|
|
||||||
let mut event_pump = sdl_context.event_pump().expect("Could not get SDL EventPump");
|
let texture_creator = canvas.texture_creator();
|
||||||
'main: loop {
|
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();
|
||||||
|
|
||||||
|
// The target time for each frame of the game loop (60 FPS).
|
||||||
|
let loop_time = Duration::from_secs(1) / 60;
|
||||||
|
let mut tick_no = 0u32;
|
||||||
|
|
||||||
|
// The start of a period of time over which we average the frame time.
|
||||||
|
let mut last_averaging_time = Instant::now();
|
||||||
|
// The total time spent sleeping during the current averaging period.
|
||||||
|
let mut sleep_time = Duration::ZERO;
|
||||||
|
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() {
|
for event in event_pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
Event::Quit { .. } |
|
Event::Window { win_event, .. } => match win_event {
|
||||||
Event::KeyDown { keycode: Some(Keycode::Q), .. } => {
|
WindowEvent::Hidden => {
|
||||||
break 'main;
|
event!(tracing::Level::DEBUG, "Window hidden");
|
||||||
|
shown = false;
|
||||||
}
|
}
|
||||||
event @ Event::KeyDown { .. } => {
|
WindowEvent::Shown => {
|
||||||
println!("{:?}", event);
|
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),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
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());
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.present();
|
// TODO: Implement a proper pausing mechanism that does not interfere with
|
||||||
::std::thread::sleep(Duration::from_millis(10));
|
// statistic gathering and other background tasks.
|
||||||
|
if !paused {
|
||||||
|
game.tick();
|
||||||
|
game.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if start.elapsed() < loop_time {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep_time += time;
|
||||||
|
} else {
|
||||||
|
event!(
|
||||||
|
tracing::Level::WARN,
|
||||||
|
"Game loop behind schedule by: {:?}",
|
||||||
|
start.elapsed() - loop_time
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tick_no += 1;
|
||||||
|
|
||||||
|
// Caclulate and display performance statistics every 60 seconds.
|
||||||
|
const PERIOD: u32 = 60 * 60;
|
||||||
|
let tick_mod = tick_no % PERIOD;
|
||||||
|
if tick_mod % PERIOD == 0 {
|
||||||
|
let average_fps = PERIOD as f32 / last_averaging_time.elapsed().as_secs_f32();
|
||||||
|
let average_sleep = sleep_time / PERIOD;
|
||||||
|
let average_process = loop_time - average_sleep;
|
||||||
|
|
||||||
|
sleep_time = Duration::ZERO;
|
||||||
|
last_averaging_time = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !main_loop() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
129
src/map.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
//! This module defines the game map and provides functions for interacting with it.
|
||||||
|
use crate::constants::{MapTile, BOARD_OFFSET, CELL_SIZE};
|
||||||
|
use crate::constants::{BOARD_HEIGHT, BOARD_WIDTH};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct Position(pub u32, pub u32);
|
||||||
|
|
||||||
|
impl Position {
|
||||||
|
pub fn as_i32(&self) -> (i32, i32) {
|
||||||
|
(self.0 as i32, self.1 as i32)
|
||||||
|
}
|
||||||
|
pub fn wrapping_add(&self, dx: i32, dy: i32) -> Position {
|
||||||
|
Position((self.0 as i32 + dx) as u32, (self.1 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 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,
|
||||||
|
'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 as usize][y as usize] = tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map {
|
||||||
|
current: map,
|
||||||
|
default: map.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the map to its original state.
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
// Restore the map to its original state
|
||||||
|
for x in 0..BOARD_WIDTH as usize {
|
||||||
|
for y in 0..BOARD_HEIGHT as usize {
|
||||||
|
self.current[x][y] = 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
256
src/pacman.rs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
//! 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 tracing::event;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
animation::AnimatedTexture,
|
||||||
|
constants::MapTile,
|
||||||
|
constants::{BOARD_OFFSET, BOARD_WIDTH, CELL_SIZE},
|
||||||
|
direction::Direction,
|
||||||
|
entity::Entity,
|
||||||
|
map::Map,
|
||||||
|
modulation::{SimpleTickModulator, TickModulator},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The Pac-Man entity.
|
||||||
|
pub struct Pacman<'a> {
|
||||||
|
/// The absolute position of Pac-Man on the board, in pixels.
|
||||||
|
pub pixel_position: (i32, i32),
|
||||||
|
/// The position of Pac-Man on the board, in grid coordinates.
|
||||||
|
/// This is only updated at the moment Pac-Man is aligned with the grid.
|
||||||
|
pub cell_position: (u32, u32),
|
||||||
|
/// The current direction of Pac-Man.
|
||||||
|
pub direction: Direction,
|
||||||
|
/// 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,
|
||||||
|
map: Rc<RefCell<Map>>,
|
||||||
|
speed: u32,
|
||||||
|
modulation: SimpleTickModulator,
|
||||||
|
sprite: AnimatedTexture<'a>,
|
||||||
|
pub in_tunnel: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pacman<'_> {
|
||||||
|
/// Creates a new `Pacman` instance.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `starting_position` - The starting position of Pac-Man, in grid coordinates.
|
||||||
|
/// * `atlas` - The texture atlas containing the Pac-Man sprites.
|
||||||
|
/// * `map` - A reference to the game map.
|
||||||
|
pub fn new<'a>(
|
||||||
|
starting_position: (u32, u32),
|
||||||
|
atlas: Texture<'a>,
|
||||||
|
map: Rc<RefCell<Map>>,
|
||||||
|
) -> Pacman<'a> {
|
||||||
|
Pacman {
|
||||||
|
pixel_position: Map::cell_to_pixel(starting_position),
|
||||||
|
cell_position: starting_position,
|
||||||
|
direction: Direction::Right,
|
||||||
|
next_direction: None,
|
||||||
|
speed: 3,
|
||||||
|
map,
|
||||||
|
stopped: false,
|
||||||
|
modulation: SimpleTickModulator::new(1.0),
|
||||||
|
sprite: AnimatedTexture::new(atlas, 2, 3, 32, 32, Some((-4, -4))),
|
||||||
|
in_tunnel: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders Pac-Man to the canvas.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `canvas` - The SDL canvas to render to.
|
||||||
|
pub fn render(&mut self, canvas: &mut Canvas<Window>) {
|
||||||
|
if self.stopped {
|
||||||
|
self.sprite
|
||||||
|
.render_static(canvas, self.pixel_position, self.direction, Some(2));
|
||||||
|
} else {
|
||||||
|
self.sprite
|
||||||
|
.render(canvas, self.pixel_position, self.direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the next cell in the given direction.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `direction` - The direction to check. If `None`, the current direction is used.
|
||||||
|
pub fn next_cell(&self, direction: Option<Direction>) -> (i32, i32) {
|
||||||
|
let (x, y) = direction.unwrap_or(self.direction).offset();
|
||||||
|
let cell = self.cell_position;
|
||||||
|
(cell.0 as i32 + x, cell.1 as i32 + y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles a requested direction change.
|
||||||
|
///
|
||||||
|
/// The direction change is only applied if the next tile in the requested
|
||||||
|
/// direction is not a wall.
|
||||||
|
fn handle_direction_change(&mut self) -> bool {
|
||||||
|
match self.next_direction {
|
||||||
|
// If there is no next direction, do nothing.
|
||||||
|
None => return false,
|
||||||
|
// If the next direction is the same as the current direction, do nothing.
|
||||||
|
Some(next_direction) => {
|
||||||
|
if next_direction == self.direction {
|
||||||
|
self.next_direction = None;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the next cell in the proposed direction.
|
||||||
|
let proposed_next_cell = self.next_cell(self.next_direction);
|
||||||
|
let proposed_next_tile = self
|
||||||
|
.map
|
||||||
|
.borrow()
|
||||||
|
.get_tile(proposed_next_cell)
|
||||||
|
.unwrap_or(MapTile::Empty);
|
||||||
|
|
||||||
|
// If the next tile is a wall, do nothing.
|
||||||
|
if proposed_next_tile == MapTile::Wall {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the next tile is not a wall, change direction.
|
||||||
|
event!(
|
||||||
|
tracing::Level::DEBUG,
|
||||||
|
"Direction change: {:?} -> {:?} at position ({}, {}) internal ({}, {})",
|
||||||
|
self.direction,
|
||||||
|
self.next_direction.unwrap(),
|
||||||
|
self.pixel_position.0,
|
||||||
|
self.pixel_position.1,
|
||||||
|
self.internal_position().0,
|
||||||
|
self.internal_position().1
|
||||||
|
);
|
||||||
|
self.direction = self.next_direction.unwrap();
|
||||||
|
self.next_direction = None;
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the internal position of Pac-Man, rounded down to the nearest
|
||||||
|
/// even number.
|
||||||
|
///
|
||||||
|
/// This is used to ensure that Pac-Man is aligned with the grid before
|
||||||
|
/// changing direction.
|
||||||
|
fn internal_position_even(&self) -> (u32, u32) {
|
||||||
|
let (x, y) = self.internal_position();
|
||||||
|
((x / 2u32) * 2u32, (y / 2u32) * 2u32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.pixel_position
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_position(&self) -> (u32, u32) {
|
||||||
|
self.cell_position
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internal_position(&self) -> (u32, u32) {
|
||||||
|
let (x, y) = self.position();
|
||||||
|
(x as u32 % CELL_SIZE, y as u32 % CELL_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(&mut self) {
|
||||||
|
// Pac-Man can only change direction when he is perfectly aligned with the grid.
|
||||||
|
let can_change = self.internal_position_even() == (0, 0);
|
||||||
|
|
||||||
|
if can_change {
|
||||||
|
self.cell_position = (
|
||||||
|
(self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0,
|
||||||
|
(self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1,
|
||||||
|
);
|
||||||
|
|
||||||
|
let current_tile = self
|
||||||
|
.map
|
||||||
|
.borrow()
|
||||||
|
.get_tile((self.cell_position.0 as i32, self.cell_position.1 as i32))
|
||||||
|
.unwrap_or(MapTile::Empty);
|
||||||
|
if current_tile == MapTile::Tunnel {
|
||||||
|
self.in_tunnel = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tunnel logic: if in tunnel, force movement and prevent direction change
|
||||||
|
if self.in_tunnel {
|
||||||
|
// If out of bounds, teleport to the opposite side and exit tunnel
|
||||||
|
if self.cell_position.0 == 0 {
|
||||||
|
self.cell_position.0 = BOARD_WIDTH - 2;
|
||||||
|
self.pixel_position =
|
||||||
|
Map::cell_to_pixel((self.cell_position.0 + 1, self.cell_position.1));
|
||||||
|
self.in_tunnel = false;
|
||||||
|
} else if self.cell_position.0 == BOARD_WIDTH - 1 {
|
||||||
|
self.cell_position.0 = 1;
|
||||||
|
self.pixel_position =
|
||||||
|
Map::cell_to_pixel((self.cell_position.0 - 1, self.cell_position.1));
|
||||||
|
self.in_tunnel = false;
|
||||||
|
} else {
|
||||||
|
// While in tunnel, do not allow direction change
|
||||||
|
// and always move in the current direction
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle direction change as normal
|
||||||
|
self.handle_direction_change();
|
||||||
|
|
||||||
|
// Check if the next tile in the current direction is a wall.
|
||||||
|
let next_tile_position = self.next_cell(None);
|
||||||
|
let next_tile = self
|
||||||
|
.map
|
||||||
|
.borrow()
|
||||||
|
.get_tile(next_tile_position)
|
||||||
|
.unwrap_or(MapTile::Empty);
|
||||||
|
|
||||||
|
if !self.stopped && next_tile == MapTile::Wall {
|
||||||
|
self.stopped = true;
|
||||||
|
} else if self.stopped && next_tile != MapTile::Wall {
|
||||||
|
self.stopped = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.stopped {
|
||||||
|
if self.modulation.next() {
|
||||||
|
let speed = self.speed as i32;
|
||||||
|
match self.direction {
|
||||||
|
Direction::Right => {
|
||||||
|
self.pixel_position.0 += speed;
|
||||||
|
}
|
||||||
|
Direction::Left => {
|
||||||
|
self.pixel_position.0 -= speed;
|
||||||
|
}
|
||||||
|
Direction::Up => {
|
||||||
|
self.pixel_position.1 -= speed;
|
||||||
|
}
|
||||||
|
Direction::Down => {
|
||||||
|
self.pixel_position.1 += speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the cell position if Pac-Man is aligned with the grid.
|
||||||
|
if self.internal_position_even() == (0, 0) {
|
||||||
|
self.cell_position = (
|
||||||
|
(self.pixel_position.0 as u32 / CELL_SIZE) - BOARD_OFFSET.0,
|
||||||
|
(self.pixel_position.1 as u32 / CELL_SIZE) - BOARD_OFFSET.1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||