mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-13 16:12:18 -06:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b60888219b | ||
|
|
3c50bfeab6 | ||
|
|
132067c573 |
75
Cargo.lock
generated
75
Cargo.lock
generated
@@ -570,12 +570,76 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-complex",
|
||||||
|
"num-integer",
|
||||||
|
"num-iter",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-complex"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-iter"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-rational"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -618,6 +682,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"speculoos",
|
||||||
"spin_sleep",
|
"spin_sleep",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
@@ -965,6 +1030,16 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "speculoos"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00c84ba5fa63b0de837c0d3cef5373ac1c3c6342053b7f446a210a1dde79a034"
|
||||||
|
dependencies = [
|
||||||
|
"num",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ libc = "0.2.175" # TODO: Describe why this is required.
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.1"
|
pretty_assertions = "1.4.1"
|
||||||
|
speculoos = "0.13.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
phf = { version = "0.13.1", features = ["macros"] }
|
phf = { version = "0.13.1", features = ["macros"] }
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ pub mod collider {
|
|||||||
pub mod ui {
|
pub mod ui {
|
||||||
/// Debug font size in points
|
/// Debug font size in points
|
||||||
pub const DEBUG_FONT_SIZE: u16 = 12;
|
pub const DEBUG_FONT_SIZE: u16 = 12;
|
||||||
/// Power pellet blink rate in seconds
|
/// Power pellet blink rate in ticks (at 60 FPS, 12 ticks = 0.2 seconds)
|
||||||
pub const POWER_PELLET_BLINK_RATE: f32 = 0.2;
|
pub const POWER_PELLET_BLINK_RATE: u32 = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map tile types that define gameplay behavior and collision properties.
|
/// Map tile types that define gameplay behavior and collision properties.
|
||||||
|
|||||||
120
src/formatter.rs
120
src/formatter.rs
@@ -4,9 +4,9 @@ use std::fmt;
|
|||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use time::macros::format_description;
|
use time::macros::format_description;
|
||||||
use time::{format_description::FormatItem, OffsetDateTime};
|
use time::{format_description::FormatItem, OffsetDateTime};
|
||||||
use tracing::{Event, Subscriber};
|
use tracing::{Event, Level, Subscriber};
|
||||||
use tracing_subscriber::fmt::format::Writer;
|
use tracing_subscriber::fmt::format::Writer;
|
||||||
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
|
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields, FormattedFields};
|
||||||
use tracing_subscriber::registry::LookupSpan;
|
use tracing_subscriber::registry::LookupSpan;
|
||||||
|
|
||||||
/// Global atomic counter for tracking game ticks
|
/// Global atomic counter for tracking game ticks
|
||||||
@@ -25,15 +25,7 @@ const TIMESTAMP_FORMAT: &[FormatItem<'static>] = format_description!("[hour]:[mi
|
|||||||
|
|
||||||
/// A custom formatter that includes both timestamp and tick counter in hexadecimal
|
/// A custom formatter that includes both timestamp and tick counter in hexadecimal
|
||||||
///
|
///
|
||||||
/// This formatter provides:
|
/// Re-implementation of the Full formatter to add a tick counter and timestamp.
|
||||||
/// - High-precision timestamps (HH:MM:SS.mmm on Emscripten, HH:MM:SS.mmmmm otherwise)
|
|
||||||
/// - Hexadecimal tick counter for frame correlation
|
|
||||||
/// - Standard log level and target information
|
|
||||||
///
|
|
||||||
/// Performance considerations:
|
|
||||||
/// - Timestamp format is cached at compile time
|
|
||||||
/// - Tick counter access is atomic and very fast
|
|
||||||
/// - Combined formatting operations for efficiency
|
|
||||||
pub struct CustomFormatter;
|
pub struct CustomFormatter;
|
||||||
|
|
||||||
impl<S, N> FormatEvent<S, N> for CustomFormatter
|
impl<S, N> FormatEvent<S, N> for CustomFormatter
|
||||||
@@ -42,35 +34,109 @@ where
|
|||||||
N: for<'a> FormatFields<'a> + 'static,
|
N: for<'a> FormatFields<'a> + 'static,
|
||||||
{
|
{
|
||||||
fn format_event(&self, ctx: &FmtContext<'_, S, N>, mut writer: Writer<'_>, event: &Event<'_>) -> fmt::Result {
|
fn format_event(&self, ctx: &FmtContext<'_, S, N>, mut writer: Writer<'_>, event: &Event<'_>) -> fmt::Result {
|
||||||
// Format timestamp using cached format description
|
let meta = event.metadata();
|
||||||
|
|
||||||
|
// 1) Timestamp (dimmed when ANSI)
|
||||||
let now = OffsetDateTime::now_utc();
|
let now = OffsetDateTime::now_utc();
|
||||||
let formatted_time = now.format(&TIMESTAMP_FORMAT).map_err(|e| {
|
let formatted_time = now.format(&TIMESTAMP_FORMAT).map_err(|e| {
|
||||||
// Preserve the original error information for debugging
|
|
||||||
eprintln!("Failed to format timestamp: {}", e);
|
eprintln!("Failed to format timestamp: {}", e);
|
||||||
fmt::Error
|
fmt::Error
|
||||||
})?;
|
})?;
|
||||||
|
write_dimmed(&mut writer, formatted_time)?;
|
||||||
|
writer.write_char(' ')?;
|
||||||
|
|
||||||
// Get tick count and format everything together
|
// 2) Tick counter, dim when ANSI
|
||||||
let tick_count = get_tick_count();
|
let tick_count = get_tick_count() & TICK_DISPLAY_MASK;
|
||||||
let metadata = event.metadata();
|
if writer.has_ansi_escapes() {
|
||||||
|
write!(writer, "\x1b[2m0x{:04X}\x1b[0m ", tick_count)?;
|
||||||
|
} else {
|
||||||
|
write!(writer, "0x{:04X} ", tick_count)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Combined formatting: timestamp, tick counter, level, and target in one write
|
// 3) Colored 5-char level like Full
|
||||||
write!(
|
write_colored_level(&mut writer, meta.level())?;
|
||||||
writer,
|
writer.write_char(' ')?;
|
||||||
"{} 0x{:04X} {:5} {}: ",
|
|
||||||
formatted_time,
|
|
||||||
tick_count & TICK_DISPLAY_MASK,
|
|
||||||
metadata.level(),
|
|
||||||
metadata.target()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Format the fields (the actual log message)
|
// 4) Span scope chain (bold names, fields in braces, dimmed ':')
|
||||||
ctx.field_format().format_fields(writer.by_ref(), event)?;
|
if let Some(scope) = ctx.event_scope() {
|
||||||
|
let mut saw_any = false;
|
||||||
|
for span in scope.from_root() {
|
||||||
|
write_bold(&mut writer, span.metadata().name())?;
|
||||||
|
saw_any = true;
|
||||||
|
let ext = span.extensions();
|
||||||
|
if let Some(fields) = &ext.get::<FormattedFields<N>>() {
|
||||||
|
if !fields.is_empty() {
|
||||||
|
write_bold(&mut writer, "{")?;
|
||||||
|
write!(writer, "{}", fields)?;
|
||||||
|
write_bold(&mut writer, "}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
write!(writer, "\x1b[2m:\x1b[0m")?;
|
||||||
|
} else {
|
||||||
|
writer.write_char(':')?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if saw_any {
|
||||||
|
writer.write_char(' ')?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Target (dimmed), then a space
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
write!(writer, "\x1b[2m{}\x1b[0m\x1b[2m:\x1b[0m ", meta.target())?;
|
||||||
|
} else {
|
||||||
|
write!(writer, "{}: ", meta.target())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Event fields
|
||||||
|
ctx.format_fields(writer.by_ref(), event)?;
|
||||||
|
|
||||||
|
// 7) Newline
|
||||||
writeln!(writer)
|
writeln!(writer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write the verbosity level with the same coloring/alignment as the Full formatter.
|
||||||
|
fn write_colored_level(writer: &mut Writer<'_>, level: &Level) -> fmt::Result {
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
// Basic ANSI color sequences; reset with \x1b[0m
|
||||||
|
let (color, text) = match *level {
|
||||||
|
Level::TRACE => ("\x1b[35m", "TRACE"), // purple
|
||||||
|
Level::DEBUG => ("\x1b[34m", "DEBUG"), // blue
|
||||||
|
Level::INFO => ("\x1b[32m", " INFO"), // green, note leading space
|
||||||
|
Level::WARN => ("\x1b[33m", " WARN"), // yellow, note leading space
|
||||||
|
Level::ERROR => ("\x1b[31m", "ERROR"), // red
|
||||||
|
};
|
||||||
|
write!(writer, "{}{}\x1b[0m", color, text)
|
||||||
|
} else {
|
||||||
|
// Right-pad to width 5 like Full's non-ANSI mode
|
||||||
|
match *level {
|
||||||
|
Level::TRACE => write!(writer, "{:>5}", "TRACE"),
|
||||||
|
Level::DEBUG => write!(writer, "{:>5}", "DEBUG"),
|
||||||
|
Level::INFO => write!(writer, "{:>5}", " INFO"),
|
||||||
|
Level::WARN => write!(writer, "{:>5}", " WARN"),
|
||||||
|
Level::ERROR => write!(writer, "{:>5}", "ERROR"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_dimmed(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result {
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
write!(writer, "\x1b[2m{}\x1b[0m", s)
|
||||||
|
} else {
|
||||||
|
write!(writer, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_bold(writer: &mut Writer<'_>, s: impl fmt::Display) -> fmt::Result {
|
||||||
|
if writer.has_ansi_escapes() {
|
||||||
|
write!(writer, "\x1b[1m{}\x1b[0m", s)
|
||||||
|
} else {
|
||||||
|
write!(writer, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Increment the global tick counter by 1
|
/// Increment the global tick counter by 1
|
||||||
///
|
///
|
||||||
/// This should be called once per game tick/frame from the main game loop
|
/// This should be called once per game tick/frame from the main game loop
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ impl Game {
|
|||||||
world.insert_resource(SystemTimings::default());
|
world.insert_resource(SystemTimings::default());
|
||||||
world.insert_resource(Timing::default());
|
world.insert_resource(Timing::default());
|
||||||
world.insert_resource(Bindings::default());
|
world.insert_resource(Bindings::default());
|
||||||
world.insert_resource(DeltaTime(0f32));
|
world.insert_resource(DeltaTime { seconds: 0.0, ticks: 0 });
|
||||||
world.insert_resource(RenderDirty::default());
|
world.insert_resource(RenderDirty::default());
|
||||||
world.insert_resource(DebugState::default());
|
world.insert_resource(DebugState::default());
|
||||||
world.insert_resource(AudioState::default());
|
world.insert_resource(AudioState::default());
|
||||||
@@ -633,7 +633,7 @@ impl Game {
|
|||||||
///
|
///
|
||||||
/// `true` if the game should terminate (exit command received), `false` to continue
|
/// `true` if the game should terminate (exit command received), `false` to continue
|
||||||
pub fn tick(&mut self, dt: f32) -> bool {
|
pub fn tick(&mut self, dt: f32) -> bool {
|
||||||
self.world.insert_resource(DeltaTime(dt));
|
self.world.insert_resource(DeltaTime { seconds: dt, ticks: 1 });
|
||||||
|
|
||||||
// Note: We don't need to read the current tick here since we increment it after running systems
|
// Note: We don't need to read the current tick here since we increment it after running systems
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,24 @@ use crate::systems::{
|
|||||||
|
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct Blinking {
|
pub struct Blinking {
|
||||||
pub timer: f32,
|
pub tick_timer: u32,
|
||||||
pub interval: f32,
|
pub interval_ticks: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Blinking {
|
impl Blinking {
|
||||||
pub fn new(interval: f32) -> Self {
|
pub fn new(interval_ticks: u32) -> Self {
|
||||||
Self { timer: 0.0, interval }
|
Self {
|
||||||
|
tick_timer: 0,
|
||||||
|
interval_ticks,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates blinking entities by toggling their visibility at regular intervals.
|
/// Updates blinking entities by toggling their visibility at regular intervals.
|
||||||
///
|
///
|
||||||
/// This system manages entities that have both `Blinking` and `Renderable` components,
|
/// This system manages entities that have both `Blinking` and `Renderable` components,
|
||||||
/// accumulating time and toggling visibility when the specified interval is reached.
|
/// accumulating ticks and toggling visibility when the specified interval is reached.
|
||||||
|
/// Uses integer arithmetic for deterministic behavior.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn blinking_system(
|
pub fn blinking_system(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
@@ -42,22 +46,40 @@ pub fn blinking_system(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increase the timer by the delta time
|
// Increase the timer by the delta ticks
|
||||||
blinking.timer += time.0;
|
blinking.tick_timer += time.ticks;
|
||||||
|
|
||||||
// If the timer is less than the interval, there's nothing to do yet
|
// Handle zero interval case (immediate toggling)
|
||||||
if blinking.timer < blinking.interval {
|
if blinking.interval_ticks == 0 {
|
||||||
continue;
|
if time.ticks > 0 {
|
||||||
}
|
|
||||||
|
|
||||||
// Subtract the interval (allows for the timer to retain partial interval progress)
|
|
||||||
blinking.timer -= blinking.interval;
|
|
||||||
|
|
||||||
// Toggle the Hidden component
|
|
||||||
if hidden {
|
if hidden {
|
||||||
commands.entity(entity).remove::<Hidden>();
|
commands.entity(entity).remove::<Hidden>();
|
||||||
} else {
|
} else {
|
||||||
commands.entity(entity).insert(Hidden);
|
commands.entity(entity).insert(Hidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate how many complete intervals have passed
|
||||||
|
let complete_intervals = blinking.tick_timer / blinking.interval_ticks;
|
||||||
|
|
||||||
|
// If no complete intervals have passed, there's nothing to do yet
|
||||||
|
if complete_intervals == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the timer to the remainder after complete intervals
|
||||||
|
blinking.tick_timer %= blinking.interval_ticks;
|
||||||
|
|
||||||
|
// Toggle the Hidden component for each complete interval
|
||||||
|
// Since toggling twice is a no-op, we only need to toggle if the count is odd
|
||||||
|
if complete_intervals % 2 == 1 {
|
||||||
|
if hidden {
|
||||||
|
commands.entity(entity).remove::<Hidden>();
|
||||||
|
} else {
|
||||||
|
commands.entity(entity).insert(Hidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,35 @@ pub struct GlobalState {
|
|||||||
pub struct ScoreResource(pub u32);
|
pub struct ScoreResource(pub u32);
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct DeltaTime(pub f32);
|
pub struct DeltaTime {
|
||||||
|
/// Floating-point delta time in seconds
|
||||||
|
pub seconds: f32,
|
||||||
|
/// Integer tick delta (usually 1, but can be different for testing)
|
||||||
|
pub ticks: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl DeltaTime {
|
||||||
|
/// Creates a new DeltaTime from a floating-point delta time in seconds
|
||||||
|
///
|
||||||
|
/// While this method exists as a helper, it does not mean that seconds and ticks are interchangeable.
|
||||||
|
pub fn from_seconds(seconds: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
seconds,
|
||||||
|
ticks: (seconds * 60.0).round() as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new DeltaTime from an integer tick delta
|
||||||
|
///
|
||||||
|
/// While this method exists as a helper, it does not mean that seconds and ticks are interchangeable.
|
||||||
|
pub fn from_ticks(ticks: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
seconds: ticks as f32 / 60.0,
|
||||||
|
ticks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Movement modifiers that can affect Pac-Man's speed or handling.
|
/// Movement modifiers that can affect Pac-Man's speed or handling.
|
||||||
#[derive(Component, Debug, Clone, Copy)]
|
#[derive(Component, Debug, Clone, Copy)]
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub fn ghost_movement_system(
|
|||||||
mut ghosts: Query<(&Ghost, &mut Velocity, &mut Position), Without<Frozen>>,
|
mut ghosts: Query<(&Ghost, &mut Velocity, &mut Position), Without<Frozen>>,
|
||||||
) {
|
) {
|
||||||
for (_ghost, mut velocity, mut position) in ghosts.iter_mut() {
|
for (_ghost, mut velocity, mut position) in ghosts.iter_mut() {
|
||||||
let mut distance = velocity.speed * 60.0 * delta_time.0;
|
let mut distance = velocity.speed * 60.0 * delta_time.seconds;
|
||||||
loop {
|
loop {
|
||||||
match *position {
|
match *position {
|
||||||
Position::Stopped { node: current_node } => {
|
Position::Stopped { node: current_node } => {
|
||||||
@@ -111,7 +111,7 @@ pub fn eaten_ghost_system(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Position::Moving { to, .. } => {
|
Position::Moving { to, .. } => {
|
||||||
let distance = velocity.speed * 60.0 * delta_time.0;
|
let distance = velocity.speed * 60.0 * delta_time.seconds;
|
||||||
if let Some(_overflow) = position.tick(distance) {
|
if let Some(_overflow) = position.tick(distance) {
|
||||||
// Reached target node, check if we're at ghost house center
|
// Reached target node, check if we're at ghost house center
|
||||||
if to == ghost_house_center {
|
if to == ghost_house_center {
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ pub fn input_system(
|
|||||||
// Update touch reference position with easing
|
// Update touch reference position with easing
|
||||||
if let Some(ref mut touch_data) = touch_state.active_touch {
|
if let Some(ref mut touch_data) = touch_state.active_touch {
|
||||||
// Apply easing to the reference position and get the delta for direction calculation
|
// Apply easing to the reference position and get the delta for direction calculation
|
||||||
let (delta, distance) = update_touch_reference_position(touch_data, delta_time.0);
|
let (delta, distance) = update_touch_reference_position(touch_data, delta_time.seconds);
|
||||||
|
|
||||||
// Check for direction based on updated reference position
|
// Check for direction based on updated reference position
|
||||||
if distance >= TOUCH_DIRECTION_THRESHOLD {
|
if distance >= TOUCH_DIRECTION_THRESHOLD {
|
||||||
@@ -336,7 +336,7 @@ pub fn input_system(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let (false, CursorPosition::Some { remaining_time, .. }) = (cursor_seen, &mut *cursor) {
|
if let (false, CursorPosition::Some { remaining_time, .. }) = (cursor_seen, &mut *cursor) {
|
||||||
*remaining_time -= delta_time.0;
|
*remaining_time -= delta_time.seconds;
|
||||||
if *remaining_time <= 0.0 {
|
if *remaining_time <= 0.0 {
|
||||||
*cursor = CursorPosition::None;
|
*cursor = CursorPosition::None;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,12 +99,12 @@ pub fn player_movement_system(
|
|||||||
} else {
|
} else {
|
||||||
*buffered_direction = BufferedDirection::Some {
|
*buffered_direction = BufferedDirection::Some {
|
||||||
direction,
|
direction,
|
||||||
remaining_time: remaining_time - delta_time.0,
|
remaining_time: remaining_time - delta_time.seconds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut distance = velocity.speed * modifiers.speed_multiplier * 60.0 * delta_time.0;
|
let mut distance = velocity.speed * modifiers.speed_multiplier * 60.0 * delta_time.seconds;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match *position {
|
match *position {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub fn directional_render_system(
|
|||||||
dt: Res<DeltaTime>,
|
dt: Res<DeltaTime>,
|
||||||
mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable)>,
|
mut query: Query<(&Position, &Velocity, &mut DirectionalAnimation, &mut Renderable)>,
|
||||||
) {
|
) {
|
||||||
let ticks = (dt.0 * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
|
let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
|
||||||
|
|
||||||
for (position, velocity, mut anim, mut renderable) in query.iter_mut() {
|
for (position, velocity, mut anim, mut renderable) in query.iter_mut() {
|
||||||
let stopped = matches!(position, Position::Stopped { .. });
|
let stopped = matches!(position, Position::Stopped { .. });
|
||||||
@@ -90,7 +90,7 @@ pub fn directional_render_system(
|
|||||||
///
|
///
|
||||||
/// This system handles entities that use LinearAnimation component for simple frame cycling.
|
/// This system handles entities that use LinearAnimation component for simple frame cycling.
|
||||||
pub fn linear_render_system(dt: Res<DeltaTime>, mut query: Query<(&mut LinearAnimation, &mut Renderable)>) {
|
pub fn linear_render_system(dt: Res<DeltaTime>, mut query: Query<(&mut LinearAnimation, &mut Renderable)>) {
|
||||||
let ticks = (dt.0 * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
|
let ticks = (dt.seconds * 60.0).round() as u16; // Convert from seconds to ticks at 60 ticks/sec
|
||||||
|
|
||||||
for (mut anim, mut renderable) in query.iter_mut() {
|
for (mut anim, mut renderable) in query.iter_mut() {
|
||||||
// Tick animation
|
// Tick animation
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
use crate::texture::sprite::AtlasTile;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct BlinkingTexture {
|
|
||||||
tile: AtlasTile,
|
|
||||||
blink_duration: f32,
|
|
||||||
time_bank: f32,
|
|
||||||
is_on: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlinkingTexture {
|
|
||||||
pub fn new(tile: AtlasTile, blink_duration: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
tile,
|
|
||||||
blink_duration,
|
|
||||||
time_bank: 0.0,
|
|
||||||
is_on: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&mut self, dt: f32) {
|
|
||||||
self.time_bank += dt;
|
|
||||||
if self.time_bank >= self.blink_duration {
|
|
||||||
self.time_bank -= self.blink_duration;
|
|
||||||
self.is_on = !self.is_on;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_on(&self) -> bool {
|
|
||||||
self.is_on
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tile(&self) -> &AtlasTile {
|
|
||||||
&self.tile
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods for testing
|
|
||||||
pub fn time_bank(&self) -> f32 {
|
|
||||||
self.time_bank
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blink_duration(&self) -> f32 {
|
|
||||||
self.blink_duration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod animated;
|
pub mod animated;
|
||||||
pub mod blinking;
|
|
||||||
pub mod sprite;
|
pub mod sprite;
|
||||||
pub mod sprites;
|
pub mod sprites;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
use pacman::texture::blinking::BlinkingTexture;
|
|
||||||
|
|
||||||
mod common;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_blinking_texture() {
|
|
||||||
let tile = common::mock_atlas_tile(1);
|
|
||||||
let mut texture = BlinkingTexture::new(tile, 0.5);
|
|
||||||
|
|
||||||
assert!(texture.is_on());
|
|
||||||
|
|
||||||
texture.tick(0.5);
|
|
||||||
assert!(!texture.is_on());
|
|
||||||
|
|
||||||
texture.tick(0.5);
|
|
||||||
assert!(texture.is_on());
|
|
||||||
|
|
||||||
texture.tick(0.5);
|
|
||||||
assert!(!texture.is_on());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_blinking_texture_partial_duration() {
|
|
||||||
let tile = common::mock_atlas_tile(1);
|
|
||||||
let mut texture = BlinkingTexture::new(tile, 0.5);
|
|
||||||
|
|
||||||
texture.tick(0.625);
|
|
||||||
assert!(!texture.is_on());
|
|
||||||
assert_eq!(texture.time_bank(), 0.125);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_blinking_texture_negative_time() {
|
|
||||||
let tile = common::mock_atlas_tile(1);
|
|
||||||
let mut texture = BlinkingTexture::new(tile, 0.5);
|
|
||||||
|
|
||||||
texture.tick(-0.1);
|
|
||||||
assert!(texture.is_on());
|
|
||||||
assert_eq!(texture.time_bank(), -0.1);
|
|
||||||
}
|
|
||||||
@@ -85,7 +85,10 @@ pub fn create_test_world() -> World {
|
|||||||
world.insert_resource(AudioState::default());
|
world.insert_resource(AudioState::default());
|
||||||
world.insert_resource(GlobalState { exit: false });
|
world.insert_resource(GlobalState { exit: false });
|
||||||
world.insert_resource(DebugState::default());
|
world.insert_resource(DebugState::default());
|
||||||
world.insert_resource(DeltaTime(1.0 / 60.0)); // 60 FPS
|
world.insert_resource(DeltaTime {
|
||||||
|
seconds: 1.0 / 60.0,
|
||||||
|
ticks: 1,
|
||||||
|
}); // 60 FPS
|
||||||
world.insert_resource(create_test_map());
|
world.insert_resource(create_test_map());
|
||||||
|
|
||||||
world
|
world
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use glam::I8Vec2;
|
|
||||||
use pacman::map::direction::*;
|
use pacman::map::direction::*;
|
||||||
|
use speculoos::prelude::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_direction_opposite() {
|
fn test_direction_opposite() {
|
||||||
@@ -11,21 +11,47 @@ fn test_direction_opposite() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (dir, expected) in test_cases {
|
for (dir, expected) in test_cases {
|
||||||
assert_eq!(dir.opposite(), expected);
|
assert_that(&dir.opposite()).is_equal_to(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_direction_as_ivec2() {
|
fn test_direction_opposite_symmetry() {
|
||||||
let test_cases = [
|
// Test that opposite() is symmetric: opposite(opposite(d)) == d
|
||||||
(Direction::Up, -I8Vec2::Y),
|
for &dir in &Direction::DIRECTIONS {
|
||||||
(Direction::Down, I8Vec2::Y),
|
assert_that(&dir.opposite().opposite()).is_equal_to(dir);
|
||||||
(Direction::Left, -I8Vec2::X),
|
|
||||||
(Direction::Right, I8Vec2::X),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (dir, expected) in test_cases {
|
|
||||||
assert_eq!(dir.as_ivec2(), expected);
|
|
||||||
assert_eq!(I8Vec2::from(dir), expected);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_direction_opposite_exhaustive() {
|
||||||
|
// Test that every direction has a unique opposite
|
||||||
|
let mut opposites = std::collections::HashSet::new();
|
||||||
|
for &dir in &Direction::DIRECTIONS {
|
||||||
|
let opposite = dir.opposite();
|
||||||
|
assert_that(&opposites.insert(opposite)).is_true();
|
||||||
|
}
|
||||||
|
assert_that(&opposites).has_length(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_direction_as_usize_exhaustive() {
|
||||||
|
// Test that as_usize() returns unique values for all directions
|
||||||
|
let mut usizes = std::collections::HashSet::new();
|
||||||
|
for &dir in &Direction::DIRECTIONS {
|
||||||
|
let usize_val = dir.as_usize();
|
||||||
|
assert_that(&usizes.insert(usize_val)).is_true();
|
||||||
|
}
|
||||||
|
assert_that(&usizes).has_length(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_direction_as_ivec2_exhaustive() {
|
||||||
|
// Test that as_ivec2() returns unique values for all directions
|
||||||
|
let mut ivec2s = std::collections::HashSet::new();
|
||||||
|
for &dir in &Direction::DIRECTIONS {
|
||||||
|
let ivec2_val = dir.as_ivec2();
|
||||||
|
assert_that(&ivec2s.insert(ivec2_val)).is_true();
|
||||||
|
}
|
||||||
|
assert_that(&ivec2s).has_length(4);
|
||||||
|
}
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ fn test_player_movement_system_buffered_direction_expires() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set delta time to expire the buffered direction
|
// Set delta time to expire the buffered direction
|
||||||
world.insert_resource(DeltaTime(0.02));
|
world.insert_resource(DeltaTime::from_seconds(0.02));
|
||||||
|
|
||||||
// Run the system
|
// Run the system
|
||||||
world
|
world
|
||||||
@@ -410,7 +410,7 @@ fn test_buffered_direction_timing() {
|
|||||||
.expect("System should run successfully");
|
.expect("System should run successfully");
|
||||||
|
|
||||||
// Run movement system multiple times with small delta times
|
// Run movement system multiple times with small delta times
|
||||||
world.insert_resource(DeltaTime(0.1)); // 0.1 seconds
|
world.insert_resource(DeltaTime::from_seconds(0.1)); // 0.1 seconds
|
||||||
|
|
||||||
// First run - buffered direction should still be active
|
// First run - buffered direction should still be active
|
||||||
world
|
world
|
||||||
@@ -428,7 +428,7 @@ fn test_buffered_direction_timing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run again to fully expire the buffered direction
|
// Run again to fully expire the buffered direction
|
||||||
world.insert_resource(DeltaTime(0.2)); // Total 0.3 seconds, should expire
|
world.insert_resource(DeltaTime::from_seconds(0.2)); // Total 0.3 seconds, should expire
|
||||||
world
|
world
|
||||||
.run_system_once(player_movement_system)
|
.run_system_once(player_movement_system)
|
||||||
.expect("System should run successfully");
|
.expect("System should run successfully");
|
||||||
|
|||||||
Reference in New Issue
Block a user