mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-06 09:15:46 -06:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6af25af5f3 | |||
| f1935ad016 | |||
| 4d397bba5f | |||
| 80930ddd35 | |||
| 0133dd5329 | |||
| 635418a4da | |||
| 31193160a9 | |||
| 3086453c7b | |||
| a8b83b8e2b | |||
| 8ce2af89c8 | |||
| 5f0ee87dd9 | |||
| b88895e82f | |||
| 2f0c734d13 | |||
| e96b3159d7 | |||
| 8c95ecc547 | |||
| 02a98c9f32 | |||
| 7f95c0233e | |||
| a531228b95 | |||
| de86f383bf | |||
| bd811ee783 | |||
| 57d7f75940 |
99
Cargo.lock
generated
99
Cargo.lock
generated
@@ -252,6 +252,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "circular-buffer"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23bdce1da528cadbac4654b5632bfcd8c6c63e25b1d42cea919a95958790b51d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -316,6 +322,12 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "disqualified"
|
name = "disqualified"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -528,6 +540,12 @@ version = "2.7.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "micromap"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18c087666f377f857b49564f8791b481260c67825d6b337e1e38ddf54a985a88"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nonmax"
|
name = "nonmax"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -553,6 +571,12 @@ dependencies = [
|
|||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-width"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faede9396d7883a8c9c989e0b53c984bf770defb5cb8ed6c345b4c0566cf32b9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -571,13 +595,19 @@ version = "0.2.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"circular-buffer",
|
||||||
"glam 0.30.5",
|
"glam 0.30.5",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
|
"micromap",
|
||||||
|
"num-width",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
"pathfinding",
|
"pathfinding",
|
||||||
"phf",
|
"phf",
|
||||||
"rand 0.9.2",
|
"pretty_assertions",
|
||||||
|
"rand",
|
||||||
"sdl2",
|
"sdl2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -586,6 +616,7 @@ dependencies = [
|
|||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"thousands",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -637,29 +668,30 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
version = "0.11.3"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_macros",
|
"phf_macros",
|
||||||
"phf_shared",
|
"phf_shared",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.11.3"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
"phf_shared",
|
"phf_shared",
|
||||||
"rand 0.8.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_macros"
|
name = "phf_macros"
|
||||||
version = "0.11.3"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"phf_generator",
|
"phf_generator",
|
||||||
"phf_shared",
|
"phf_shared",
|
||||||
@@ -670,9 +702,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_shared"
|
name = "phf_shared"
|
||||||
version = "0.11.3"
|
version = "0.12.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"siphasher",
|
"siphasher",
|
||||||
]
|
]
|
||||||
@@ -698,6 +730,16 @@ dependencies = [
|
|||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||||
|
dependencies = [
|
||||||
|
"diff",
|
||||||
|
"yansi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.95"
|
version = "1.0.95"
|
||||||
@@ -722,30 +764,15 @@ version = "5.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
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]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_core 0.9.3",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rand_core"
|
|
||||||
version = "0.6.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
@@ -986,24 +1013,30 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "2.0.12"
|
version = "2.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "2.0.12"
|
version = "2.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thousands"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.7"
|
version = "1.1.7"
|
||||||
@@ -1427,3 +1460,9 @@ checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|||||||
17
Cargo.toml
17
Cargo.toml
@@ -6,7 +6,7 @@ edition = "2021"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing = { version = "0.1.40", features = ["max_level_debug", "release_max_level_debug"]}
|
tracing = { version = "0.1.41", features = ["max_level_debug", "release_max_level_debug"]}
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
|
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
@@ -15,16 +15,23 @@ spin_sleep = "1.3.2"
|
|||||||
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
|
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
|
||||||
pathfinding = "4.14"
|
pathfinding = "4.14"
|
||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0.14"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
glam = { version = "0.30.5", features = [] }
|
glam = "0.30.5"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.142"
|
serde_json = "1.0.142"
|
||||||
smallvec = "1.15.1"
|
smallvec = "1.15.1"
|
||||||
strum = "0.27.2"
|
strum = "0.27.2"
|
||||||
strum_macros = "0.27.2"
|
strum_macros = "0.27.2"
|
||||||
phf = { version = "0.11", features = ["macros"] }
|
phf = { version = "0.12.1", features = ["macros"] }
|
||||||
bevy_ecs = "0.16.1"
|
bevy_ecs = "0.16.1"
|
||||||
|
bitflags = "2.9.1"
|
||||||
|
parking_lot = "0.12.3"
|
||||||
|
micromap = "0.1.0"
|
||||||
|
thousands = "0.2.0"
|
||||||
|
pretty_assertions = "1.4.1"
|
||||||
|
num-width = "0.1.0"
|
||||||
|
circular-buffer = "1.1.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
@@ -63,4 +70,4 @@ libc = "0.2.175"
|
|||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
phf = { version = "0.11", features = ["macros"] }
|
phf = { version = "0.12.1", features = ["macros"] }
|
||||||
|
|||||||
15
src/app.rs
15
src/app.rs
@@ -5,7 +5,6 @@ use sdl2::render::TextureCreator;
|
|||||||
use sdl2::ttf::Sdl2TtfContext;
|
use sdl2::ttf::Sdl2TtfContext;
|
||||||
use sdl2::video::WindowContext;
|
use sdl2::video::WindowContext;
|
||||||
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::error::{GameError, GameResult};
|
use crate::error::{GameError, GameResult};
|
||||||
|
|
||||||
@@ -50,7 +49,6 @@ impl App {
|
|||||||
window
|
window
|
||||||
.into_canvas()
|
.into_canvas()
|
||||||
.accelerated()
|
.accelerated()
|
||||||
.present_vsync()
|
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?,
|
.map_err(|e| GameError::Sdl(e.to_string()))?,
|
||||||
));
|
));
|
||||||
@@ -64,12 +62,6 @@ impl App {
|
|||||||
let game = Game::new(canvas, texture_creator, event_pump)?;
|
let game = Game::new(canvas, texture_creator, event_pump)?;
|
||||||
// game.audio.set_mute(cfg!(debug_assertions));
|
// game.audio.set_mute(cfg!(debug_assertions));
|
||||||
|
|
||||||
// Initial draw
|
|
||||||
// game.draw(&mut canvas, &mut backbuffer)
|
|
||||||
// .map_err(|e| GameError::Sdl(e.to_string()))?;
|
|
||||||
// game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO)
|
|
||||||
// .map_err(|e| GameError::Sdl(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(App {
|
Ok(App {
|
||||||
game,
|
game,
|
||||||
focused: true,
|
focused: true,
|
||||||
@@ -116,17 +108,12 @@ impl App {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
|
// Sleep if we still have time left
|
||||||
// error!("Failed to draw game: {}", e);
|
|
||||||
// }
|
|
||||||
|
|
||||||
if start.elapsed() < LOOP_TIME {
|
if start.elapsed() < LOOP_TIME {
|
||||||
let time = LOOP_TIME.saturating_sub(start.elapsed());
|
let time = LOOP_TIME.saturating_sub(start.elapsed());
|
||||||
if time != Duration::ZERO {
|
if time != Duration::ZERO {
|
||||||
get_platform().sleep(time, self.focused);
|
get_platform().sleep(time, self.focused);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
warn!("Game loop behind schedule by: {:?}", start.elapsed() - LOOP_TIME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
|
||||||
use crate::systems::components::NodeId;
|
use crate::systems::movement::NodeId;
|
||||||
|
|
||||||
use super::direction::Direction;
|
use super::direction::Direction;
|
||||||
|
|
||||||
/// Defines who can traverse a given edge.
|
use bitflags::bitflags;
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub enum EdgePermissions {
|
bitflags! {
|
||||||
/// Anyone can use this edge.
|
/// Defines who can traverse a given edge using flags for fast checking.
|
||||||
#[default]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
All,
|
pub struct TraversalFlags: u8 {
|
||||||
/// Only ghosts can use this edge.
|
const PACMAN = 1 << 0;
|
||||||
GhostsOnly,
|
const GHOST = 1 << 1;
|
||||||
|
|
||||||
|
/// Convenience flag for edges that all entities can use
|
||||||
|
const ALL = Self::PACMAN.bits() | Self::GHOST.bits();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a directed edge from one node to another with a given weight (e.g., distance).
|
/// Represents a directed edge from one node to another with a given weight (e.g., distance).
|
||||||
@@ -24,7 +28,7 @@ pub struct Edge {
|
|||||||
/// The cardinal direction of this edge.
|
/// The cardinal direction of this edge.
|
||||||
pub direction: Direction,
|
pub direction: Direction,
|
||||||
/// Defines who is allowed to traverse this edge.
|
/// Defines who is allowed to traverse this edge.
|
||||||
pub permissions: EdgePermissions,
|
pub traversal_flags: TraversalFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a node in the graph, defined by its position.
|
/// Represents a node in the graph, defined by its position.
|
||||||
@@ -132,8 +136,8 @@ impl Graph {
|
|||||||
return Err("To node does not exist.");
|
return Err("To node does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let edge_a = self.add_edge(from, to, replace, distance, direction, EdgePermissions::default());
|
let edge_a = self.add_edge(from, to, replace, distance, direction, TraversalFlags::ALL);
|
||||||
let edge_b = self.add_edge(to, from, replace, distance, direction.opposite(), EdgePermissions::default());
|
let edge_b = self.add_edge(to, from, replace, distance, direction.opposite(), TraversalFlags::ALL);
|
||||||
|
|
||||||
if edge_a.is_err() && edge_b.is_err() {
|
if edge_a.is_err() && edge_b.is_err() {
|
||||||
return Err("Failed to connect nodes in both directions.");
|
return Err("Failed to connect nodes in both directions.");
|
||||||
@@ -161,7 +165,7 @@ impl Graph {
|
|||||||
replace: bool,
|
replace: bool,
|
||||||
distance: Option<f32>,
|
distance: Option<f32>,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
permissions: EdgePermissions,
|
traversal_flags: TraversalFlags,
|
||||||
) -> Result<(), &'static str> {
|
) -> Result<(), &'static str> {
|
||||||
let edge = Edge {
|
let edge = Edge {
|
||||||
target: to,
|
target: to,
|
||||||
@@ -180,7 +184,7 @@ impl Graph {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
direction,
|
direction,
|
||||||
permissions,
|
traversal_flags,
|
||||||
};
|
};
|
||||||
|
|
||||||
if from >= self.adjacency_list.len() {
|
if from >= self.adjacency_list.len() {
|
||||||
@@ -219,6 +223,19 @@ impl Graph {
|
|||||||
self.nodes.len()
|
self.nodes.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all nodes in the graph.
|
||||||
|
pub fn nodes(&self) -> impl Iterator<Item = &Node> {
|
||||||
|
self.nodes.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an iterator over all edges in the graph.
|
||||||
|
pub fn edges(&self) -> impl Iterator<Item = (NodeId, Edge)> + '_ {
|
||||||
|
self.adjacency_list
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(node_id, intersection)| intersection.edges().map(move |edge| (node_id, edge)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Finds a specific edge from a source node to a target node.
|
/// Finds a specific edge from a source node to a target node.
|
||||||
pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option<Edge> {
|
pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option<Edge> {
|
||||||
self.adjacency_list.get(from)?.edges().find(|edge| edge.target == to)
|
self.adjacency_list.get(from)?.edges().find(|edge| edge.target == to)
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
use bevy_ecs::event::Event;
|
use bevy_ecs::prelude::*;
|
||||||
|
|
||||||
use crate::systems::input::GameCommand;
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum GameCommand {
|
||||||
|
Exit,
|
||||||
|
MovePlayer(crate::entity::direction::Direction),
|
||||||
|
ToggleDebug,
|
||||||
|
MuteAudio,
|
||||||
|
ResetLevel,
|
||||||
|
TogglePause,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Event)]
|
#[derive(Event, Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum GameEvent {
|
pub enum GameEvent {
|
||||||
Command(GameCommand),
|
Command(GameCommand),
|
||||||
|
Collision(Entity, Entity),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<GameCommand> for GameEvent {
|
impl From<GameCommand> for GameEvent {
|
||||||
|
|||||||
173
src/game/mod.rs
173
src/game/mod.rs
@@ -7,28 +7,44 @@ use crate::entity::direction::Direction;
|
|||||||
use crate::error::{GameError, GameResult, TextureError};
|
use crate::error::{GameError, GameResult, TextureError};
|
||||||
use crate::events::GameEvent;
|
use crate::events::GameEvent;
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::components::{
|
use crate::systems::blinking::Blinking;
|
||||||
DeltaTime, DirectionalAnimated, EntityType, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity,
|
use crate::systems::movement::{Movable, MovementState, Position};
|
||||||
|
use crate::systems::{
|
||||||
|
blinking::blinking_system,
|
||||||
|
collision::collision_system,
|
||||||
|
components::{
|
||||||
|
Collider, CollisionLayer, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider,
|
||||||
|
PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, Score, ScoreResource,
|
||||||
|
},
|
||||||
|
control::player_system,
|
||||||
|
debug::{debug_render_system, DebugState, DebugTextureResource},
|
||||||
|
input::input_system,
|
||||||
|
item::item_system,
|
||||||
|
movement::movement_system,
|
||||||
|
profiling::{profile, SystemTimings},
|
||||||
|
render::{directional_render_system, dirty_render_system, render_system, BackbufferResource, MapTextureResource},
|
||||||
};
|
};
|
||||||
use crate::systems::control::player_system;
|
|
||||||
use crate::systems::movement::movement_system;
|
|
||||||
use crate::systems::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource};
|
|
||||||
use crate::texture::animated::AnimatedTexture;
|
use crate::texture::animated::AnimatedTexture;
|
||||||
use bevy_ecs::event::EventRegistry;
|
|
||||||
use bevy_ecs::observer::Trigger;
|
|
||||||
use bevy_ecs::schedule::IntoScheduleConfigs;
|
use bevy_ecs::schedule::IntoScheduleConfigs;
|
||||||
use bevy_ecs::system::ResMut;
|
use bevy_ecs::system::NonSendMut;
|
||||||
use bevy_ecs::{schedule::Schedule, world::World};
|
use bevy_ecs::{
|
||||||
|
event::EventRegistry,
|
||||||
|
observer::Trigger,
|
||||||
|
schedule::Schedule,
|
||||||
|
system::{Res, ResMut},
|
||||||
|
world::World,
|
||||||
|
};
|
||||||
use sdl2::image::LoadTexture;
|
use sdl2::image::LoadTexture;
|
||||||
use sdl2::render::{Canvas, ScaleMode, TextureCreator};
|
use sdl2::render::{Canvas, ScaleMode, TextureCreator};
|
||||||
use sdl2::video::{Window, WindowContext};
|
use sdl2::video::{Window, WindowContext};
|
||||||
use sdl2::EventPump;
|
use sdl2::EventPump;
|
||||||
|
|
||||||
use crate::asset::{get_asset_bytes, Asset};
|
|
||||||
use crate::map::render::MapRenderer;
|
|
||||||
use crate::systems::input::{input_system, Bindings, GameCommand};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
asset::{get_asset_bytes, Asset},
|
||||||
constants,
|
constants,
|
||||||
|
events::GameCommand,
|
||||||
|
map::render::MapRenderer,
|
||||||
|
systems::input::Bindings,
|
||||||
texture::sprite::{AtlasMapper, SpriteAtlas},
|
texture::sprite::{AtlasMapper, SpriteAtlas},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,6 +81,13 @@ impl Game {
|
|||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
map_texture.set_scale_mode(ScaleMode::Nearest);
|
map_texture.set_scale_mode(ScaleMode::Nearest);
|
||||||
|
|
||||||
|
// Create debug texture at output resolution for crisp debug rendering
|
||||||
|
let output_size = canvas.output_size().unwrap();
|
||||||
|
let mut debug_texture = texture_creator
|
||||||
|
.create_texture_target(None, output_size.0, output_size.1)
|
||||||
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
debug_texture.set_scale_mode(ScaleMode::Nearest);
|
||||||
|
|
||||||
// Load atlas and create map texture
|
// Load atlas and create map texture
|
||||||
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
let atlas_bytes = get_asset_bytes(Asset::Atlas)?;
|
||||||
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
let atlas_texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
||||||
@@ -93,7 +116,7 @@ impl Game {
|
|||||||
// Render map to texture
|
// Render map to texture
|
||||||
canvas
|
canvas
|
||||||
.with_texture_canvas(&mut map_texture, |map_canvas| {
|
.with_texture_canvas(&mut map_texture, |map_canvas| {
|
||||||
MapRenderer::render_map(map_canvas, &mut atlas, &mut map_tiles);
|
MapRenderer::render_map(map_canvas, &mut atlas, &map_tiles);
|
||||||
})
|
})
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
@@ -128,22 +151,32 @@ impl Game {
|
|||||||
|
|
||||||
let player = PlayerBundle {
|
let player = PlayerBundle {
|
||||||
player: PlayerControlled,
|
player: PlayerControlled,
|
||||||
position: Position::AtNode(pacman_start_node),
|
position: Position {
|
||||||
velocity: Velocity {
|
node: pacman_start_node,
|
||||||
direction: Direction::Up,
|
edge_progress: None,
|
||||||
next_direction: None,
|
},
|
||||||
speed: 1.125,
|
movement_state: MovementState::Stopped,
|
||||||
|
movable: Movable {
|
||||||
|
speed: 1.15,
|
||||||
|
current_direction: Direction::Left,
|
||||||
|
requested_direction: Some(Direction::Left), // Start moving left immediately
|
||||||
},
|
},
|
||||||
sprite: Renderable {
|
sprite: Renderable {
|
||||||
sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png")
|
sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png")
|
||||||
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?,
|
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?,
|
||||||
layer: 0,
|
layer: 0,
|
||||||
|
visible: true,
|
||||||
},
|
},
|
||||||
directional_animated: DirectionalAnimated {
|
directional_animated: DirectionalAnimated {
|
||||||
textures,
|
textures,
|
||||||
stopped_textures,
|
stopped_textures,
|
||||||
},
|
},
|
||||||
entity_type: EntityType::Player,
|
entity_type: EntityType::Player,
|
||||||
|
collider: Collider {
|
||||||
|
size: constants::CELL_SIZE as f32 * 1.375,
|
||||||
|
layer: CollisionLayer::PACMAN,
|
||||||
|
},
|
||||||
|
pacman_collider: PacmanCollider,
|
||||||
};
|
};
|
||||||
|
|
||||||
world.insert_non_send_resource(atlas);
|
world.insert_non_send_resource(atlas);
|
||||||
@@ -151,35 +184,110 @@ impl Game {
|
|||||||
world.insert_non_send_resource(canvas);
|
world.insert_non_send_resource(canvas);
|
||||||
world.insert_non_send_resource(BackbufferResource(backbuffer));
|
world.insert_non_send_resource(BackbufferResource(backbuffer));
|
||||||
world.insert_non_send_resource(MapTextureResource(map_texture));
|
world.insert_non_send_resource(MapTextureResource(map_texture));
|
||||||
|
world.insert_non_send_resource(DebugTextureResource(debug_texture));
|
||||||
|
|
||||||
world.insert_resource(map);
|
world.insert_resource(map);
|
||||||
world.insert_resource(GlobalState { exit: false });
|
world.insert_resource(GlobalState { exit: false });
|
||||||
|
world.insert_resource(ScoreResource(0));
|
||||||
|
world.insert_resource(SystemTimings::default());
|
||||||
world.insert_resource(Bindings::default());
|
world.insert_resource(Bindings::default());
|
||||||
world.insert_resource(DeltaTime(0f32));
|
world.insert_resource(DeltaTime(0f32));
|
||||||
|
world.insert_resource(RenderDirty::default());
|
||||||
|
world.insert_resource(DebugState::default());
|
||||||
|
|
||||||
world.add_observer(|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>| match *event {
|
world.add_observer(
|
||||||
GameEvent::Command(command) => match command {
|
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| match *event {
|
||||||
GameCommand::Exit => {
|
GameEvent::Command(command) => match command {
|
||||||
state.exit = true;
|
GameCommand::Exit => {
|
||||||
}
|
state.exit = true;
|
||||||
_ => {}
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
GameEvent::Collision(_a, _b) => {}
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
schedule.add_systems(
|
schedule.add_systems(
|
||||||
(
|
(
|
||||||
input_system,
|
profile("input", input_system),
|
||||||
player_system,
|
profile("player", player_system),
|
||||||
movement_system,
|
profile("movement", movement_system),
|
||||||
directional_render_system,
|
profile("collision", collision_system),
|
||||||
render_system,
|
profile("item", item_system),
|
||||||
|
profile("blinking", blinking_system),
|
||||||
|
profile("directional_render", directional_render_system),
|
||||||
|
profile("dirty_render", dirty_render_system),
|
||||||
|
profile("render", render_system),
|
||||||
|
profile("debug_render", debug_render_system),
|
||||||
|
profile(
|
||||||
|
"present",
|
||||||
|
|mut canvas: NonSendMut<&mut Canvas<Window>>,
|
||||||
|
backbuffer: NonSendMut<BackbufferResource>,
|
||||||
|
debug_state: Res<DebugState>,
|
||||||
|
mut dirty: ResMut<RenderDirty>| {
|
||||||
|
if dirty.0 || *debug_state != DebugState::Off {
|
||||||
|
// Only copy backbuffer to main canvas if debug rendering is off
|
||||||
|
// (debug rendering draws directly to main canvas)
|
||||||
|
if *debug_state == DebugState::Off {
|
||||||
|
canvas.copy(&backbuffer.0, None, None).unwrap();
|
||||||
|
}
|
||||||
|
dirty.0 = false;
|
||||||
|
canvas.present();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.chain(),
|
.chain(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Spawn player
|
// Spawn player
|
||||||
world.spawn(player);
|
world.spawn(player);
|
||||||
|
|
||||||
|
// Spawn items
|
||||||
|
let pellet_sprite = SpriteAtlas::get_tile(world.non_send_resource::<SpriteAtlas>(), "maze/pellet.png")
|
||||||
|
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/pellet.png".to_string())))?;
|
||||||
|
let energizer_sprite = SpriteAtlas::get_tile(world.non_send_resource::<SpriteAtlas>(), "maze/energizer.png")
|
||||||
|
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/energizer.png".to_string())))?;
|
||||||
|
|
||||||
|
let nodes: Vec<_> = world.resource::<Map>().iter_nodes().map(|(id, tile)| (*id, *tile)).collect();
|
||||||
|
|
||||||
|
for (node_id, tile) in nodes {
|
||||||
|
let (item_type, score, sprite, size) = match tile {
|
||||||
|
crate::constants::MapTile::Pellet => (EntityType::Pellet, 10, pellet_sprite, constants::CELL_SIZE as f32 * 0.4),
|
||||||
|
crate::constants::MapTile::PowerPellet => (
|
||||||
|
EntityType::PowerPellet,
|
||||||
|
50,
|
||||||
|
energizer_sprite,
|
||||||
|
constants::CELL_SIZE as f32 * 0.95,
|
||||||
|
),
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut item = world.spawn(ItemBundle {
|
||||||
|
position: Position {
|
||||||
|
node: node_id,
|
||||||
|
edge_progress: None,
|
||||||
|
},
|
||||||
|
sprite: Renderable {
|
||||||
|
sprite,
|
||||||
|
layer: 1,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
entity_type: item_type,
|
||||||
|
score: Score(score),
|
||||||
|
collider: Collider {
|
||||||
|
size,
|
||||||
|
layer: CollisionLayer::ITEM,
|
||||||
|
},
|
||||||
|
item_collider: ItemCollider,
|
||||||
|
});
|
||||||
|
|
||||||
|
if item_type == EntityType::PowerPellet {
|
||||||
|
item.insert(Blinking {
|
||||||
|
timer: 0.0,
|
||||||
|
interval: 0.2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Game { world, schedule })
|
Ok(Game { world, schedule })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +321,6 @@ impl Game {
|
|||||||
// match event {
|
// match event {
|
||||||
// GameEvent::Command(command) => self.handle_command(command),
|
// GameEvent::Command(command) => self.handle_command(command),
|
||||||
// }
|
// }
|
||||||
// }
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// /// Resets the game state, randomizing ghost positions and resetting Pac-Man
|
// /// Resets the game state, randomizing ghost positions and resetting Pac-Man
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
//! Map construction and building functionality.
|
//! Map construction and building functionality.
|
||||||
|
|
||||||
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
|
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::direction::Direction;
|
||||||
use crate::entity::graph::{EdgePermissions, Graph, Node};
|
use crate::entity::graph::{Graph, Node, TraversalFlags};
|
||||||
use crate::map::parser::MapTileParser;
|
use crate::map::parser::MapTileParser;
|
||||||
use crate::map::render::MapRenderer;
|
use crate::map::render::MapRenderer;
|
||||||
use crate::systems::components::NodeId;
|
use crate::systems::movement::NodeId;
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
use bevy_ecs::resource::Resource;
|
use bevy_ecs::resource::Resource;
|
||||||
use glam::{IVec2, Vec2};
|
use glam::{IVec2, Vec2};
|
||||||
@@ -33,6 +32,8 @@ pub struct Map {
|
|||||||
pub grid_to_node: HashMap<IVec2, NodeId>,
|
pub grid_to_node: HashMap<IVec2, NodeId>,
|
||||||
/// A mapping of the starting positions of the entities.
|
/// A mapping of the starting positions of the entities.
|
||||||
pub start_positions: NodePositions,
|
pub start_positions: NodePositions,
|
||||||
|
/// The raw tile data for the map.
|
||||||
|
tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Map {
|
impl Map {
|
||||||
@@ -153,46 +154,16 @@ impl Map {
|
|||||||
graph,
|
graph,
|
||||||
grid_to_node,
|
grid_to_node,
|
||||||
start_positions,
|
start_positions,
|
||||||
|
tiles: map,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generates Item entities for pellets and energizers from the parsed map.
|
pub fn iter_nodes(&self) -> impl Iterator<Item = (&NodeId, &MapTile)> {
|
||||||
// pub fn generate_items(&self, atlas: &SpriteAtlas) -> GameResult<Vec<Item>> {
|
self.grid_to_node.iter().map(move |(pos, node_id)| {
|
||||||
// // Pre-load sprites to avoid repeated texture lookups
|
let tile = &self.tiles[pos.x as usize][pos.y as usize];
|
||||||
// let pellet_sprite = SpriteAtlas::get_tile(atlas, "maze/pellet.png")
|
(node_id, tile)
|
||||||
// .ok_or_else(|| MapError::InvalidConfig("Pellet texture not found".to_string()))?;
|
})
|
||||||
// let energizer_sprite = SpriteAtlas::get_tile(atlas, "maze/energizer.png")
|
}
|
||||||
// .ok_or_else(|| MapError::InvalidConfig("Energizer texture not found".to_string()))?;
|
|
||||||
|
|
||||||
// // Pre-allocate with estimated capacity (typical Pac-Man maps have ~240 pellets + 4 energizers)
|
|
||||||
// let mut items = Vec::with_capacity(250);
|
|
||||||
|
|
||||||
// // Parse the raw board once
|
|
||||||
// let parsed_map = MapTileParser::parse_board(RAW_BOARD)?;
|
|
||||||
// let map = parsed_map.tiles;
|
|
||||||
|
|
||||||
// // Iterate through the map and collect items more efficiently
|
|
||||||
// for (x, row) in map.iter().enumerate() {
|
|
||||||
// for (y, tile) in row.iter().enumerate() {
|
|
||||||
// match tile {
|
|
||||||
// MapTile::Pellet | MapTile::PowerPellet => {
|
|
||||||
// let grid_pos = IVec2::new(x as i32, y as i32);
|
|
||||||
// if let Some(&node_id) = self.grid_to_node.get(&grid_pos) {
|
|
||||||
// let (item_type, sprite) = match tile {
|
|
||||||
// MapTile::Pellet => (ItemType::Pellet, Sprite::new(pellet_sprite)),
|
|
||||||
// MapTile::PowerPellet => (ItemType::Energizer, Sprite::new(energizer_sprite)),
|
|
||||||
// _ => unreachable!(), // We already filtered for these types
|
|
||||||
// };
|
|
||||||
// items.push(Item::new(node_id, item_type, sprite));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// _ => {}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(items)
|
|
||||||
// }
|
|
||||||
|
|
||||||
/// Renders a debug visualization with cursor-based highlighting.
|
/// Renders a debug visualization with cursor-based highlighting.
|
||||||
///
|
///
|
||||||
@@ -294,7 +265,7 @@ impl Map {
|
|||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
Direction::Down,
|
Direction::Down,
|
||||||
EdgePermissions::GhostsOnly,
|
TraversalFlags::GHOST,
|
||||||
)
|
)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {e}")))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {e}")))?;
|
||||||
|
|
||||||
@@ -305,7 +276,7 @@ impl Map {
|
|||||||
false,
|
false,
|
||||||
None,
|
None,
|
||||||
Direction::Up,
|
Direction::Up,
|
||||||
EdgePermissions::GhostsOnly,
|
TraversalFlags::GHOST,
|
||||||
)
|
)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {e}")))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {e}")))?;
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ impl MapRenderer {
|
|||||||
///
|
///
|
||||||
/// This function draws the static map texture to the screen at the correct
|
/// This function draws the static map texture to the screen at the correct
|
||||||
/// position and scale.
|
/// position and scale.
|
||||||
pub fn render_map<T: RenderTarget>(canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, map_tiles: &mut [AtlasTile]) {
|
pub fn render_map<T: RenderTarget>(canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, map_tiles: &[AtlasTile]) {
|
||||||
for (y, row) in TILE_MAP.iter().enumerate() {
|
for (y, row) in TILE_MAP.iter().enumerate() {
|
||||||
for (x, &tile_index) in row.iter().enumerate() {
|
for (x, &tile_index) in row.iter().enumerate() {
|
||||||
let mut tile = map_tiles[tile_index];
|
let mut tile = map_tiles[tile_index];
|
||||||
|
|||||||
27
src/systems/blinking.rs
Normal file
27
src/systems/blinking.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use bevy_ecs::{
|
||||||
|
component::Component,
|
||||||
|
system::{Query, Res},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::systems::components::{DeltaTime, Renderable};
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Blinking {
|
||||||
|
pub timer: f32,
|
||||||
|
pub interval: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates blinking entities by toggling their visibility at regular intervals.
|
||||||
|
///
|
||||||
|
/// This system manages entities that have both `Blinking` and `Renderable` components,
|
||||||
|
/// accumulating time and toggling visibility when the specified interval is reached.
|
||||||
|
pub fn blinking_system(time: Res<DeltaTime>, mut query: Query<(&mut Blinking, &mut Renderable)>) {
|
||||||
|
for (mut blinking, mut renderable) in query.iter_mut() {
|
||||||
|
blinking.timer += time.0;
|
||||||
|
|
||||||
|
if blinking.timer >= blinking.interval {
|
||||||
|
blinking.timer = 0.0;
|
||||||
|
renderable.visible = !renderable.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/systems/collision.rs
Normal file
48
src/systems/collision.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use bevy_ecs::entity::Entity;
|
||||||
|
use bevy_ecs::event::EventWriter;
|
||||||
|
use bevy_ecs::query::With;
|
||||||
|
use bevy_ecs::system::{Query, Res};
|
||||||
|
|
||||||
|
use crate::error::GameError;
|
||||||
|
use crate::events::GameEvent;
|
||||||
|
use crate::map::builder::Map;
|
||||||
|
use crate::systems::components::{Collider, ItemCollider, PacmanCollider};
|
||||||
|
use crate::systems::movement::Position;
|
||||||
|
|
||||||
|
pub fn collision_system(
|
||||||
|
map: Res<Map>,
|
||||||
|
pacman_query: Query<(Entity, &Position, &Collider), With<PacmanCollider>>,
|
||||||
|
item_query: Query<(Entity, &Position, &Collider), With<ItemCollider>>,
|
||||||
|
mut events: EventWriter<GameEvent>,
|
||||||
|
mut errors: EventWriter<GameError>,
|
||||||
|
) {
|
||||||
|
// Check PACMAN × ITEM collisions
|
||||||
|
for (pacman_entity, pacman_pos, pacman_collider) in pacman_query.iter() {
|
||||||
|
for (item_entity, item_pos, item_collider) in item_query.iter() {
|
||||||
|
match (pacman_pos.get_pixel_pos(&map.graph), item_pos.get_pixel_pos(&map.graph)) {
|
||||||
|
(Ok(pacman_pixel), Ok(item_pixel)) => {
|
||||||
|
// Calculate the distance between the two entities's precise pixel positions
|
||||||
|
let distance = pacman_pixel.distance(item_pixel);
|
||||||
|
// Calculate the distance at which the two entities will collide
|
||||||
|
let collision_distance = (pacman_collider.size + item_collider.size) / 2.0;
|
||||||
|
|
||||||
|
// If the distance between the two entities is less than the collision distance, then the two entities are colliding
|
||||||
|
if distance < collision_distance {
|
||||||
|
events.write(GameEvent::Collision(pacman_entity, item_entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Either or both of the pixel positions failed to get, so we need to report the error
|
||||||
|
(result_a, result_b) => {
|
||||||
|
for result in [result_a, result_b] {
|
||||||
|
if let Err(e) = result {
|
||||||
|
errors.write(GameError::InvalidState(format!(
|
||||||
|
"Collision system failed to get pixel positions for entities {:?} and {:?}: {}",
|
||||||
|
pacman_entity, item_entity, e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
use bevy_ecs::{bundle::Bundle, component::Component, resource::Resource};
|
use bevy_ecs::{bundle::Bundle, component::Component, resource::Resource};
|
||||||
use glam::Vec2;
|
use bitflags::bitflags;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
entity::{direction::Direction, graph::Graph},
|
entity::graph::TraversalFlags,
|
||||||
error::{EntityError, GameResult},
|
systems::movement::{Movable, MovementState, Position},
|
||||||
texture::{animated::AnimatedTexture, sprite::AtlasTile},
|
texture::{animated::AnimatedTexture, sprite::AtlasTile},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,7 +18,17 @@ pub enum EntityType {
|
|||||||
Ghost,
|
Ghost,
|
||||||
Pellet,
|
Pellet,
|
||||||
PowerPellet,
|
PowerPellet,
|
||||||
Wall,
|
}
|
||||||
|
|
||||||
|
impl EntityType {
|
||||||
|
/// Returns the traversal flags for this entity type.
|
||||||
|
pub fn traversal_flags(&self) -> TraversalFlags {
|
||||||
|
match self {
|
||||||
|
EntityType::Player => TraversalFlags::PACMAN,
|
||||||
|
EntityType::Ghost => TraversalFlags::GHOST,
|
||||||
|
_ => TraversalFlags::empty(), // Static entities don't traverse
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A component for entities that have a sprite, with a layer for ordering.
|
/// A component for entities that have a sprite, with a layer for ordering.
|
||||||
@@ -28,6 +38,7 @@ pub enum EntityType {
|
|||||||
pub struct Renderable {
|
pub struct Renderable {
|
||||||
pub sprite: AtlasTile,
|
pub sprite: AtlasTile,
|
||||||
pub layer: u8,
|
pub layer: u8,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A component for entities that have a directional animated texture.
|
/// A component for entities that have a directional animated texture.
|
||||||
@@ -37,107 +48,55 @@ pub struct DirectionalAnimated {
|
|||||||
pub stopped_textures: [Option<AnimatedTexture>; 4],
|
pub stopped_textures: [Option<AnimatedTexture>; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A unique identifier for a node, represented by its index in the graph's storage.
|
bitflags! {
|
||||||
pub type NodeId = usize;
|
#[derive(Component, Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct CollisionLayer: u8 {
|
||||||
/// Represents the current position of an entity traversing the graph.
|
const PACMAN = 1 << 0;
|
||||||
///
|
const GHOST = 1 << 1;
|
||||||
/// This enum allows for precise tracking of whether an entity is exactly at a node
|
const ITEM = 1 << 2;
|
||||||
/// or moving along an edge between two nodes.
|
|
||||||
#[derive(Component, Debug, Copy, Clone, PartialEq)]
|
|
||||||
pub enum Position {
|
|
||||||
/// The traverser is located exactly at a node.
|
|
||||||
AtNode(NodeId),
|
|
||||||
/// The traverser is on an edge between two nodes.
|
|
||||||
BetweenNodes {
|
|
||||||
from: NodeId,
|
|
||||||
to: NodeId,
|
|
||||||
/// The floating-point distance traversed along the edge from the `from` node.
|
|
||||||
traversed: f32,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Position {
|
|
||||||
/// Calculates the current pixel position in the game world.
|
|
||||||
///
|
|
||||||
/// Converts the graph position to screen coordinates, accounting for
|
|
||||||
/// the board offset and centering the sprite.
|
|
||||||
pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult<Vec2> {
|
|
||||||
let pos = match self {
|
|
||||||
Position::AtNode(node_id) => {
|
|
||||||
let node = graph.get_node(*node_id).ok_or(EntityError::NodeNotFound(*node_id))?;
|
|
||||||
node.position
|
|
||||||
}
|
|
||||||
Position::BetweenNodes { from, to, traversed } => {
|
|
||||||
let from_node = graph.get_node(*from).ok_or(EntityError::NodeNotFound(*from))?;
|
|
||||||
let to_node = graph.get_node(*to).ok_or(EntityError::NodeNotFound(*to))?;
|
|
||||||
let edge = graph
|
|
||||||
.find_edge(*from, *to)
|
|
||||||
.ok_or(EntityError::EdgeNotFound { from: *from, to: *to })?;
|
|
||||||
from_node.position + (to_node.position - from_node.position) * (traversed / edge.distance)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Vec2::new(
|
|
||||||
pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32,
|
|
||||||
pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Position {
|
#[derive(Component)]
|
||||||
fn default() -> Self {
|
pub struct Collider {
|
||||||
Position::AtNode(0)
|
pub size: f32,
|
||||||
}
|
pub layer: CollisionLayer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
/// Marker components for collision filtering optimization
|
||||||
impl Position {
|
#[derive(Component)]
|
||||||
/// Returns `true` if the position is exactly at a node.
|
pub struct PacmanCollider;
|
||||||
pub fn is_at_node(&self) -> bool {
|
|
||||||
matches!(self, Position::AtNode(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the `NodeId` of the current or most recently departed node.
|
#[derive(Component)]
|
||||||
#[allow(clippy::wrong_self_convention)]
|
pub struct GhostCollider;
|
||||||
pub fn from_node_id(&self) -> NodeId {
|
|
||||||
match self {
|
|
||||||
Position::AtNode(id) => *id,
|
|
||||||
Position::BetweenNodes { from, .. } => *from,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the `NodeId` of the destination node, if currently on an edge.
|
#[derive(Component)]
|
||||||
#[allow(clippy::wrong_self_convention)]
|
pub struct ItemCollider;
|
||||||
pub fn to_node_id(&self) -> Option<NodeId> {
|
|
||||||
match self {
|
|
||||||
Position::AtNode(_) => None,
|
|
||||||
Position::BetweenNodes { to, .. } => Some(*to),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true` if the traverser is stopped at a node.
|
#[derive(Component)]
|
||||||
pub fn is_stopped(&self) -> bool {
|
pub struct Score(pub u32);
|
||||||
matches!(self, Position::AtNode(_))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A component for entities that have a velocity, with a direction and speed.
|
|
||||||
#[derive(Default, Component)]
|
|
||||||
pub struct Velocity {
|
|
||||||
pub direction: Direction,
|
|
||||||
pub next_direction: Option<(Direction, u8)>,
|
|
||||||
pub speed: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Bundle)]
|
#[derive(Bundle)]
|
||||||
pub struct PlayerBundle {
|
pub struct PlayerBundle {
|
||||||
pub player: PlayerControlled,
|
pub player: PlayerControlled,
|
||||||
pub position: Position,
|
pub position: Position,
|
||||||
pub velocity: Velocity,
|
pub movement_state: MovementState,
|
||||||
|
pub movable: Movable,
|
||||||
pub sprite: Renderable,
|
pub sprite: Renderable,
|
||||||
pub directional_animated: DirectionalAnimated,
|
pub directional_animated: DirectionalAnimated,
|
||||||
pub entity_type: EntityType,
|
pub entity_type: EntityType,
|
||||||
|
pub collider: Collider,
|
||||||
|
pub pacman_collider: PacmanCollider,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Bundle)]
|
||||||
|
pub struct ItemBundle {
|
||||||
|
pub position: Position,
|
||||||
|
pub sprite: Renderable,
|
||||||
|
pub entity_type: EntityType,
|
||||||
|
pub score: Score,
|
||||||
|
pub collider: Collider,
|
||||||
|
pub item_collider: ItemCollider,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
@@ -145,5 +104,11 @@ pub struct GlobalState {
|
|||||||
pub exit: bool,
|
pub exit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct ScoreResource(pub u32);
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct DeltaTime(pub f32);
|
pub struct DeltaTime(pub f32);
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct RenderDirty(pub bool);
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
event::{EventReader, EventWriter},
|
event::{EventReader, EventWriter},
|
||||||
|
prelude::ResMut,
|
||||||
query::With,
|
query::With,
|
||||||
system::{Query, ResMut},
|
system::Query,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
events::GameEvent,
|
events::{GameCommand, GameEvent},
|
||||||
systems::{
|
systems::components::{GlobalState, PlayerControlled},
|
||||||
components::{GlobalState, PlayerControlled, Velocity},
|
systems::debug::DebugState,
|
||||||
input::GameCommand,
|
systems::movement::Movable,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles
|
// Handles player input and control
|
||||||
pub fn player_system(
|
pub fn player_system(
|
||||||
mut events: EventReader<GameEvent>,
|
mut events: EventReader<GameEvent>,
|
||||||
mut state: ResMut<GlobalState>,
|
mut state: ResMut<GlobalState>,
|
||||||
mut players: Query<&mut Velocity, With<PlayerControlled>>,
|
mut debug_state: ResMut<DebugState>,
|
||||||
|
mut players: Query<&mut Movable, With<PlayerControlled>>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
// Get the player's velocity (handling to ensure there is only one player)
|
// Get the player's movable component (ensuring there is only one player)
|
||||||
let mut velocity = match players.single_mut() {
|
let mut movable = match players.single_mut() {
|
||||||
Ok(velocity) => velocity,
|
Ok(movable) => movable,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
errors.write(GameError::InvalidState(format!("No/multiple entities queried for player system: {}", e)).into());
|
errors.write(GameError::InvalidState(format!("No/multiple entities queried for player system: {}", e)).into());
|
||||||
return;
|
return;
|
||||||
@@ -31,16 +32,19 @@ pub fn player_system(
|
|||||||
|
|
||||||
// Handle events
|
// Handle events
|
||||||
for event in events.read() {
|
for event in events.read() {
|
||||||
match event {
|
if let GameEvent::Command(command) = event {
|
||||||
GameEvent::Command(command) => match command {
|
match command {
|
||||||
GameCommand::MovePlayer(direction) => {
|
GameCommand::MovePlayer(direction) => {
|
||||||
velocity.next_direction = Some((*direction, 90));
|
movable.requested_direction = Some(*direction);
|
||||||
}
|
}
|
||||||
GameCommand::Exit => {
|
GameCommand::Exit => {
|
||||||
state.exit = true;
|
state.exit = true;
|
||||||
}
|
}
|
||||||
|
GameCommand::ToggleDebug => {
|
||||||
|
*debug_state = debug_state.next();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
217
src/systems/debug.rs
Normal file
217
src/systems/debug.rs
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
//! Debug rendering system
|
||||||
|
use crate::constants::BOARD_PIXEL_OFFSET;
|
||||||
|
use crate::map::builder::Map;
|
||||||
|
use crate::systems::components::Collider;
|
||||||
|
use crate::systems::movement::Position;
|
||||||
|
use crate::systems::profiling::SystemTimings;
|
||||||
|
use crate::systems::render::BackbufferResource;
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use sdl2::pixels::Color;
|
||||||
|
use sdl2::rect::Rect;
|
||||||
|
use sdl2::render::{Canvas, Texture, TextureCreator};
|
||||||
|
use sdl2::video::{Window, WindowContext};
|
||||||
|
|
||||||
|
#[derive(Resource, Default, Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub enum DebugState {
|
||||||
|
#[default]
|
||||||
|
Off,
|
||||||
|
Graph,
|
||||||
|
Collision,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugState {
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
DebugState::Off => DebugState::Graph,
|
||||||
|
DebugState::Graph => DebugState::Collision,
|
||||||
|
DebugState::Collision => DebugState::Off,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resource to hold the debug texture for persistent rendering
|
||||||
|
pub struct DebugTextureResource(pub Texture<'static>);
|
||||||
|
|
||||||
|
/// Transforms a position from logical canvas coordinates to output canvas coordinates
|
||||||
|
fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
|
||||||
|
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
||||||
|
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
||||||
|
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
|
||||||
|
|
||||||
|
let x = (pos.0 * scale) as i32;
|
||||||
|
let y = (pos.1 * scale) as i32;
|
||||||
|
(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transforms a position from logical canvas coordinates to output canvas coordinates (with board offset)
|
||||||
|
fn transform_position_with_offset(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
|
||||||
|
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
||||||
|
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
||||||
|
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
|
||||||
|
|
||||||
|
let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale) as i32;
|
||||||
|
let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale) as i32;
|
||||||
|
(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transforms a size from logical canvas coordinates to output canvas coordinates
|
||||||
|
fn transform_size(size: f32, output_size: (u32, u32), logical_size: (u32, u32)) -> u32 {
|
||||||
|
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
||||||
|
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
||||||
|
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
|
||||||
|
|
||||||
|
(size * scale) as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders timing information in the top-left corner of the screen
|
||||||
|
fn render_timing_display(
|
||||||
|
canvas: &mut Canvas<Window>,
|
||||||
|
texture_creator: &mut TextureCreator<WindowContext>,
|
||||||
|
timings: &SystemTimings,
|
||||||
|
) {
|
||||||
|
// Get TTF context
|
||||||
|
let ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
|
||||||
|
// Load font
|
||||||
|
let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap();
|
||||||
|
|
||||||
|
// Format timing information using the formatting module
|
||||||
|
let timing_text = timings.format_timing_display();
|
||||||
|
|
||||||
|
// Split text by newlines and render each line separately
|
||||||
|
let lines: Vec<&str> = timing_text.lines().collect();
|
||||||
|
if lines.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let line_height = 14; // Approximate line height for 12pt font
|
||||||
|
let padding = 10;
|
||||||
|
|
||||||
|
// Calculate background dimensions
|
||||||
|
let max_width = lines
|
||||||
|
.iter()
|
||||||
|
.filter(|&&l| !l.is_empty()) // Don't consider empty lines for width
|
||||||
|
.map(|line| font.size_of(line).unwrap().0)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
// Only draw background if there is text to display
|
||||||
|
if max_width > 0 {
|
||||||
|
let total_height = (lines.len() as u32) * line_height as u32;
|
||||||
|
let bg_padding = 5;
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
let bg_rect = Rect::new(
|
||||||
|
padding - bg_padding,
|
||||||
|
padding - bg_padding,
|
||||||
|
max_width + (bg_padding * 2) as u32,
|
||||||
|
total_height + bg_padding as u32,
|
||||||
|
);
|
||||||
|
canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
|
||||||
|
canvas.set_draw_color(Color::RGBA(40, 40, 40, 180));
|
||||||
|
canvas.fill_rect(bg_rect).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render each line
|
||||||
|
let surface = font.render(line).blended(Color::RGBA(255, 255, 255, 200)).unwrap();
|
||||||
|
let texture = texture_creator.create_texture_from_surface(&surface).unwrap();
|
||||||
|
|
||||||
|
// Position each line below the previous one
|
||||||
|
let y_pos = padding + (i * line_height) as i32;
|
||||||
|
let dest = Rect::new(padding, y_pos, texture.query().width, texture.query().height);
|
||||||
|
canvas.copy(&texture, None, dest).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_render_system(
|
||||||
|
mut canvas: NonSendMut<&mut Canvas<Window>>,
|
||||||
|
backbuffer: NonSendMut<BackbufferResource>,
|
||||||
|
mut debug_texture: NonSendMut<DebugTextureResource>,
|
||||||
|
debug_state: Res<DebugState>,
|
||||||
|
timings: Res<SystemTimings>,
|
||||||
|
map: Res<Map>,
|
||||||
|
colliders: Query<(&Collider, &Position)>,
|
||||||
|
) {
|
||||||
|
if *debug_state == DebugState::Off {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get canvas sizes for coordinate transformation
|
||||||
|
let output_size = canvas.output_size().unwrap();
|
||||||
|
let logical_size = canvas.logical_size();
|
||||||
|
|
||||||
|
// Copy the current backbuffer to the debug texture
|
||||||
|
canvas
|
||||||
|
.with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
|
||||||
|
// Clear the debug canvas
|
||||||
|
debug_canvas.set_draw_color(Color::BLACK);
|
||||||
|
debug_canvas.clear();
|
||||||
|
|
||||||
|
// Copy the backbuffer to the debug canvas
|
||||||
|
debug_canvas.copy(&backbuffer.0, None, None).unwrap();
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get texture creator before entering the closure to avoid borrowing conflicts
|
||||||
|
let mut texture_creator = canvas.texture_creator();
|
||||||
|
|
||||||
|
// Draw debug info on the high-resolution debug texture
|
||||||
|
canvas
|
||||||
|
.with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
|
||||||
|
match *debug_state {
|
||||||
|
DebugState::Graph => {
|
||||||
|
debug_canvas.set_draw_color(Color::RED);
|
||||||
|
for (start_node, end_node) in map.graph.edges() {
|
||||||
|
let start_node = map.graph.get_node(start_node).unwrap().position;
|
||||||
|
let end_node = map.graph.get_node(end_node.target).unwrap().position;
|
||||||
|
|
||||||
|
// Transform positions using common method
|
||||||
|
let (start_x, start_y) =
|
||||||
|
transform_position_with_offset((start_node.x, start_node.y), output_size, logical_size);
|
||||||
|
let (end_x, end_y) = transform_position_with_offset((end_node.x, end_node.y), output_size, logical_size);
|
||||||
|
|
||||||
|
debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_canvas.set_draw_color(Color::BLUE);
|
||||||
|
for node in map.graph.nodes() {
|
||||||
|
let pos = node.position;
|
||||||
|
|
||||||
|
// Transform position using common method
|
||||||
|
let (x, y) = transform_position_with_offset((pos.x, pos.y), output_size, logical_size);
|
||||||
|
let size = transform_size(4.0, output_size, logical_size);
|
||||||
|
|
||||||
|
debug_canvas
|
||||||
|
.fill_rect(Rect::new(x - (size as i32 / 2), y - (size as i32 / 2), size, size))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DebugState::Collision => {
|
||||||
|
debug_canvas.set_draw_color(Color::GREEN);
|
||||||
|
for (collider, position) in colliders.iter() {
|
||||||
|
let pos = position.get_pixel_pos(&map.graph).unwrap();
|
||||||
|
|
||||||
|
// Transform position and size using common methods
|
||||||
|
let (x, y) = transform_position((pos.x, pos.y), output_size, logical_size);
|
||||||
|
let size = transform_size(collider.size, output_size, logical_size);
|
||||||
|
|
||||||
|
// Center the collision box on the entity
|
||||||
|
let rect = Rect::new(x - (size as i32 / 2), y - (size as i32 / 2), size, size);
|
||||||
|
debug_canvas.draw_rect(rect).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render timing information in the top-left corner
|
||||||
|
render_timing_display(debug_canvas, &mut texture_creator, &timings);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Draw the debug texture directly onto the main canvas at full resolution
|
||||||
|
canvas.copy(&debug_texture.0, None, None).unwrap();
|
||||||
|
}
|
||||||
102
src/systems/formatting.rs
Normal file
102
src/systems/formatting.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use num_width::NumberWidth;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{iter, time::Duration};
|
||||||
|
|
||||||
|
// Helper to split a duration into a integer, decimal, and unit
|
||||||
|
fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
|
||||||
|
let (int, decimal, unit) = match duration {
|
||||||
|
// if greater than 1 second, return as seconds
|
||||||
|
n if n >= &Duration::from_secs(1) => {
|
||||||
|
let secs = n.as_secs();
|
||||||
|
let decimal = n.as_millis() as u64 % 1000;
|
||||||
|
(secs, decimal as u32, "s")
|
||||||
|
}
|
||||||
|
// if greater than 1 millisecond, return as milliseconds
|
||||||
|
n if n >= &Duration::from_millis(1) => {
|
||||||
|
let ms = n.as_millis() as u64;
|
||||||
|
let decimal = n.as_micros() as u64 % 1000;
|
||||||
|
(ms, decimal as u32, "ms")
|
||||||
|
}
|
||||||
|
// if greater than 1 microsecond, return as microseconds
|
||||||
|
n if n >= &Duration::from_micros(1) => {
|
||||||
|
let us = n.as_micros() as u64;
|
||||||
|
let decimal = n.as_nanos() as u64 % 1000;
|
||||||
|
(us, decimal as u32, "µs")
|
||||||
|
}
|
||||||
|
// otherwise, return as nanoseconds
|
||||||
|
n => {
|
||||||
|
let ns = n.as_nanos() as u64;
|
||||||
|
(ns, 0, "ns")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(int, decimal, unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats timing data into a vector of strings with proper alignment
|
||||||
|
pub fn format_timing_display(timing_data: impl IntoIterator<Item = (String, Duration, Duration)>) -> SmallVec<[String; 12]> {
|
||||||
|
let mut iter = timing_data.into_iter().peekable();
|
||||||
|
if iter.peek().is_none() {
|
||||||
|
return SmallVec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
name: String,
|
||||||
|
avg_int: u64,
|
||||||
|
avg_decimal: u32,
|
||||||
|
avg_unit: &'static str,
|
||||||
|
std_int: u64,
|
||||||
|
std_decimal: u32,
|
||||||
|
std_unit: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = iter
|
||||||
|
.map(|(name, avg, std_dev)| {
|
||||||
|
let (avg_int, avg_decimal, avg_unit) = get_value(&avg);
|
||||||
|
let (std_int, std_decimal, std_unit) = get_value(&std_dev);
|
||||||
|
|
||||||
|
Entry {
|
||||||
|
name: name.clone(),
|
||||||
|
avg_int,
|
||||||
|
avg_decimal,
|
||||||
|
avg_unit,
|
||||||
|
std_int,
|
||||||
|
std_decimal,
|
||||||
|
std_unit,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<SmallVec<[Entry; 12]>>();
|
||||||
|
|
||||||
|
let (max_name_width, max_avg_int_width, max_avg_decimal_width, max_std_int_width, max_std_decimal_width) = entries
|
||||||
|
.iter()
|
||||||
|
.fold((0, 0, 3, 0, 3), |(name_w, avg_int_w, avg_dec_w, std_int_w, std_dec_w), e| {
|
||||||
|
(
|
||||||
|
name_w.max(e.name.len()),
|
||||||
|
avg_int_w.max(e.avg_int.width() as usize),
|
||||||
|
avg_dec_w.max(e.avg_decimal.width() as usize),
|
||||||
|
std_int_w.max(e.std_int.width() as usize),
|
||||||
|
std_dec_w.max(e.std_decimal.width() as usize),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
entries.iter().map(|e| {
|
||||||
|
format!(
|
||||||
|
"{name:max_name_width$} : {avg_int:max_avg_int_width$}.{avg_decimal:<max_avg_decimal_width$}{avg_unit} ± {std_int:max_std_int_width$}.{std_decimal:<max_std_decimal_width$}{std_unit}",
|
||||||
|
// Content
|
||||||
|
name = e.name,
|
||||||
|
avg_int = e.avg_int,
|
||||||
|
avg_decimal = e.avg_decimal,
|
||||||
|
std_int = e.std_int,
|
||||||
|
std_decimal = e.std_decimal,
|
||||||
|
// Units
|
||||||
|
avg_unit = e.avg_unit,
|
||||||
|
std_unit = e.std_unit,
|
||||||
|
// Padding
|
||||||
|
max_name_width = max_name_width,
|
||||||
|
max_avg_int_width = max_avg_int_width,
|
||||||
|
max_avg_decimal_width = max_avg_decimal_width,
|
||||||
|
max_std_int_width = max_std_int_width,
|
||||||
|
max_std_decimal_width = max_std_decimal_width
|
||||||
|
)
|
||||||
|
}).collect::<SmallVec<[String; 12]>>()
|
||||||
|
}
|
||||||
@@ -1,23 +1,12 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{event::EventWriter, prelude::Res, resource::Resource, system::NonSendMut};
|
||||||
event::EventWriter,
|
|
||||||
resource::Resource,
|
|
||||||
system::{NonSendMut, Res},
|
|
||||||
};
|
|
||||||
use sdl2::{event::Event, keyboard::Keycode, EventPump};
|
use sdl2::{event::Event, keyboard::Keycode, EventPump};
|
||||||
|
|
||||||
use crate::{entity::direction::Direction, events::GameEvent};
|
use crate::{
|
||||||
|
entity::direction::Direction,
|
||||||
#[derive(Debug, Clone, Copy)]
|
events::{GameCommand, GameEvent},
|
||||||
pub enum GameCommand {
|
};
|
||||||
MovePlayer(Direction),
|
|
||||||
Exit,
|
|
||||||
TogglePause,
|
|
||||||
ToggleDebug,
|
|
||||||
MuteAudio,
|
|
||||||
ResetLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Resource)]
|
#[derive(Debug, Clone, Resource)]
|
||||||
pub struct Bindings {
|
pub struct Bindings {
|
||||||
@@ -59,7 +48,6 @@ pub fn input_system(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>,
|
|||||||
Event::KeyDown { keycode: Some(key), .. } => {
|
Event::KeyDown { keycode: Some(key), .. } => {
|
||||||
let command = bindings.key_bindings.get(&key).copied();
|
let command = bindings.key_bindings.get(&key).copied();
|
||||||
if let Some(command) = command {
|
if let Some(command) = command {
|
||||||
tracing::info!("triggering command: {:?}", command);
|
|
||||||
writer.write(GameEvent::Command(command));
|
writer.write(GameEvent::Command(command));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/systems/item.rs
Normal file
43
src/systems/item.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use bevy_ecs::{event::EventReader, prelude::*, query::With, system::Query};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
events::GameEvent,
|
||||||
|
systems::components::{EntityType, ItemCollider, PacmanCollider, ScoreResource},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn item_system(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut collision_events: EventReader<GameEvent>,
|
||||||
|
mut score: ResMut<ScoreResource>,
|
||||||
|
pacman_query: Query<Entity, With<PacmanCollider>>,
|
||||||
|
item_query: Query<(Entity, &EntityType), With<ItemCollider>>,
|
||||||
|
) {
|
||||||
|
for event in collision_events.read() {
|
||||||
|
if let GameEvent::Collision(entity1, entity2) = event {
|
||||||
|
// Check if one is Pacman and the other is an item
|
||||||
|
let (_pacman_entity, item_entity) = if pacman_query.get(*entity1).is_ok() && item_query.get(*entity2).is_ok() {
|
||||||
|
(*entity1, *entity2)
|
||||||
|
} else if pacman_query.get(*entity2).is_ok() && item_query.get(*entity1).is_ok() {
|
||||||
|
(*entity2, *entity1)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the item type and update score
|
||||||
|
if let Ok((item_ent, entity_type)) = item_query.get(item_entity) {
|
||||||
|
match entity_type {
|
||||||
|
EntityType::Pellet => {
|
||||||
|
score.0 += 10;
|
||||||
|
}
|
||||||
|
EntityType::PowerPellet => {
|
||||||
|
score.0 += 50;
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the collected item
|
||||||
|
commands.entity(item_ent).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,14 @@
|
|||||||
//! This module contains all the ECS-related logic, including components, systems,
|
//! This module contains all the ECS-related logic, including components, systems,
|
||||||
//! and resources.
|
//! and resources.
|
||||||
|
|
||||||
|
pub mod blinking;
|
||||||
|
pub mod collision;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod control;
|
pub mod control;
|
||||||
|
pub mod debug;
|
||||||
|
pub mod formatting;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
pub mod item;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
|
pub mod profiling;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
|||||||
@@ -1,131 +1,270 @@
|
|||||||
use crate::entity::graph::{Edge, EdgePermissions, Graph};
|
use crate::entity::graph::Graph;
|
||||||
use crate::error::{EntityError, GameError};
|
use crate::entity::{direction::Direction, graph::Edge};
|
||||||
|
use crate::error::{EntityError, GameError, GameResult};
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::components::{DeltaTime, EntityType, Position, Velocity};
|
use crate::systems::components::{DeltaTime, EntityType};
|
||||||
|
use bevy_ecs::component::Component;
|
||||||
use bevy_ecs::event::EventWriter;
|
use bevy_ecs::event::EventWriter;
|
||||||
use bevy_ecs::system::{Query, Res};
|
use bevy_ecs::system::{Query, Res};
|
||||||
|
use glam::Vec2;
|
||||||
|
|
||||||
|
/// A unique identifier for a node, represented by its index in the graph's storage.
|
||||||
|
pub type NodeId = usize;
|
||||||
|
|
||||||
|
/// Progress along an edge between two nodes.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct EdgeProgress {
|
||||||
|
pub target_node: NodeId,
|
||||||
|
/// Progress from 0.0 (at source node) to 1.0 (at target node)
|
||||||
|
pub progress: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pure spatial position component - works for both static and dynamic entities.
|
||||||
|
#[derive(Component, Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub struct Position {
|
||||||
|
/// The current/primary node this entity is at or traveling from
|
||||||
|
pub node: NodeId,
|
||||||
|
/// If Some, entity is traveling between nodes. If None, entity is stationary at node.
|
||||||
|
pub edge_progress: Option<EdgeProgress>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicit movement state - only for entities that can move.
|
||||||
|
#[derive(Component, Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum MovementState {
|
||||||
|
Stopped,
|
||||||
|
Moving { direction: Direction },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Movement capability and parameters - only for entities that can move.
|
||||||
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
|
pub struct Movable {
|
||||||
|
pub speed: f32,
|
||||||
|
pub current_direction: Direction,
|
||||||
|
pub requested_direction: Option<Direction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Position {
|
||||||
|
/// Calculates the current pixel position in the game world.
|
||||||
|
///
|
||||||
|
/// Converts the graph position to screen coordinates, accounting for
|
||||||
|
/// the board offset and centering the sprite.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an `EntityError` if the node or edge is not found.
|
||||||
|
pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult<Vec2> {
|
||||||
|
let pos = match &self.edge_progress {
|
||||||
|
None => {
|
||||||
|
// Entity is stationary at a node
|
||||||
|
let node = graph.get_node(self.node).ok_or(EntityError::NodeNotFound(self.node))?;
|
||||||
|
node.position
|
||||||
|
}
|
||||||
|
Some(edge_progress) => {
|
||||||
|
// Entity is traveling between nodes
|
||||||
|
let from_node = graph.get_node(self.node).ok_or(EntityError::NodeNotFound(self.node))?;
|
||||||
|
let to_node = graph
|
||||||
|
.get_node(edge_progress.target_node)
|
||||||
|
.ok_or(EntityError::NodeNotFound(edge_progress.target_node))?;
|
||||||
|
|
||||||
|
// For zero-distance edges (tunnels), progress >= 1.0 means we're at the target
|
||||||
|
if edge_progress.progress >= 1.0 {
|
||||||
|
to_node.position
|
||||||
|
} else {
|
||||||
|
// Interpolate position based on progress
|
||||||
|
from_node.position + (to_node.position - from_node.position) * edge_progress.progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Vec2::new(
|
||||||
|
pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32,
|
||||||
|
pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Position {
|
||||||
|
fn default() -> Self {
|
||||||
|
Position {
|
||||||
|
node: 0,
|
||||||
|
edge_progress: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl Position {
|
||||||
|
/// Returns `true` if the position is exactly at a node (not traveling).
|
||||||
|
pub fn is_at_node(&self) -> bool {
|
||||||
|
self.edge_progress.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `NodeId` of the current node (source of travel if moving).
|
||||||
|
pub fn current_node(&self) -> NodeId {
|
||||||
|
self.node
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `NodeId` of the destination node, if currently traveling.
|
||||||
|
pub fn target_node(&self) -> Option<NodeId> {
|
||||||
|
self.edge_progress.as_ref().map(|ep| ep.target_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the entity is traveling between nodes.
|
||||||
|
pub fn is_moving(&self) -> bool {
|
||||||
|
self.edge_progress.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn can_traverse(entity_type: EntityType, edge: Edge) -> bool {
|
fn can_traverse(entity_type: EntityType, edge: Edge) -> bool {
|
||||||
match entity_type {
|
let entity_flags = entity_type.traversal_flags();
|
||||||
EntityType::Player => matches!(edge.permissions, EdgePermissions::All),
|
edge.traversal_flags.contains(entity_flags)
|
||||||
EntityType::Ghost => matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly),
|
|
||||||
_ => matches!(edge.permissions, EdgePermissions::All),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn movement_system(
|
pub fn movement_system(
|
||||||
map: Res<Map>,
|
map: Res<Map>,
|
||||||
delta_time: Res<DeltaTime>,
|
delta_time: Res<DeltaTime>,
|
||||||
mut entities: Query<(&mut Velocity, &mut Position, &EntityType)>,
|
mut entities: Query<(&mut MovementState, &mut Movable, &mut Position, &EntityType)>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
for (mut velocity, mut position, entity_type) in entities.iter_mut() {
|
for (mut movement_state, mut movable, mut position, entity_type) in entities.iter_mut() {
|
||||||
let distance = velocity.speed * 60.0 * delta_time.0;
|
let distance = movable.speed * 60.0 * delta_time.0;
|
||||||
|
|
||||||
// Decrement the remaining frames for the next direction
|
match *movement_state {
|
||||||
if let Some((direction, remaining)) = velocity.next_direction {
|
MovementState::Stopped => {
|
||||||
if remaining > 0 {
|
// Check if we have a requested direction to start moving
|
||||||
velocity.next_direction = Some((direction, remaining - 1));
|
if let Some(requested_direction) = movable.requested_direction {
|
||||||
} else {
|
if let Some(edge) = map.graph.find_edge_in_direction(position.node, requested_direction) {
|
||||||
velocity.next_direction = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match *position {
|
|
||||||
Position::AtNode(node_id) => {
|
|
||||||
// We're not moving, but a buffered direction is available.
|
|
||||||
if let Some((next_direction, _)) = velocity.next_direction {
|
|
||||||
if let Some(edge) = map.graph.find_edge_in_direction(node_id, next_direction) {
|
|
||||||
if can_traverse(*entity_type, edge) {
|
if can_traverse(*entity_type, edge) {
|
||||||
// Start moving in that direction
|
// Start moving in the requested direction
|
||||||
*position = Position::BetweenNodes {
|
let progress = if edge.distance > 0.0 {
|
||||||
from: node_id,
|
distance / edge.distance
|
||||||
to: edge.target,
|
} else {
|
||||||
traversed: distance,
|
// Zero-distance edge (tunnels) - immediately teleport
|
||||||
|
tracing::debug!("Entity entering tunnel from node {} to node {}", position.node, edge.target);
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
position.edge_progress = Some(EdgeProgress {
|
||||||
|
target_node: edge.target,
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
movable.current_direction = requested_direction;
|
||||||
|
movable.requested_direction = None;
|
||||||
|
*movement_state = MovementState::Moving {
|
||||||
|
direction: requested_direction,
|
||||||
};
|
};
|
||||||
velocity.direction = next_direction;
|
|
||||||
velocity.next_direction = None;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.write(
|
errors.write(
|
||||||
EntityError::InvalidMovement(format!(
|
EntityError::InvalidMovement(format!(
|
||||||
"No edge found in direction {:?} from node {}",
|
"No edge found in direction {:?} from node {}",
|
||||||
next_direction, node_id
|
requested_direction, position.node
|
||||||
))
|
))
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Position::BetweenNodes { from, to, traversed } => {
|
MovementState::Moving { direction } => {
|
||||||
// There is no point in any of the next logic if we don't travel at all
|
// Continue moving or handle node transitions
|
||||||
if distance <= 0.0 {
|
let current_node = position.node;
|
||||||
return;
|
if let Some(edge_progress) = &mut position.edge_progress {
|
||||||
}
|
// Extract target node before mutable operations
|
||||||
|
let target_node = edge_progress.target_node;
|
||||||
|
|
||||||
let edge = map
|
// Get the current edge for distance calculation
|
||||||
.graph
|
let edge = map.graph.find_edge(current_node, target_node);
|
||||||
.find_edge(from, to)
|
|
||||||
.ok_or_else(|| {
|
if let Some(edge) = edge {
|
||||||
|
// Update progress along the edge
|
||||||
|
if edge.distance > 0.0 {
|
||||||
|
edge_progress.progress += distance / edge.distance;
|
||||||
|
} else {
|
||||||
|
// Zero-distance edge (tunnels) - immediately complete
|
||||||
|
edge_progress.progress = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if edge_progress.progress >= 1.0 {
|
||||||
|
// Reached the target node
|
||||||
|
let overflow = if edge.distance > 0.0 {
|
||||||
|
(edge_progress.progress - 1.0) * edge.distance
|
||||||
|
} else {
|
||||||
|
// Zero-distance edge - use remaining distance for overflow
|
||||||
|
distance
|
||||||
|
};
|
||||||
|
position.node = target_node;
|
||||||
|
position.edge_progress = None;
|
||||||
|
|
||||||
|
let mut continued_moving = false;
|
||||||
|
|
||||||
|
// Try to use requested direction first
|
||||||
|
if let Some(requested_direction) = movable.requested_direction {
|
||||||
|
if let Some(next_edge) = map.graph.find_edge_in_direction(position.node, requested_direction) {
|
||||||
|
if can_traverse(*entity_type, next_edge) {
|
||||||
|
let next_progress = if next_edge.distance > 0.0 {
|
||||||
|
overflow / next_edge.distance
|
||||||
|
} else {
|
||||||
|
// Zero-distance edge - immediately complete
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
position.edge_progress = Some(EdgeProgress {
|
||||||
|
target_node: next_edge.target,
|
||||||
|
progress: next_progress,
|
||||||
|
});
|
||||||
|
movable.current_direction = requested_direction;
|
||||||
|
movable.requested_direction = None;
|
||||||
|
*movement_state = MovementState::Moving {
|
||||||
|
direction: requested_direction,
|
||||||
|
};
|
||||||
|
continued_moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no requested direction or it failed, try to continue in current direction
|
||||||
|
if !continued_moving {
|
||||||
|
if let Some(next_edge) = map.graph.find_edge_in_direction(position.node, direction) {
|
||||||
|
if can_traverse(*entity_type, next_edge) {
|
||||||
|
let next_progress = if next_edge.distance > 0.0 {
|
||||||
|
overflow / next_edge.distance
|
||||||
|
} else {
|
||||||
|
// Zero-distance edge - immediately complete
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
position.edge_progress = Some(EdgeProgress {
|
||||||
|
target_node: next_edge.target,
|
||||||
|
progress: next_progress,
|
||||||
|
});
|
||||||
|
// Keep current direction and movement state
|
||||||
|
continued_moving = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we couldn't continue moving, stop
|
||||||
|
if !continued_moving {
|
||||||
|
*movement_state = MovementState::Stopped;
|
||||||
|
movable.requested_direction = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Edge not found - this is an inconsistent state
|
||||||
errors.write(
|
errors.write(
|
||||||
EntityError::InvalidMovement(format!(
|
EntityError::InvalidMovement(format!(
|
||||||
"Inconsistent state: Traverser is on a non-existent edge from {} to {}.",
|
"Inconsistent state: Moving on non-existent edge from {} to {}",
|
||||||
from, to
|
current_node, target_node
|
||||||
))
|
))
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
return;
|
*movement_state = MovementState::Stopped;
|
||||||
})
|
position.edge_progress = None;
|
||||||
.unwrap();
|
}
|
||||||
|
|
||||||
let new_traversed = traversed + distance;
|
|
||||||
|
|
||||||
if new_traversed < edge.distance {
|
|
||||||
// Still on the same edge, just update the distance.
|
|
||||||
*position = Position::BetweenNodes {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
traversed: new_traversed,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
let overflow = new_traversed - edge.distance;
|
// Movement state says moving but no edge progress - this shouldn't happen
|
||||||
let mut moved = false;
|
errors.write(EntityError::InvalidMovement("Entity in Moving state but no edge progress".to_string()).into());
|
||||||
|
*movement_state = MovementState::Stopped;
|
||||||
// If we buffered a direction, try to find an edge in that direction
|
|
||||||
if let Some((next_dir, _)) = velocity.next_direction {
|
|
||||||
if let Some(edge) = map.graph.find_edge_in_direction(to, next_dir) {
|
|
||||||
if can_traverse(*entity_type, edge) {
|
|
||||||
*position = Position::BetweenNodes {
|
|
||||||
from: to,
|
|
||||||
to: edge.target,
|
|
||||||
traversed: overflow,
|
|
||||||
};
|
|
||||||
|
|
||||||
velocity.direction = next_dir; // Remember our new direction
|
|
||||||
velocity.next_direction = None; // Consume the buffered direction
|
|
||||||
moved = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't move, try to continue in the current direction
|
|
||||||
if !moved {
|
|
||||||
if let Some(edge) = map.graph.find_edge_in_direction(to, velocity.direction) {
|
|
||||||
if can_traverse(*entity_type, edge) {
|
|
||||||
*position = Position::BetweenNodes {
|
|
||||||
from: to,
|
|
||||||
to: edge.target,
|
|
||||||
traversed: overflow,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
*position = Position::AtNode(to);
|
|
||||||
velocity.next_direction = None;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
*position = Position::AtNode(to);
|
|
||||||
velocity.next_direction = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
153
src/systems/profiling.rs
Normal file
153
src/systems/profiling.rs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
use bevy_ecs::prelude::Resource;
|
||||||
|
use bevy_ecs::system::{IntoSystem, System};
|
||||||
|
use circular_buffer::CircularBuffer;
|
||||||
|
use micromap::Map;
|
||||||
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
use thousands::Separable;
|
||||||
|
|
||||||
|
/// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic.
|
||||||
|
const MAX_SYSTEMS: usize = 11;
|
||||||
|
/// The number of durations to keep in the circular buffer.
|
||||||
|
const TIMING_WINDOW_SIZE: usize = 30;
|
||||||
|
|
||||||
|
#[derive(Resource, Default, Debug)]
|
||||||
|
pub struct SystemTimings {
|
||||||
|
/// Map of system names to a queue of durations, using a circular buffer.
|
||||||
|
///
|
||||||
|
/// Uses a RwLock to allow multiple readers for the HashMap, and a Mutex on the circular buffer for exclusive access.
|
||||||
|
/// This is probably overkill, but it's fun to play with.
|
||||||
|
///
|
||||||
|
/// Also, we use a micromap::Map as the number of systems is generally quite small.
|
||||||
|
/// Just make sure to set the capacity appropriately, or it will panic.
|
||||||
|
pub timings: RwLock<Map<&'static str, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, MAX_SYSTEMS>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemTimings {
|
||||||
|
pub fn add_timing(&self, name: &'static str, duration: Duration) {
|
||||||
|
// acquire a upgradable read lock
|
||||||
|
let mut timings = self.timings.upgradable_read();
|
||||||
|
|
||||||
|
// happy path, the name is already in the map (no need to mutate the hashmap)
|
||||||
|
if timings.contains_key(name) {
|
||||||
|
let queue = timings
|
||||||
|
.get(name)
|
||||||
|
.expect("System name not found in map after contains_key check");
|
||||||
|
let mut queue = queue.lock();
|
||||||
|
|
||||||
|
queue.push_back(duration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, acquire a write lock and insert a new queue
|
||||||
|
timings.with_upgraded(|timings| {
|
||||||
|
let queue = timings.entry(name).or_insert_with(|| Mutex::new(CircularBuffer::new()));
|
||||||
|
queue.lock().push_back(duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_stats(&self) -> Map<&'static str, (Duration, Duration), MAX_SYSTEMS> {
|
||||||
|
let timings = self.timings.read();
|
||||||
|
let mut stats = Map::new();
|
||||||
|
|
||||||
|
for (name, queue) in timings.iter() {
|
||||||
|
if queue.lock().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let durations: Vec<f64> = queue.lock().iter().map(|d| d.as_secs_f64() * 1000.0).collect();
|
||||||
|
let count = durations.len() as f64;
|
||||||
|
|
||||||
|
let sum: f64 = durations.iter().sum();
|
||||||
|
let mean = sum / count;
|
||||||
|
|
||||||
|
let variance = durations.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count;
|
||||||
|
let std_dev = variance.sqrt();
|
||||||
|
|
||||||
|
stats.insert(
|
||||||
|
*name,
|
||||||
|
(
|
||||||
|
Duration::from_secs_f64(mean / 1000.0),
|
||||||
|
Duration::from_secs_f64(std_dev / 1000.0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_total_stats(&self) -> (Duration, Duration) {
|
||||||
|
let timings = self.timings.read();
|
||||||
|
let mut all_durations = Vec::new();
|
||||||
|
|
||||||
|
for queue in timings.values() {
|
||||||
|
all_durations.extend(queue.lock().iter().map(|d| d.as_secs_f64() * 1000.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if all_durations.is_empty() {
|
||||||
|
return (Duration::ZERO, Duration::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = all_durations.len() as f64;
|
||||||
|
let sum: f64 = all_durations.iter().sum();
|
||||||
|
let mean = sum / count;
|
||||||
|
|
||||||
|
let variance = all_durations.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count;
|
||||||
|
let std_dev = variance.sqrt();
|
||||||
|
|
||||||
|
(
|
||||||
|
Duration::from_secs_f64(mean / 1000.0),
|
||||||
|
Duration::from_secs_f64(std_dev / 1000.0),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_timing_display(&self) -> String {
|
||||||
|
let stats = self.get_stats();
|
||||||
|
let (total_avg, total_std) = self.get_total_stats();
|
||||||
|
|
||||||
|
let effective_fps = match 1.0 / total_avg.as_secs_f64() {
|
||||||
|
f if f > 100.0 => (f as u32).separate_with_commas(),
|
||||||
|
f if f < 10.0 => format!("{:.1} FPS", f),
|
||||||
|
f => format!("{:.0} FPS", f),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Collect timing data for formatting
|
||||||
|
let mut timing_data = Vec::new();
|
||||||
|
|
||||||
|
// Add total stats
|
||||||
|
timing_data.push((effective_fps, total_avg, total_std));
|
||||||
|
|
||||||
|
// Add top 5 most expensive systems
|
||||||
|
let mut sorted_stats: Vec<_> = stats.iter().collect();
|
||||||
|
sorted_stats.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
|
||||||
|
|
||||||
|
for (name, (avg, std_dev)) in sorted_stats.iter().take(5) {
|
||||||
|
timing_data.push((name.to_string(), *avg, *std_dev));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the formatting module to format the data
|
||||||
|
crate::systems::formatting::format_timing_display(timing_data).join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn profile<S, M>(name: &'static str, system: S) -> impl FnMut(&mut bevy_ecs::world::World)
|
||||||
|
where
|
||||||
|
S: IntoSystem<(), (), M> + 'static,
|
||||||
|
{
|
||||||
|
let mut system: S::System = IntoSystem::into_system(system);
|
||||||
|
let mut is_initialized = false;
|
||||||
|
move |world: &mut bevy_ecs::world::World| {
|
||||||
|
if !is_initialized {
|
||||||
|
system.initialize(world);
|
||||||
|
is_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
system.run((), world);
|
||||||
|
let duration = start.elapsed();
|
||||||
|
|
||||||
|
if let Some(timings) = world.get_resource::<SystemTimings>() {
|
||||||
|
timings.add_timing(name, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,51 @@
|
|||||||
use crate::error::{GameError, TextureError};
|
use crate::error::{GameError, TextureError};
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::components::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity};
|
use crate::systems::components::{DeltaTime, DirectionalAnimated, RenderDirty, Renderable};
|
||||||
|
use crate::systems::movement::{Movable, MovementState, Position};
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
use bevy_ecs::entity::Entity;
|
use bevy_ecs::entity::Entity;
|
||||||
use bevy_ecs::event::EventWriter;
|
use bevy_ecs::event::EventWriter;
|
||||||
use bevy_ecs::system::{NonSendMut, Query, Res};
|
use bevy_ecs::prelude::{Changed, Or, RemovedComponents};
|
||||||
|
use bevy_ecs::system::{NonSendMut, Query, Res, ResMut};
|
||||||
use sdl2::render::{Canvas, Texture};
|
use sdl2::render::{Canvas, Texture};
|
||||||
use sdl2::video::Window;
|
use sdl2::video::Window;
|
||||||
|
|
||||||
|
pub fn dirty_render_system(
|
||||||
|
mut dirty: ResMut<RenderDirty>,
|
||||||
|
changed_renderables: Query<(), Or<(Changed<Renderable>, Changed<Position>)>>,
|
||||||
|
removed_renderables: RemovedComponents<Renderable>,
|
||||||
|
) {
|
||||||
|
if !changed_renderables.is_empty() || !removed_renderables.is_empty() {
|
||||||
|
dirty.0 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Updates the directional animated texture of an entity.
|
/// Updates the directional animated texture of an entity.
|
||||||
///
|
///
|
||||||
/// This runs before the render system so it can update the sprite based on the current direction of travel, as well as whether the entity is moving.
|
/// This runs before the render system so it can update the sprite based on the current direction of travel, as well as whether the entity is moving.
|
||||||
pub fn directional_render_system(
|
pub fn directional_render_system(
|
||||||
dt: Res<DeltaTime>,
|
dt: Res<DeltaTime>,
|
||||||
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable, &Position)>,
|
mut renderables: Query<(&MovementState, &Movable, &mut DirectionalAnimated, &mut Renderable)>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
for (velocity, mut texture, mut renderable, position) in renderables.iter_mut() {
|
for (movement_state, movable, mut texture, mut renderable) in renderables.iter_mut() {
|
||||||
let stopped = matches!(position, Position::AtNode(_));
|
let stopped = matches!(movement_state, MovementState::Stopped);
|
||||||
|
let current_direction = movable.current_direction;
|
||||||
|
|
||||||
let texture = if stopped {
|
let texture = if stopped {
|
||||||
texture.stopped_textures[velocity.direction.as_usize()].as_mut()
|
texture.stopped_textures[current_direction.as_usize()].as_mut()
|
||||||
} else {
|
} else {
|
||||||
texture.textures[velocity.direction.as_usize()].as_mut()
|
texture.textures[current_direction.as_usize()].as_mut()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(texture) = texture {
|
if let Some(texture) = texture {
|
||||||
if !stopped {
|
if !stopped {
|
||||||
texture.tick(dt.0);
|
texture.tick(dt.0);
|
||||||
}
|
}
|
||||||
renderable.sprite = *texture.current_tile();
|
let new_tile = *texture.current_tile();
|
||||||
|
if renderable.sprite != new_tile {
|
||||||
|
renderable.sprite = new_tile;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into());
|
errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into());
|
||||||
continue;
|
continue;
|
||||||
@@ -48,13 +65,13 @@ pub fn render_system(
|
|||||||
mut backbuffer: NonSendMut<BackbufferResource>,
|
mut backbuffer: NonSendMut<BackbufferResource>,
|
||||||
mut atlas: NonSendMut<SpriteAtlas>,
|
mut atlas: NonSendMut<SpriteAtlas>,
|
||||||
map: Res<Map>,
|
map: Res<Map>,
|
||||||
renderables: Query<(Entity, &mut Renderable, &Position)>,
|
dirty: Res<RenderDirty>,
|
||||||
|
renderables: Query<(Entity, &Renderable, &Position)>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
// Clear the main canvas first
|
if !dirty.0 {
|
||||||
canvas.set_draw_color(sdl2::pixels::Color::BLACK);
|
return;
|
||||||
canvas.clear();
|
}
|
||||||
|
|
||||||
// Render to backbuffer
|
// Render to backbuffer
|
||||||
canvas
|
canvas
|
||||||
.with_texture_canvas(&mut backbuffer.0, |backbuffer_canvas| {
|
.with_texture_canvas(&mut backbuffer.0, |backbuffer_canvas| {
|
||||||
@@ -63,17 +80,16 @@ pub fn render_system(
|
|||||||
backbuffer_canvas.clear();
|
backbuffer_canvas.clear();
|
||||||
|
|
||||||
// Copy the pre-rendered map texture to the backbuffer
|
// Copy the pre-rendered map texture to the backbuffer
|
||||||
backbuffer_canvas
|
if let Err(e) = backbuffer_canvas.copy(&map_texture.0, None, None) {
|
||||||
.copy(&map_texture.0, None, None)
|
errors.write(TextureError::RenderFailed(e.to_string()).into());
|
||||||
.err()
|
}
|
||||||
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
|
||||||
|
|
||||||
// Render all entities to the backbuffer
|
// Render all entities to the backbuffer
|
||||||
for (_, mut renderable, position) in renderables
|
for (_, renderable, position) in renderables.iter() {
|
||||||
// .iter_mut()
|
if !renderable.visible {
|
||||||
// .sort_by_key::<&mut Renderable, _, _>(|(renderable, renderable, renderable)| renderable.layer)
|
continue;
|
||||||
// .collect()
|
}
|
||||||
{
|
|
||||||
let pos = position.get_pixel_pos(&map.graph);
|
let pos = position.get_pixel_pos(&map.graph);
|
||||||
match pos {
|
match pos {
|
||||||
Ok(pos) => {
|
Ok(pos) => {
|
||||||
@@ -96,12 +112,4 @@ pub fn render_system(
|
|||||||
})
|
})
|
||||||
.err()
|
.err()
|
||||||
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
||||||
|
|
||||||
// Copy backbuffer to main canvas and present
|
|
||||||
canvas
|
|
||||||
.copy(&backbuffer.0, None, None)
|
|
||||||
.err()
|
|
||||||
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
|
||||||
|
|
||||||
canvas.present();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub struct MapperFrame {
|
|||||||
pub height: u16,
|
pub height: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub struct AtlasTile {
|
pub struct AtlasTile {
|
||||||
pub pos: U16Vec2,
|
pub pos: U16Vec2,
|
||||||
pub size: U16Vec2,
|
pub size: U16Vec2,
|
||||||
@@ -30,7 +30,7 @@ pub struct AtlasTile {
|
|||||||
|
|
||||||
impl AtlasTile {
|
impl AtlasTile {
|
||||||
pub fn render<C: RenderTarget>(
|
pub fn render<C: RenderTarget>(
|
||||||
&mut self,
|
&self,
|
||||||
canvas: &mut Canvas<C>,
|
canvas: &mut Canvas<C>,
|
||||||
atlas: &mut SpriteAtlas,
|
atlas: &mut SpriteAtlas,
|
||||||
dest: Rect,
|
dest: Rect,
|
||||||
@@ -41,7 +41,7 @@ impl AtlasTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_with_color<C: RenderTarget>(
|
pub fn render_with_color<C: RenderTarget>(
|
||||||
&mut self,
|
&self,
|
||||||
canvas: &mut Canvas<C>,
|
canvas: &mut Canvas<C>,
|
||||||
atlas: &mut SpriteAtlas,
|
atlas: &mut SpriteAtlas,
|
||||||
dest: Rect,
|
dest: Rect,
|
||||||
|
|||||||
@@ -103,9 +103,9 @@ impl TextTexture {
|
|||||||
&self.char_map
|
&self.char_map
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tile(&mut self, c: char, atlas: &mut SpriteAtlas) -> Result<Option<&mut AtlasTile>> {
|
pub fn get_tile(&mut self, c: char, atlas: &mut SpriteAtlas) -> Result<Option<&AtlasTile>> {
|
||||||
if self.char_map.contains_key(&c) {
|
if self.char_map.contains_key(&c) {
|
||||||
return Ok(self.char_map.get_mut(&c));
|
return Ok(self.char_map.get(&c));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(tile_name) = char_to_tile_name(c) {
|
if let Some(tile_name) = char_to_tile_name(c) {
|
||||||
@@ -113,7 +113,7 @@ impl TextTexture {
|
|||||||
.get_tile(&tile_name)
|
.get_tile(&tile_name)
|
||||||
.ok_or(GameError::Texture(TextureError::AtlasTileNotFound(tile_name)))?;
|
.ok_or(GameError::Texture(TextureError::AtlasTileNotFound(tile_name)))?;
|
||||||
self.char_map.insert(c, tile);
|
self.char_map.insert(c, tile);
|
||||||
Ok(self.char_map.get_mut(&c))
|
Ok(self.char_map.get(&c))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|||||||
97
tests/formatting.rs
Normal file
97
tests/formatting.rs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
use itertools::izip;
|
||||||
|
use pacman::systems::formatting::format_timing_display;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
fn get_timing_data() -> Vec<(String, Duration, Duration)> {
|
||||||
|
vec![
|
||||||
|
("total".to_string(), Duration::from_micros(1234), Duration::from_micros(570)),
|
||||||
|
("input".to_string(), Duration::from_micros(120), Duration::from_micros(45)),
|
||||||
|
("player".to_string(), Duration::from_micros(456), Duration::from_micros(123)),
|
||||||
|
("movement".to_string(), Duration::from_micros(789), Duration::from_micros(234)),
|
||||||
|
("render".to_string(), Duration::from_micros(12), Duration::from_micros(3)),
|
||||||
|
("debug".to_string(), Duration::from_nanos(460), Duration::from_nanos(557)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_formatted_output() -> impl IntoIterator<Item = String> {
|
||||||
|
format_timing_display(get_timing_data())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_formatting_alignment() {
|
||||||
|
let mut colon_positions = vec![];
|
||||||
|
let mut first_decimal_positions = vec![];
|
||||||
|
let mut second_decimal_positions = vec![];
|
||||||
|
let mut first_unit_positions = vec![];
|
||||||
|
let mut second_unit_positions = vec![];
|
||||||
|
|
||||||
|
get_formatted_output().into_iter().for_each(|line| {
|
||||||
|
let (mut got_decimal, mut got_unit) = (false, false);
|
||||||
|
for (i, char) in line.chars().enumerate() {
|
||||||
|
match char {
|
||||||
|
':' => colon_positions.push(i),
|
||||||
|
'.' => {
|
||||||
|
if got_decimal {
|
||||||
|
second_decimal_positions.push(i);
|
||||||
|
} else {
|
||||||
|
first_decimal_positions.push(i);
|
||||||
|
}
|
||||||
|
got_decimal = true;
|
||||||
|
}
|
||||||
|
's' => {
|
||||||
|
if got_unit {
|
||||||
|
first_unit_positions.push(i);
|
||||||
|
} else {
|
||||||
|
second_unit_positions.push(i);
|
||||||
|
got_unit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert that all positions were found
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
&colon_positions,
|
||||||
|
&first_decimal_positions,
|
||||||
|
&second_decimal_positions,
|
||||||
|
&first_unit_positions,
|
||||||
|
&second_unit_positions
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.all(|p| p.len() == 6),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert that all positions are the same
|
||||||
|
assert!(
|
||||||
|
colon_positions.iter().all(|&p| p == colon_positions[0]),
|
||||||
|
"colon positions are not the same {:?}",
|
||||||
|
colon_positions
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
first_decimal_positions.iter().all(|&p| p == first_decimal_positions[0]),
|
||||||
|
"first decimal positions are not the same {:?}",
|
||||||
|
first_decimal_positions
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
second_decimal_positions.iter().all(|&p| p == second_decimal_positions[0]),
|
||||||
|
"second decimal positions are not the same {:?}",
|
||||||
|
second_decimal_positions
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
first_unit_positions.iter().all(|&p| p == first_unit_positions[0]),
|
||||||
|
"first unit positions are not the same {:?}",
|
||||||
|
first_unit_positions
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
second_unit_positions.iter().all(|&p| p == second_unit_positions[0]),
|
||||||
|
"second unit positions are not the same {:?}",
|
||||||
|
second_unit_positions
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use pacman::entity::direction::Direction;
|
use pacman::entity::direction::Direction;
|
||||||
use pacman::entity::graph::{EdgePermissions, Graph, Node};
|
use pacman::entity::graph::{Graph, Node, TraversalFlags};
|
||||||
|
|
||||||
fn create_test_graph() -> Graph {
|
fn create_test_graph() -> Graph {
|
||||||
let mut graph = Graph::new();
|
let mut graph = Graph::new();
|
||||||
@@ -78,11 +78,11 @@ fn test_graph_edge_permissions() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
graph
|
graph
|
||||||
.add_edge(node1, node2, false, None, Direction::Right, EdgePermissions::GhostsOnly)
|
.add_edge(node1, node2, false, None, Direction::Right, TraversalFlags::GHOST)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let edge = graph.find_edge_in_direction(node1, Direction::Right).unwrap();
|
let edge = graph.find_edge_in_direction(node1, Direction::Right).unwrap();
|
||||||
assert_eq!(edge.permissions, EdgePermissions::GhostsOnly);
|
assert_eq!(edge.traversal_flags, TraversalFlags::GHOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -118,21 +118,21 @@ fn should_error_on_negative_edge_distance() {
|
|||||||
position: glam::Vec2::new(16.0, 0.0),
|
position: glam::Vec2::new(16.0, 0.0),
|
||||||
});
|
});
|
||||||
|
|
||||||
let result = graph.add_edge(node1, node2, false, Some(-1.0), Direction::Right, EdgePermissions::All);
|
let result = graph.add_edge(node1, node2, false, Some(-1.0), Direction::Right, TraversalFlags::ALL);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_error_on_duplicate_edge_without_replace() {
|
fn should_error_on_duplicate_edge_without_replace() {
|
||||||
let mut graph = create_test_graph();
|
let mut graph = create_test_graph();
|
||||||
let result = graph.add_edge(0, 1, false, None, Direction::Right, EdgePermissions::All);
|
let result = graph.add_edge(0, 1, false, None, Direction::Right, TraversalFlags::ALL);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_allow_replacing_an_edge() {
|
fn should_allow_replacing_an_edge() {
|
||||||
let mut graph = create_test_graph();
|
let mut graph = create_test_graph();
|
||||||
let result = graph.add_edge(0, 1, true, Some(42.0), Direction::Right, EdgePermissions::All);
|
let result = graph.add_edge(0, 1, true, Some(42.0), Direction::Right, TraversalFlags::ALL);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
let edge = graph.find_edge(0, 1).unwrap();
|
let edge = graph.find_edge(0, 1).unwrap();
|
||||||
|
|||||||
40
tests/profiling.rs
Normal file
40
tests/profiling.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use pacman::systems::profiling::SystemTimings;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timing_statistics() {
|
||||||
|
let timings = SystemTimings::default();
|
||||||
|
|
||||||
|
// Add some test data
|
||||||
|
timings.add_timing("test_system", Duration::from_millis(10));
|
||||||
|
timings.add_timing("test_system", Duration::from_millis(12));
|
||||||
|
timings.add_timing("test_system", Duration::from_millis(8));
|
||||||
|
|
||||||
|
let stats = timings.get_stats();
|
||||||
|
let (avg, std_dev) = stats.get("test_system").unwrap();
|
||||||
|
|
||||||
|
// Average should be 10ms, standard deviation should be small
|
||||||
|
assert!((avg.as_millis() as f64 - 10.0).abs() < 1.0);
|
||||||
|
assert!(std_dev.as_millis() > 0);
|
||||||
|
|
||||||
|
let (total_avg, total_std) = timings.get_total_stats();
|
||||||
|
assert!((total_avg.as_millis() as f64 - 10.0).abs() < 1.0);
|
||||||
|
assert!(total_std.as_millis() > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn test_window_size_limit() {
|
||||||
|
// let timings = SystemTimings::default();
|
||||||
|
|
||||||
|
// // Add more than 90 timings to test window size limit
|
||||||
|
// for i in 0..100 {
|
||||||
|
// timings.add_timing("test_system", Duration::from_millis(i));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let stats = timings.get_stats();
|
||||||
|
// let (avg, _) = stats.get("test_system").unwrap();
|
||||||
|
|
||||||
|
// // Should only keep the last 90 values, so average should be around 55ms
|
||||||
|
// // (average of 10-99)
|
||||||
|
// assert!((avg.as_millis() as f64 - 55.0).abs() < 5.0);
|
||||||
|
// }
|
||||||
Reference in New Issue
Block a user