Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12a63374a8 | |||
| d80d7061e7 | |||
| abdefe0af0 | |||
| 4f76de7c9f | |||
| db8cd6220a | |||
| ced4e87d41 | |||
| 09e3d85821 | |||
| c1e421bbbb | |||
| 3a9381a56c | |||
| 90bdfbd2ae | |||
| a230d15ffc | |||
| 60bbd1f5d6 | |||
| 43ce8a4e01 | |||
| 1529a64588 | |||
| be5eec64c9 |
38
.github/workflows/coverage.yaml
vendored
@@ -42,15 +42,39 @@ jobs:
|
||||
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: taiki-e/install-action@nextest
|
||||
- uses: taiki-e/install-action@just
|
||||
|
||||
# Note: We manually link zlib. This should be synchronized with the flags set for Linux in .cargo/config.toml.
|
||||
- name: Generate coverage report
|
||||
run: |
|
||||
cargo llvm-cov --no-fail-fast --lcov --output-path lcov.info nextest
|
||||
just coverage
|
||||
|
||||
- name: Download Coveralls CLI
|
||||
run: |
|
||||
# use GitHub Releases URL instead of coveralls.io because they can't maintain their own files; it 404s
|
||||
curl -L https://github.com/coverallsapp/coverage-reporter/releases/download/v0.6.15/coveralls-linux-x86_64.tar.gz | tar -xz -C /usr/local/bin
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
uses: coverallsapp/github-action@v2
|
||||
with:
|
||||
files: ./lcov.info
|
||||
format: lcov
|
||||
allow-empty: false
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
run: |
|
||||
if [ ! -f "lcov.info" ]; then
|
||||
echo "Error: lcov.info file not found. Coverage generation may have failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in {1..10}; do
|
||||
echo "Attempt $i: Uploading coverage to Coveralls..."
|
||||
if coveralls -n report lcov.info; then
|
||||
echo "Successfully uploaded coverage report."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ $i -lt 10 ]; then
|
||||
delay=$((2**i))
|
||||
echo "Attempt $i failed. Retrying in $delay seconds..."
|
||||
sleep $delay
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Failed to upload coverage report after 10 attempts."
|
||||
exit 1
|
||||
|
||||
15
.gitignore
vendored
@@ -1,8 +1,17 @@
|
||||
# IDE, Other files
|
||||
.vscode
|
||||
.idea
|
||||
rust-sdl2-emscripten/
|
||||
|
||||
# Build files
|
||||
target/
|
||||
dist/
|
||||
emsdk/
|
||||
.idea
|
||||
rust-sdl2-emscripten/
|
||||
assets/site/build.css
|
||||
|
||||
# Site build f iles
|
||||
tailwindcss-*
|
||||
assets/site/build.css
|
||||
|
||||
# Coverage reports
|
||||
lcov.info
|
||||
coverage.html
|
||||
|
||||
68
Cargo.lock
generated
@@ -194,7 +194,8 @@ dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"pathfinding",
|
||||
"rand",
|
||||
"phf",
|
||||
"rand 0.9.2",
|
||||
"sdl2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -223,6 +224,48 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.13"
|
||||
@@ -253,15 +296,30 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
@@ -399,6 +457,12 @@ dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
|
||||
@@ -23,6 +23,7 @@ serde_json = "1.0.142"
|
||||
smallvec = "1.15.1"
|
||||
strum = "0.27.2"
|
||||
strum_macros = "0.27.2"
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
@@ -57,3 +58,8 @@ aarch64-apple-darwin = { triplet = "arm64-osx" }
|
||||
|
||||
[target.'cfg(target_os = "emscripten")'.dependencies]
|
||||
libc = "0.2.175"
|
||||
|
||||
[build-dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
|
||||
33
Justfile
Normal file
@@ -0,0 +1,33 @@
|
||||
set shell := ["bash", "-c"]
|
||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
||||
|
||||
# Regex to exclude files from coverage report, double escapes for Justfile + CLI
|
||||
# You can use src\\\\..., but the filename alone is acceptable too
|
||||
coverage_exclude_pattern := "src\\\\app.rs|audio.rs|src\\\\error.rs|platform\\\\emscripten.rs"
|
||||
|
||||
# !!! --ignore-filename-regex should be used on both reports & coverage testing
|
||||
# !!! --remap-path-prefix prevents the absolute path from being used in the generated report
|
||||
|
||||
# Generate HTML report (for humans, source line inspection)
|
||||
html: coverage
|
||||
cargo llvm-cov report \
|
||||
--remap-path-prefix \
|
||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
||||
--html \
|
||||
--open
|
||||
|
||||
# Display report (for humans)
|
||||
report-coverage: coverage
|
||||
cargo llvm-cov report \
|
||||
--remap-path-prefix \
|
||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}"
|
||||
|
||||
# Run & generate report (for CI)
|
||||
coverage:
|
||||
cargo llvm-cov \
|
||||
--lcov \
|
||||
--remap-path-prefix \
|
||||
--ignore-filename-regex "{{ coverage_exclude_pattern }}" \
|
||||
--output-path lcov.info \
|
||||
--profile coverage \
|
||||
--no-fail-fast nextest
|
||||
@@ -1,6 +1,6 @@
|
||||
# Pac-Man
|
||||
|
||||
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
||||
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
|
||||
|
||||
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
|
||||
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg
|
||||
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
BIN
assets/unpacked/maze/tiles/0.png
Normal file
|
After Width: | Height: | Size: 102 B |
BIN
assets/unpacked/maze/tiles/1.png
Normal file
|
After Width: | Height: | Size: 79 B |
BIN
assets/unpacked/maze/tiles/10.png
Normal file
|
After Width: | Height: | Size: 84 B |
BIN
assets/unpacked/maze/tiles/11.png
Normal file
|
After Width: | Height: | Size: 77 B |
BIN
assets/unpacked/maze/tiles/12.png
Normal file
|
After Width: | Height: | Size: 80 B |
BIN
assets/unpacked/maze/tiles/13.png
Normal file
|
After Width: | Height: | Size: 87 B |
BIN
assets/unpacked/maze/tiles/14.png
Normal file
|
After Width: | Height: | Size: 79 B |
BIN
assets/unpacked/maze/tiles/15.png
Normal file
|
After Width: | Height: | Size: 89 B |
BIN
assets/unpacked/maze/tiles/16.png
Normal file
|
After Width: | Height: | Size: 91 B |
BIN
assets/unpacked/maze/tiles/17.png
Normal file
|
After Width: | Height: | Size: 87 B |
BIN
assets/unpacked/maze/tiles/18.png
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
assets/unpacked/maze/tiles/19.png
Normal file
|
After Width: | Height: | Size: 77 B |
BIN
assets/unpacked/maze/tiles/2.png
Normal file
|
After Width: | Height: | Size: 93 B |
BIN
assets/unpacked/maze/tiles/20.png
Normal file
|
After Width: | Height: | Size: 91 B |
BIN
assets/unpacked/maze/tiles/21.png
Normal file
|
After Width: | Height: | Size: 97 B |
BIN
assets/unpacked/maze/tiles/22.png
Normal file
|
After Width: | Height: | Size: 107 B |
BIN
assets/unpacked/maze/tiles/23.png
Normal file
|
After Width: | Height: | Size: 88 B |
BIN
assets/unpacked/maze/tiles/24.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/25.png
Normal file
|
After Width: | Height: | Size: 80 B |
BIN
assets/unpacked/maze/tiles/26.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/27.png
Normal file
|
After Width: | Height: | Size: 93 B |
BIN
assets/unpacked/maze/tiles/28.png
Normal file
|
After Width: | Height: | Size: 89 B |
BIN
assets/unpacked/maze/tiles/29.png
Normal file
|
After Width: | Height: | Size: 90 B |
BIN
assets/unpacked/maze/tiles/3.png
Normal file
|
After Width: | Height: | Size: 87 B |
BIN
assets/unpacked/maze/tiles/30.png
Normal file
|
After Width: | Height: | Size: 79 B |
BIN
assets/unpacked/maze/tiles/31.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
assets/unpacked/maze/tiles/32.png
Normal file
|
After Width: | Height: | Size: 98 B |
BIN
assets/unpacked/maze/tiles/33.png
Normal file
|
After Width: | Height: | Size: 96 B |
BIN
assets/unpacked/maze/tiles/34.png
Normal file
|
After Width: | Height: | Size: 100 B |
BIN
assets/unpacked/maze/tiles/4.png
Normal file
|
After Width: | Height: | Size: 105 B |
BIN
assets/unpacked/maze/tiles/5.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/6.png
Normal file
|
After Width: | Height: | Size: 71 B |
BIN
assets/unpacked/maze/tiles/7.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/8.png
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
assets/unpacked/maze/tiles/9.png
Normal file
|
After Width: | Height: | Size: 82 B |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
10
bacon.toml
@@ -36,7 +36,7 @@ analyzer = "nextest"
|
||||
|
||||
[jobs.coverage]
|
||||
command = [
|
||||
"cargo", "llvm-cov", "--profile", "coverage", "--color", "always", "--no-fail-fast", "nextest", "--no-capture", "--summary-only", "--"
|
||||
"just", "report-coverage"
|
||||
]
|
||||
need_stdout = true
|
||||
ignored_lines = [
|
||||
@@ -49,8 +49,14 @@ ignored_lines = [
|
||||
"[─]+",
|
||||
"test.+ok",
|
||||
"PASS|START",
|
||||
"Starting \\d+ test"
|
||||
"Starting \\d+ test",
|
||||
"\\s*#",
|
||||
"\\s*Finished.+in \\d+",
|
||||
"\\s*Summary\\s+\\[",
|
||||
"\\s*Blocking",
|
||||
"Finished report saved to"
|
||||
]
|
||||
on_change_strategy = "wait_then_restart"
|
||||
|
||||
[jobs.doc]
|
||||
command = ["cargo", "doc", "--no-deps"]
|
||||
|
||||
50
build.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AtlasMapper {
|
||||
frames: HashMap<String, MapperFrame>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize)]
|
||||
struct MapperFrame {
|
||||
x: u16,
|
||||
y: u16,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("atlas_data.rs");
|
||||
let mut file = BufWriter::new(File::create(&path).unwrap());
|
||||
|
||||
let atlas_json = include_str!("./assets/game/atlas.json");
|
||||
let atlas_mapper: AtlasMapper = serde_json::from_str(atlas_json).unwrap();
|
||||
|
||||
writeln!(&mut file, "use phf::phf_map;").unwrap();
|
||||
|
||||
writeln!(&mut file, "use crate::texture::sprite::MapperFrame;").unwrap();
|
||||
|
||||
writeln!(
|
||||
&mut file,
|
||||
"pub static ATLAS_FRAMES: phf::Map<&'static str, MapperFrame> = phf_map! {{"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for (name, frame) in atlas_mapper.frames {
|
||||
writeln!(
|
||||
&mut file,
|
||||
" \"{}\" => MapperFrame {{ x: {}, y: {}, width: {}, height: {} }},",
|
||||
name, frame.x, frame.y, frame.width, frame.height
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(&mut file, "}};").unwrap();
|
||||
println!("cargo:rerun-if-changed=assets/game/atlas.json");
|
||||
}
|
||||
75
src/app.rs
@@ -2,25 +2,28 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use glam::Vec2;
|
||||
use sdl2::event::{Event, WindowEvent};
|
||||
use sdl2::keyboard::Keycode;
|
||||
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
||||
use sdl2::ttf::Sdl2TtfContext;
|
||||
use sdl2::video::{Window, WindowContext};
|
||||
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
||||
use tracing::{error, event};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::error::{GameError, GameResult};
|
||||
|
||||
use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
|
||||
use crate::game::Game;
|
||||
use crate::input::commands::GameCommand;
|
||||
use crate::input::InputSystem;
|
||||
use crate::platform::get_platform;
|
||||
|
||||
pub struct App {
|
||||
game: Game,
|
||||
input_system: InputSystem,
|
||||
canvas: Canvas<Window>,
|
||||
event_pump: &'static mut EventPump,
|
||||
backbuffer: Texture<'static>,
|
||||
paused: bool,
|
||||
focused: bool,
|
||||
last_tick: Instant,
|
||||
cursor_pos: Vec2,
|
||||
}
|
||||
@@ -51,7 +54,13 @@ impl App {
|
||||
.build()
|
||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||
|
||||
let mut canvas = window.into_canvas().build().map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||
let mut canvas = window
|
||||
.into_canvas()
|
||||
.accelerated()
|
||||
.present_vsync()
|
||||
.build()
|
||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||
|
||||
canvas
|
||||
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
|
||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||
@@ -72,12 +81,14 @@ impl App {
|
||||
game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO)
|
||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||
|
||||
Ok(Self {
|
||||
Ok(App {
|
||||
game,
|
||||
input_system: InputSystem::new(),
|
||||
canvas,
|
||||
event_pump,
|
||||
backbuffer,
|
||||
paused: false,
|
||||
focused: true,
|
||||
last_tick: Instant::now(),
|
||||
cursor_pos: Vec2::ZERO,
|
||||
})
|
||||
@@ -90,46 +101,42 @@ impl App {
|
||||
for event in self.event_pump.poll_iter() {
|
||||
match event {
|
||||
Event::Window { win_event, .. } => match win_event {
|
||||
WindowEvent::Hidden => {
|
||||
event!(tracing::Level::DEBUG, "Window hidden");
|
||||
WindowEvent::FocusGained => {
|
||||
self.focused = true;
|
||||
}
|
||||
WindowEvent::Shown => {
|
||||
event!(tracing::Level::DEBUG, "Window shown");
|
||||
WindowEvent::FocusLost => {
|
||||
debug!("Window focus lost");
|
||||
self.focused = false;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
// It doesn't really make sense to have this available in the browser
|
||||
#[cfg(not(target_os = "emscripten"))]
|
||||
Event::Quit { .. }
|
||||
| Event::KeyDown {
|
||||
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
|
||||
..
|
||||
} => {
|
||||
event!(tracing::Level::INFO, "Exit requested. Exiting...");
|
||||
Event::Quit { .. } => {
|
||||
info!("Exit requested. Exiting...");
|
||||
return false;
|
||||
}
|
||||
Event::KeyDown {
|
||||
keycode: Some(Keycode::P),
|
||||
..
|
||||
} => {
|
||||
self.paused = !self.paused;
|
||||
event!(tracing::Level::INFO, "{}", if self.paused { "Paused" } else { "Unpaused" });
|
||||
}
|
||||
Event::KeyDown {
|
||||
keycode: Some(Keycode::Space),
|
||||
..
|
||||
} => {
|
||||
self.game.toggle_debug_mode();
|
||||
}
|
||||
Event::KeyDown { keycode: Some(key), .. } => {
|
||||
self.game.keyboard_event(key);
|
||||
}
|
||||
Event::MouseMotion { x, y, .. } => {
|
||||
// Convert window coordinates to logical coordinates
|
||||
self.cursor_pos = Vec2::new(x as f32, y as f32);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let commands = self.input_system.handle_event(&event);
|
||||
for command in commands {
|
||||
match command {
|
||||
GameCommand::Exit => {
|
||||
info!("Exit requested. Exiting...");
|
||||
return false;
|
||||
}
|
||||
GameCommand::TogglePause => {
|
||||
self.paused = !self.paused;
|
||||
info!("{}", if self.paused { "Paused" } else { "Unpaused" });
|
||||
}
|
||||
_ => self.game.post_event(command.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dt = self.last_tick.elapsed().as_secs_f32();
|
||||
@@ -151,14 +158,10 @@ impl App {
|
||||
if start.elapsed() < LOOP_TIME {
|
||||
let time = LOOP_TIME.saturating_sub(start.elapsed());
|
||||
if time != Duration::ZERO {
|
||||
get_platform().sleep(time);
|
||||
get_platform().sleep(time, self.focused);
|
||||
}
|
||||
} else {
|
||||
event!(
|
||||
tracing::Level::WARN,
|
||||
"Game loop behind schedule by: {:?}",
|
||||
start.elapsed() - LOOP_TIME
|
||||
);
|
||||
warn!("Game loop behind schedule by: {:?}", start.elapsed() - LOOP_TIME);
|
||||
}
|
||||
|
||||
true
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
//! On desktop, assets are embedded using include_bytes!; on Emscripten, assets are loaded from the filesystem.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
|
||||
pub enum Asset {
|
||||
Wav1,
|
||||
Wav2,
|
||||
Wav3,
|
||||
Wav4,
|
||||
Atlas,
|
||||
AtlasJson,
|
||||
// Add more as needed
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
@@ -25,7 +24,6 @@ impl Asset {
|
||||
Wav3 => "sound/waka/3.ogg",
|
||||
Wav4 => "sound/waka/4.ogg",
|
||||
Atlas => "atlas.png",
|
||||
AtlasJson => "atlas.json",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +33,7 @@ mod imp {
|
||||
use crate::error::AssetError;
|
||||
use crate::platform::get_platform;
|
||||
|
||||
/// Returns the raw bytes of the given asset.
|
||||
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
|
||||
get_platform().get_asset_bytes(asset)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@ pub const SCALE: f32 = 2.6;
|
||||
pub const BOARD_CELL_OFFSET: UVec2 = UVec2::new(0, 3);
|
||||
/// The offset of the game board from the top-left corner of the window, in pixels.
|
||||
pub const BOARD_PIXEL_OFFSET: UVec2 = UVec2::new(BOARD_CELL_OFFSET.x * CELL_SIZE, BOARD_CELL_OFFSET.y * CELL_SIZE);
|
||||
/// The size of the game board, in pixels.
|
||||
pub const BOARD_PIXEL_SIZE: UVec2 = UVec2::new(BOARD_CELL_SIZE.x * CELL_SIZE, BOARD_CELL_SIZE.y * CELL_SIZE);
|
||||
/// The size of the canvas, in pixels.
|
||||
pub const CANVAS_SIZE: UVec2 = UVec2::new(
|
||||
(BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use smallvec::SmallVec;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::entity::traversal::Position;
|
||||
use crate::entity::{graph::NodeId, traversal::Position};
|
||||
|
||||
/// Trait for entities that can participate in collision detection.
|
||||
pub trait Collidable {
|
||||
@@ -19,7 +19,7 @@ pub trait Collidable {
|
||||
#[derive(Default)]
|
||||
pub struct CollisionSystem {
|
||||
/// Maps node IDs to lists of entity IDs that are at that node
|
||||
node_entities: HashMap<usize, Vec<EntityId>>,
|
||||
node_entities: HashMap<NodeId, Vec<EntityId>>,
|
||||
/// Maps entity IDs to their current positions
|
||||
entity_positions: HashMap<EntityId, Position>,
|
||||
/// Next available entity ID
|
||||
@@ -62,7 +62,7 @@ impl CollisionSystem {
|
||||
}
|
||||
|
||||
/// Gets all entity IDs at a specific node
|
||||
pub fn entities_at_node(&self, node: usize) -> &[EntityId] {
|
||||
pub fn entities_at_node(&self, node: NodeId) -> &[EntityId] {
|
||||
self.node_entities.get(&node).map(|v| v.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ fn positions_overlap(a: &Position, b: &Position) -> bool {
|
||||
}
|
||||
|
||||
/// Gets all nodes that an entity is currently at or between.
|
||||
fn get_nodes(pos: &Position) -> SmallVec<[usize; 2]> {
|
||||
fn get_nodes(pos: &Position) -> SmallVec<[NodeId; 2]> {
|
||||
let mut nodes = SmallVec::new();
|
||||
match pos {
|
||||
Position::AtNode(node) => nodes.push(*node),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use glam::IVec2;
|
||||
|
||||
/// The four cardinal directions.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(usize)]
|
||||
pub enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
|
||||
@@ -192,14 +192,15 @@ impl Graph {
|
||||
|
||||
// Check if the edge already exists in this direction or to the same target
|
||||
if let Some(err) = adjacency_list.edges().find_map(|e| {
|
||||
// If we're not replacing the edge, we don't want to replace an edge that already exists in this direction
|
||||
if !replace && e.direction == direction {
|
||||
Some(Err("Edge already exists in this direction."))
|
||||
} else if e.target == to {
|
||||
Some(Err("Edge already exists."))
|
||||
} else {
|
||||
None
|
||||
if !replace {
|
||||
// If we're not replacing the edge, we don't want to replace an edge that already exists in this direction
|
||||
if e.direction == direction {
|
||||
return Some(Err("Edge already exists in this direction."));
|
||||
} else if e.target == to {
|
||||
return Some(Err("Edge already exists."));
|
||||
}
|
||||
}
|
||||
None
|
||||
}) {
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use crate::entity::{
|
||||
use crate::texture::animated::AnimatedTexture;
|
||||
use crate::texture::directional::DirectionalAnimatedTexture;
|
||||
use crate::texture::sprite::SpriteAtlas;
|
||||
use sdl2::keyboard::Keycode;
|
||||
use tracing::error;
|
||||
|
||||
use crate::error::{GameError, GameResult, TextureError};
|
||||
@@ -107,24 +106,6 @@ impl Pacman {
|
||||
texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
|
||||
})
|
||||
}
|
||||
|
||||
/// Handles keyboard input to change Pac-Man's direction.
|
||||
///
|
||||
/// Maps arrow keys to directions and queues the direction change
|
||||
/// for the next valid intersection.
|
||||
pub fn handle_key(&mut self, keycode: Keycode) {
|
||||
let direction = match keycode {
|
||||
Keycode::Up => Some(Direction::Up),
|
||||
Keycode::Down => Some(Direction::Down),
|
||||
Keycode::Left => Some(Direction::Left),
|
||||
Keycode::Right => Some(Direction::Right),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(direction) = direction {
|
||||
self.traverser.set_next_direction(direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Collidable for Pacman {
|
||||
|
||||
12
src/game/events.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use crate::input::commands::GameCommand;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum GameEvent {
|
||||
InputCommand(GameCommand),
|
||||
}
|
||||
|
||||
impl From<GameCommand> for GameEvent {
|
||||
fn from(command: GameCommand) -> Self {
|
||||
GameEvent::InputCommand(command)
|
||||
}
|
||||
}
|
||||
118
src/game/mod.rs
@@ -1,32 +1,34 @@
|
||||
//! This module contains the main game logic and state.
|
||||
|
||||
use glam::{UVec2, Vec2};
|
||||
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
||||
use sdl2::{
|
||||
keyboard::Keycode,
|
||||
pixels::Color,
|
||||
render::{Canvas, RenderTarget, Texture, TextureCreator},
|
||||
video::WindowContext,
|
||||
};
|
||||
use sdl2::pixels::Color;
|
||||
use sdl2::render::{Canvas, Texture, TextureCreator};
|
||||
use sdl2::video::WindowContext;
|
||||
|
||||
use crate::error::{EntityError, GameError, GameResult};
|
||||
use crate::entity::r#trait::Entity;
|
||||
use crate::error::GameResult;
|
||||
|
||||
use crate::entity::{
|
||||
collision::{Collidable, CollisionSystem, EntityId},
|
||||
ghost::{Ghost, GhostType},
|
||||
pacman::Pacman,
|
||||
r#trait::Entity,
|
||||
};
|
||||
|
||||
use crate::map::render::MapRenderer;
|
||||
use crate::{constants, texture::sprite::SpriteAtlas};
|
||||
|
||||
use self::events::GameEvent;
|
||||
use self::state::GameState;
|
||||
|
||||
pub mod events;
|
||||
pub mod state;
|
||||
use state::GameState;
|
||||
|
||||
/// The `Game` struct is the main entry point for the game.
|
||||
///
|
||||
/// It contains the game's state and logic, and is responsible for
|
||||
/// handling user input, updating the game state, and rendering the game.
|
||||
pub struct Game {
|
||||
state: GameState,
|
||||
state: state::GameState,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
@@ -36,16 +38,38 @@ impl Game {
|
||||
Ok(Game { state })
|
||||
}
|
||||
|
||||
pub fn keyboard_event(&mut self, keycode: Keycode) {
|
||||
self.state.pacman.handle_key(keycode);
|
||||
pub fn post_event(&mut self, event: GameEvent) {
|
||||
self.state.event_queue.push_back(event);
|
||||
}
|
||||
|
||||
if keycode == Keycode::M {
|
||||
self.state.audio.set_mute(!self.state.audio.is_muted());
|
||||
fn handle_command(&mut self, command: crate::input::commands::GameCommand) {
|
||||
use crate::input::commands::GameCommand;
|
||||
match command {
|
||||
GameCommand::MovePlayer(direction) => {
|
||||
self.state.pacman.set_next_direction(direction);
|
||||
}
|
||||
GameCommand::ToggleDebug => {
|
||||
self.toggle_debug_mode();
|
||||
}
|
||||
GameCommand::MuteAudio => {
|
||||
let is_muted = self.state.audio.is_muted();
|
||||
self.state.audio.set_mute(!is_muted);
|
||||
}
|
||||
GameCommand::ResetLevel => {
|
||||
if let Err(e) = self.reset_game_state() {
|
||||
tracing::error!("Failed to reset game state: {}", e);
|
||||
}
|
||||
}
|
||||
GameCommand::Exit | GameCommand::TogglePause => {
|
||||
// These are handled in app.rs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if keycode == Keycode::R {
|
||||
if let Err(e) = self.reset_game_state() {
|
||||
tracing::error!("Failed to reset game state: {}", e);
|
||||
fn process_events(&mut self) {
|
||||
while let Some(event) = self.state.event_queue.pop_front() {
|
||||
match event {
|
||||
GameEvent::InputCommand(command) => self.handle_command(command),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +115,7 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, dt: f32) {
|
||||
self.process_events();
|
||||
self.state.pacman.tick(dt, &self.state.map.graph);
|
||||
|
||||
// Update all ghosts
|
||||
@@ -167,14 +192,37 @@ impl Game {
|
||||
self.state.ghost_ids.iter().position(|&id| id == entity_id)
|
||||
}
|
||||
|
||||
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
|
||||
pub fn draw<T: sdl2::render::RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
|
||||
// Only render the map texture once and cache it
|
||||
if !self.state.map_rendered {
|
||||
let mut map_texture = self
|
||||
.state
|
||||
.texture_creator
|
||||
.create_texture_target(None, constants::CANVAS_SIZE.x, constants::CANVAS_SIZE.y)
|
||||
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||
|
||||
canvas
|
||||
.with_texture_canvas(&mut map_texture, |map_canvas| {
|
||||
let mut map_tiles = Vec::with_capacity(35);
|
||||
for i in 0..35 {
|
||||
let tile_name = format!("maze/tiles/{}.png", i);
|
||||
let tile = SpriteAtlas::get_tile(&self.state.atlas, &tile_name).unwrap();
|
||||
map_tiles.push(tile);
|
||||
}
|
||||
MapRenderer::render_map(map_canvas, &mut self.state.atlas, &mut map_tiles);
|
||||
})
|
||||
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||
self.state.map_texture = Some(map_texture);
|
||||
self.state.map_rendered = true;
|
||||
}
|
||||
|
||||
canvas
|
||||
.with_texture_canvas(backbuffer, |canvas| {
|
||||
canvas.set_draw_color(Color::BLACK);
|
||||
canvas.clear();
|
||||
self.state
|
||||
.map
|
||||
.render(canvas, &mut self.state.atlas, &mut self.state.map_texture);
|
||||
if let Some(ref map_texture) = self.state.map_texture {
|
||||
canvas.copy(map_texture, None, None).unwrap();
|
||||
}
|
||||
|
||||
// Render all items
|
||||
for item in &self.state.items {
|
||||
@@ -194,12 +242,12 @@ impl Game {
|
||||
tracing::error!("Failed to render pacman: {}", e);
|
||||
}
|
||||
})
|
||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn present_backbuffer<T: RenderTarget>(
|
||||
pub fn present_backbuffer<T: sdl2::render::RenderTarget>(
|
||||
&mut self,
|
||||
canvas: &mut Canvas<T>,
|
||||
backbuffer: &Texture,
|
||||
@@ -207,7 +255,7 @@ impl Game {
|
||||
) -> GameResult<()> {
|
||||
canvas
|
||||
.copy(backbuffer, None, None)
|
||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||
if self.state.debug_mode {
|
||||
if let Err(e) =
|
||||
self.state
|
||||
@@ -227,7 +275,7 @@ impl Game {
|
||||
///
|
||||
/// Each ghost's path is drawn in its respective color with a small offset
|
||||
/// to prevent overlapping lines.
|
||||
fn render_pathfinding_debug<T: RenderTarget>(&self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
||||
fn render_pathfinding_debug<T: sdl2::render::RenderTarget>(&self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
||||
let pacman_node = self.state.pacman.current_node_id();
|
||||
|
||||
for ghost in self.state.ghosts.iter() {
|
||||
@@ -248,10 +296,10 @@ impl Game {
|
||||
|
||||
// Use the overall direction from start to end to determine the perpendicular offset
|
||||
let offset = match ghost.ghost_type {
|
||||
GhostType::Blinky => Vec2::new(0.25, 0.5),
|
||||
GhostType::Pinky => Vec2::new(-0.25, -0.25),
|
||||
GhostType::Inky => Vec2::new(0.5, -0.5),
|
||||
GhostType::Clyde => Vec2::new(-0.5, 0.25),
|
||||
GhostType::Blinky => glam::Vec2::new(0.25, 0.5),
|
||||
GhostType::Pinky => glam::Vec2::new(-0.25, -0.25),
|
||||
GhostType::Inky => glam::Vec2::new(0.5, -0.5),
|
||||
GhostType::Clyde => glam::Vec2::new(-0.5, 0.25),
|
||||
} * 5.0;
|
||||
|
||||
// Calculate offset positions for all nodes using the same perpendicular direction
|
||||
@@ -262,7 +310,7 @@ impl Game {
|
||||
.map
|
||||
.graph
|
||||
.get_node(node_id)
|
||||
.ok_or(GameError::Entity(EntityError::NodeNotFound(node_id)))?;
|
||||
.ok_or(crate::error::EntityError::NodeNotFound(node_id))?;
|
||||
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||
offset_positions.push(pos + offset);
|
||||
}
|
||||
@@ -278,7 +326,7 @@ impl Game {
|
||||
// Draw the line
|
||||
canvas
|
||||
.draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32))
|
||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||
.map_err(|e| crate::error::GameError::Sdl(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +335,7 @@ impl Game {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_hud<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
||||
fn draw_hud<T: sdl2::render::RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> GameResult<()> {
|
||||
let lives = 3;
|
||||
let score_text = format!("{:02}", self.state.score);
|
||||
let x_offset = 4;
|
||||
@@ -299,7 +347,7 @@ impl Game {
|
||||
canvas,
|
||||
&mut self.state.atlas,
|
||||
&format!("{lives}UP HIGH SCORE "),
|
||||
UVec2::new(8 * lives_offset as u32 + x_offset, y_offset),
|
||||
glam::UVec2::new(8 * lives_offset as u32 + x_offset, y_offset),
|
||||
) {
|
||||
tracing::error!("Failed to render HUD text: {}", e);
|
||||
}
|
||||
@@ -307,7 +355,7 @@ impl Game {
|
||||
canvas,
|
||||
&mut self.state.atlas,
|
||||
&score_text,
|
||||
UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset),
|
||||
glam::UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset),
|
||||
) {
|
||||
tracing::error!("Failed to render score text: {}", e);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
use sdl2::{image::LoadTexture, pixels::Color, render::TextureCreator, video::WindowContext};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use sdl2::{
|
||||
image::LoadTexture,
|
||||
render::{Texture, TextureCreator},
|
||||
video::WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
@@ -12,13 +18,16 @@ use crate::{
|
||||
pacman::Pacman,
|
||||
},
|
||||
error::{GameError, GameResult, TextureError},
|
||||
map::Map,
|
||||
game::events::GameEvent,
|
||||
map::builder::Map,
|
||||
texture::{
|
||||
sprite::{AtlasMapper, AtlasTile, SpriteAtlas},
|
||||
sprite::{AtlasMapper, SpriteAtlas},
|
||||
text::TextTexture,
|
||||
},
|
||||
};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
|
||||
|
||||
/// The `GameState` struct holds all the essential data for the game.
|
||||
///
|
||||
/// This includes the score, map, entities (Pac-Man, ghosts, items),
|
||||
@@ -29,23 +38,28 @@ pub struct GameState {
|
||||
pub score: u32,
|
||||
pub map: Map,
|
||||
pub pacman: Pacman,
|
||||
pub pacman_id: EntityId,
|
||||
pub ghosts: SmallVec<[Ghost; 4]>,
|
||||
pub ghost_ids: SmallVec<[EntityId; 4]>,
|
||||
pub items: Vec<Item>,
|
||||
pub item_ids: Vec<EntityId>,
|
||||
pub debug_mode: bool,
|
||||
pub event_queue: VecDeque<GameEvent>,
|
||||
|
||||
// Collision system
|
||||
pub(crate) collision_system: CollisionSystem,
|
||||
pub(crate) pacman_id: EntityId,
|
||||
pub(crate) ghost_ids: SmallVec<[EntityId; 4]>,
|
||||
pub(crate) item_ids: Vec<EntityId>,
|
||||
|
||||
// Rendering resources
|
||||
pub(crate) atlas: SpriteAtlas,
|
||||
pub(crate) map_texture: AtlasTile,
|
||||
pub(crate) text_texture: TextTexture,
|
||||
|
||||
// Audio
|
||||
pub audio: Audio,
|
||||
|
||||
// Map texture pre-rendering
|
||||
pub(crate) map_texture: Option<Texture<'static>>,
|
||||
pub(crate) map_rendered: bool,
|
||||
pub(crate) texture_creator: &'static TextureCreator<WindowContext>,
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
@@ -57,7 +71,7 @@ impl GameState {
|
||||
pub fn new(texture_creator: &'static TextureCreator<WindowContext>) -> GameResult<Self> {
|
||||
let map = Map::new(RAW_BOARD)?;
|
||||
|
||||
let pacman_start_node = map.start_positions.pacman;
|
||||
let start_node = map.start_positions.pacman;
|
||||
|
||||
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
||||
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
||||
@@ -67,17 +81,15 @@ impl GameState {
|
||||
GameError::Texture(TextureError::LoadFailed(e.to_string()))
|
||||
}
|
||||
})?;
|
||||
let atlas_json = get_asset_bytes(Asset::AtlasJson)?;
|
||||
let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json)?;
|
||||
let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper);
|
||||
|
||||
let mut map_texture = SpriteAtlas::get_tile(&atlas, "maze/full.png")
|
||||
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/full.png".to_string())))?;
|
||||
map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9));
|
||||
let atlas_mapper = AtlasMapper {
|
||||
frames: ATLAS_FRAMES.into_iter().map(|(k, v)| (k.to_string(), *v)).collect(),
|
||||
};
|
||||
let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper);
|
||||
|
||||
let text_texture = TextTexture::new(1.0);
|
||||
let audio = Audio::new();
|
||||
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?;
|
||||
let pacman = Pacman::new(&map.graph, start_node, &atlas)?;
|
||||
|
||||
// Generate items (pellets and energizers)
|
||||
let items = map.generate_items(&atlas)?;
|
||||
@@ -89,11 +101,10 @@ impl GameState {
|
||||
let pacman_id = collision_system.register_entity(pacman.position());
|
||||
|
||||
// Register items
|
||||
let mut item_ids = Vec::new();
|
||||
for item in &items {
|
||||
let item_id = collision_system.register_entity(item.position());
|
||||
item_ids.push(item_id);
|
||||
}
|
||||
let item_ids = items
|
||||
.iter()
|
||||
.map(|item| collision_system.register_entity(item.position()))
|
||||
.collect();
|
||||
|
||||
// Create and register ghosts
|
||||
let ghosts = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde]
|
||||
@@ -110,26 +121,30 @@ impl GameState {
|
||||
.map(|(ghost_type, start_node)| Ghost::new(&map.graph, *start_node, *ghost_type, &atlas))
|
||||
.collect::<GameResult<SmallVec<[_; 4]>>>()?;
|
||||
|
||||
// Register ghosts
|
||||
let ghost_ids = ghosts
|
||||
.iter()
|
||||
.map(|ghost| collision_system.register_entity(ghost.position()))
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
score: 0,
|
||||
map,
|
||||
atlas,
|
||||
pacman,
|
||||
ghosts,
|
||||
items,
|
||||
debug_mode: false,
|
||||
collision_system,
|
||||
pacman_id,
|
||||
ghosts,
|
||||
ghost_ids,
|
||||
items,
|
||||
item_ids,
|
||||
map_texture,
|
||||
text_texture,
|
||||
audio,
|
||||
atlas,
|
||||
score: 0,
|
||||
debug_mode: false,
|
||||
collision_system,
|
||||
map_texture: None,
|
||||
map_rendered: false,
|
||||
texture_creator,
|
||||
event_queue: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||