Compare commits

...

4 Commits

Author SHA1 Message Date
Ryan Walters
2bdb039aa9 fix: correct broken timing format tests 2025-09-01 12:57:48 -05:00
Ryan Walters
6dd0152938 chore: remove unused dependencies 2025-09-01 12:46:39 -05:00
Ryan Walters
4881e33c6f refactor: use U16Vec2 for sprites, remove unnecessary Deserialize trait 2025-09-01 12:44:13 -05:00
Ryan Walters
0cbd6f1aac refactor: switch NodeId to u16, use I8Vec2 for grid coordinates 2025-09-01 12:37:44 -05:00
17 changed files with 144 additions and 117 deletions

2
Cargo.lock generated
View File

@@ -598,11 +598,9 @@ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"circular-buffer", "circular-buffer",
"glam 0.30.5", "glam 0.30.5",
"lazy_static",
"libc", "libc",
"micromap", "micromap",
"num-width", "num-width",
"once_cell",
"parking_lot", "parking_lot",
"pathfinding", "pathfinding",
"phf", "phf",

View File

@@ -9,16 +9,13 @@ edition = "2021"
tracing = { version = "0.1.41", 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"
sdl2 = { version = "0.38.0", features = ["image", "ttf"] } sdl2 = { version = "0.38.0", features = ["image", "ttf"] }
spin_sleep = "1.3.2" 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"
thiserror = "2.0.14" thiserror = "2.0.14"
anyhow = "1.0" anyhow = "1.0"
glam = "0.30.5" glam = "0.30.5"
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"
@@ -71,3 +68,6 @@ libc = "0.2.175"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
phf = { version = "0.12.1", features = ["macros"] } phf = { version = "0.12.1", features = ["macros"] }
[package.metadata.cargo-machete]
ignored = ["phf"]

View File

@@ -19,6 +19,15 @@ struct MapperFrame {
height: u16, height: u16,
} }
impl MapperFrame {
fn to_u16vec2_format(self) -> String {
format!(
"MapperFrame {{ pos: glam::U16Vec2::new({}, {}), size: glam::U16Vec2::new({}, {}) }}",
self.x, self.y, self.width, self.height
)
}
}
fn main() { fn main() {
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("atlas_data.rs"); let path = Path::new(&env::var("OUT_DIR").unwrap()).join("atlas_data.rs");
let mut file = BufWriter::new(File::create(&path).unwrap()); let mut file = BufWriter::new(File::create(&path).unwrap());
@@ -37,12 +46,7 @@ fn main() {
.unwrap(); .unwrap();
for (name, frame) in atlas_mapper.frames { for (name, frame) in atlas_mapper.frames {
writeln!( writeln!(&mut file, " \"{}\" => {},", name, frame.to_u16vec2_format()).unwrap();
&mut file,
" \"{}\" => MapperFrame {{ x: {}, y: {}, width: {}, height: {} }},",
name, frame.x, frame.y, frame.width, frame.height
)
.unwrap();
} }
writeln!(&mut file, "}};").unwrap(); writeln!(&mut file, "}};").unwrap();

View File

@@ -11,7 +11,7 @@ use crate::systems::blinking::Blinking;
use crate::systems::movement::{BufferedDirection, Position, Velocity}; use crate::systems::movement::{BufferedDirection, Position, Velocity};
use crate::systems::profiling::SystemId; use crate::systems::profiling::SystemId;
use crate::systems::render::RenderDirty; use crate::systems::render::RenderDirty;
use crate::systems::{self, ghost_collision_system, present_system, Hidden, MovementModifiers}; use crate::systems::{self, ghost_collision_system, present_system, Hidden, MovementModifiers, NodeId};
use crate::systems::{ use crate::systems::{
audio_system, blinking_system, collision_system, debug_render_system, directional_render_system, dirty_render_system, audio_system, blinking_system, collision_system, debug_render_system, directional_render_system, dirty_render_system,
eaten_ghost_system, ghost_movement_system, ghost_state_animation_system, hud_render_system, item_system, profile, eaten_ghost_system, ghost_movement_system, ghost_state_animation_system, hud_render_system, item_system, profile,
@@ -303,7 +303,7 @@ impl Game {
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/energizer.png".to_string())))?; .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/energizer.png".to_string())))?;
// Build a list of item entities to spawn from the map // Build a list of item entities to spawn from the map
let nodes: Vec<(usize, EntityType, AtlasTile, f32)> = world let nodes: Vec<(NodeId, EntityType, AtlasTile, f32)> = world
.resource::<Map>() .resource::<Map>()
.iter_nodes() .iter_nodes()
.filter_map(|(id, tile)| match tile { .filter_map(|(id, tile)| match tile {

View File

@@ -5,7 +5,7 @@ use crate::map::graph::{Graph, Node, TraversalFlags};
use crate::map::parser::MapTileParser; use crate::map::parser::MapTileParser;
use crate::systems::movement::NodeId; use crate::systems::movement::NodeId;
use bevy_ecs::resource::Resource; use bevy_ecs::resource::Resource;
use glam::{IVec2, Vec2}; use glam::{I8Vec2, IVec2, Vec2};
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use tracing::debug; use tracing::debug;
@@ -38,7 +38,7 @@ pub struct Map {
/// Connected graph of navigable positions. /// Connected graph of navigable positions.
pub graph: Graph, pub graph: Graph,
/// Bidirectional mapping between 2D grid coordinates and graph node indices. /// Bidirectional mapping between 2D grid coordinates and graph node indices.
pub grid_to_node: HashMap<IVec2, NodeId>, pub grid_to_node: HashMap<I8Vec2, NodeId>,
/// Predetermined spawn locations for all game entities /// Predetermined spawn locations for all game entities
pub start_positions: NodePositions, pub start_positions: NodePositions,
/// 2D array of tile types for collision detection and rendering /// 2D array of tile types for collision detection and rendering
@@ -76,8 +76,8 @@ impl Map {
let mut queue = VecDeque::new(); let mut queue = VecDeque::new();
queue.push_back(start_pos); queue.push_back(start_pos);
let pos = Vec2::new( let pos = Vec2::new(
(start_pos.x * CELL_SIZE as i32) as f32, (start_pos.x as i32 * CELL_SIZE as i32) as f32,
(start_pos.y * CELL_SIZE as i32) as f32, (start_pos.y as i32 * CELL_SIZE as i32) as f32,
) + cell_offset; ) + cell_offset;
let node_id = graph.add_node(Node { position: pos }); let node_id = graph.add_node(Node { position: pos });
grid_to_node.insert(start_pos, node_id); grid_to_node.insert(start_pos, node_id);
@@ -89,9 +89,9 @@ impl Map {
// Skip if the new position is out of bounds // Skip if the new position is out of bounds
if new_position.x < 0 if new_position.x < 0
|| new_position.x >= BOARD_CELL_SIZE.x as i32 || new_position.x as i32 >= BOARD_CELL_SIZE.x as i32
|| new_position.y < 0 || new_position.y < 0
|| new_position.y >= BOARD_CELL_SIZE.y as i32 || new_position.y as i32 >= BOARD_CELL_SIZE.y as i32
{ {
continue; continue;
} }
@@ -108,8 +108,8 @@ impl Map {
) { ) {
// Add the new position to the graph/queue // Add the new position to the graph/queue
let pos = Vec2::new( let pos = Vec2::new(
(new_position.x * CELL_SIZE as i32) as f32, (new_position.x as i32 * CELL_SIZE as i32) as f32,
(new_position.y * CELL_SIZE as i32) as f32, (new_position.y as i32 * CELL_SIZE as i32) as f32,
) + cell_offset; ) + cell_offset;
let new_node_id = graph.add_node(Node { position: pos }); let new_node_id = graph.add_node(Node { position: pos });
grid_to_node.insert(new_position, new_node_id); grid_to_node.insert(new_position, new_node_id);
@@ -132,7 +132,7 @@ impl Map {
for (grid_pos, &node_id) in &grid_to_node { for (grid_pos, &node_id) in &grid_to_node {
for dir in Direction::DIRECTIONS { for dir in Direction::DIRECTIONS {
// If the node doesn't have an edge in this direction, look for a neighbor in that direction // If the node doesn't have an edge in this direction, look for a neighbor in that direction
if graph.adjacency_list[node_id].get(dir).is_none() { if graph.adjacency_list[node_id as usize].get(dir).is_none() {
let neighbor = grid_pos + dir.as_ivec2(); let neighbor = grid_pos + dir.as_ivec2();
// If the neighbor exists, connect the node to it // If the neighbor exists, connect the node to it
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) { if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
@@ -199,9 +199,9 @@ impl Map {
/// representing the four key positions within the ghost house structure. /// representing the four key positions within the ghost house structure.
fn build_house( fn build_house(
graph: &mut Graph, graph: &mut Graph,
grid_to_node: &HashMap<IVec2, NodeId>, grid_to_node: &HashMap<I8Vec2, NodeId>,
house_door: &[Option<IVec2>; 2], house_door: &[Option<I8Vec2>; 2],
) -> GameResult<(usize, usize, usize, usize)> { ) -> GameResult<(NodeId, NodeId, NodeId, NodeId)> {
// Calculate the position of the house entrance node // Calculate the position of the house entrance node
let (house_entrance_node_id, house_entrance_node_position) = { let (house_entrance_node_id, house_entrance_node_position) = {
// Translate the grid positions to the actual node ids // Translate the grid positions to the actual node ids
@@ -222,10 +222,13 @@ impl Map {
// Calculate the position of the house node // Calculate the position of the house node
let (node_id, node_position) = { let (node_id, node_position) = {
let left_pos = graph.get_node(*left_node).ok_or(MapError::NodeNotFound(*left_node))?.position; let left_pos = graph
.get_node(*left_node)
.ok_or(MapError::NodeNotFound(*left_node as usize))?
.position;
let right_pos = graph let right_pos = graph
.get_node(*right_node) .get_node(*right_node)
.ok_or(MapError::NodeNotFound(*right_node))? .ok_or(MapError::NodeNotFound(*right_node as usize))?
.position; .position;
let house_node = graph.add_node(Node { let house_node = graph.add_node(Node {
position: left_pos.lerp(right_pos, 0.5), position: left_pos.lerp(right_pos, 0.5),
@@ -249,10 +252,10 @@ impl Map {
// Place the nodes at, above, and below the center position // Place the nodes at, above, and below the center position
let center_node_id = graph.add_node(Node { position: center_pos }); let center_node_id = graph.add_node(Node { position: center_pos });
let top_node_id = graph.add_node(Node { let top_node_id = graph.add_node(Node {
position: center_pos + (Direction::Up.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(), position: center_pos + IVec2::from(Direction::Up.as_ivec2()).as_vec2() * (CELL_SIZE as f32 / 2.0),
}); });
let bottom_node_id = graph.add_node(Node { let bottom_node_id = graph.add_node(Node {
position: center_pos + (Direction::Down.as_ivec2() * (CELL_SIZE as i32 / 2)).as_vec2(), position: center_pos + IVec2::from(Direction::Down.as_ivec2()).as_vec2() * (CELL_SIZE as f32 / 2.0),
}); });
// Connect the center node to the top and bottom nodes // Connect the center node to the top and bottom nodes
@@ -268,7 +271,7 @@ impl Map {
// Calculate the position of the center line's center node // Calculate the position of the center line's center node
let center_line_center_position = let center_line_center_position =
house_entrance_node_position + (Direction::Down.as_ivec2() * (3 * CELL_SIZE as i32)).as_vec2(); house_entrance_node_position + IVec2::from(Direction::Down.as_ivec2()).as_vec2() * (3.0 * CELL_SIZE as f32);
// Create the center line // Create the center line
let (center_center_node_id, center_top_node_id) = create_house_line(graph, center_line_center_position)?; let (center_center_node_id, center_top_node_id) = create_house_line(graph, center_line_center_position)?;
@@ -300,13 +303,13 @@ impl Map {
// Create the left line // Create the left line
let (left_center_node_id, _) = create_house_line( let (left_center_node_id, _) = create_house_line(
graph, graph,
center_line_center_position + (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(), center_line_center_position + IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
)?; )?;
// Create the right line // Create the right line
let (right_center_node_id, _) = create_house_line( let (right_center_node_id, _) = create_house_line(
graph, graph,
center_line_center_position + (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(), center_line_center_position + IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
)?; )?;
debug!("Left center node id: {left_center_node_id}"); debug!("Left center node id: {left_center_node_id}");
@@ -336,8 +339,8 @@ impl Map {
/// Creates hidden intermediate nodes beyond the visible tunnel entrances and connects them with zero-distance edges for instantaneous traversal. /// Creates hidden intermediate nodes beyond the visible tunnel entrances and connects them with zero-distance edges for instantaneous traversal.
fn build_tunnels( fn build_tunnels(
graph: &mut Graph, graph: &mut Graph,
grid_to_node: &HashMap<IVec2, NodeId>, grid_to_node: &HashMap<I8Vec2, NodeId>,
tunnel_ends: &[Option<IVec2>; 2], tunnel_ends: &[Option<I8Vec2>; 2],
) -> GameResult<()> { ) -> GameResult<()> {
// Create the hidden tunnel nodes // Create the hidden tunnel nodes
let left_tunnel_hidden_node_id = { let left_tunnel_hidden_node_id = {
@@ -353,7 +356,7 @@ impl Map {
Direction::Left, Direction::Left,
Node { Node {
position: left_tunnel_entrance_node.position position: left_tunnel_entrance_node.position
+ (Direction::Left.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(), + IVec2::from(Direction::Left.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
}, },
) )
.map_err(|e| { .map_err(|e| {
@@ -378,7 +381,7 @@ impl Map {
Direction::Right, Direction::Right,
Node { Node {
position: right_tunnel_entrance_node.position position: right_tunnel_entrance_node.position
+ (Direction::Right.as_ivec2() * (CELL_SIZE as i32 * 2)).as_vec2(), + IVec2::from(Direction::Right.as_ivec2()).as_vec2() * (CELL_SIZE as f32 * 2.0),
}, },
) )
.map_err(|e| { .map_err(|e| {

View File

@@ -1,4 +1,4 @@
use glam::IVec2; use glam::I8Vec2;
use strum_macros::AsRefStr; use strum_macros::AsRefStr;
/// The four cardinal directions. /// The four cardinal directions.
@@ -28,8 +28,8 @@ impl Direction {
} }
} }
/// Returns the direction as an IVec2. /// Returns the direction as an I8Vec2.
pub fn as_ivec2(self) -> IVec2 { pub fn as_ivec2(self) -> I8Vec2 {
self.into() self.into()
} }
@@ -45,13 +45,13 @@ impl Direction {
} }
} }
impl From<Direction> for IVec2 { impl From<Direction> for I8Vec2 {
fn from(dir: Direction) -> Self { fn from(dir: Direction) -> Self {
match dir { match dir {
Direction::Up => -IVec2::Y, Direction::Up => -I8Vec2::Y,
Direction::Down => IVec2::Y, Direction::Down => I8Vec2::Y,
Direction::Left => -IVec2::X, Direction::Left => -I8Vec2::X,
Direction::Right => IVec2::X, Direction::Right => I8Vec2::X,
} }
} }
} }

View File

@@ -107,7 +107,7 @@ impl Graph {
/// Adds a new node with the given data to the graph and returns its ID. /// Adds a new node with the given data to the graph and returns its ID.
pub fn add_node(&mut self, data: Node) -> NodeId { pub fn add_node(&mut self, data: Node) -> NodeId {
let id = self.nodes.len(); let id = self.nodes.len() as NodeId;
self.nodes.push(data); self.nodes.push(data);
self.adjacency_list.push(Intersection::default()); self.adjacency_list.push(Intersection::default());
id id
@@ -129,10 +129,10 @@ impl Graph {
distance: Option<f32>, distance: Option<f32>,
direction: Direction, direction: Direction,
) -> Result<(), &'static str> { ) -> Result<(), &'static str> {
if from >= self.adjacency_list.len() { if from as usize >= self.adjacency_list.len() {
return Err("From node does not exist."); return Err("From node does not exist.");
} }
if to >= self.adjacency_list.len() { if to as usize >= self.adjacency_list.len() {
return Err("To node does not exist."); return Err("To node does not exist.");
} }
@@ -178,8 +178,8 @@ impl Graph {
} }
None => { None => {
// If no distance is provided, calculate it based on the positions of the nodes // If no distance is provided, calculate it based on the positions of the nodes
let from_pos = self.nodes[from].position; let from_pos = self.nodes[from as usize].position;
let to_pos = self.nodes[to].position; let to_pos = self.nodes[to as usize].position;
from_pos.distance(to_pos) from_pos.distance(to_pos)
} }
}, },
@@ -187,11 +187,11 @@ impl Graph {
traversal_flags, traversal_flags,
}; };
if from >= self.adjacency_list.len() { if from as usize >= self.adjacency_list.len() {
return Err("From node does not exist."); return Err("From node does not exist.");
} }
let adjacency_list = &mut self.adjacency_list[from]; let adjacency_list = &mut self.adjacency_list[from as usize];
// Check if the edge already exists in this direction or to the same target // Check if the edge already exists in this direction or to the same target
if let Some(err) = adjacency_list.edges().find_map(|e| { if let Some(err) = adjacency_list.edges().find_map(|e| {
@@ -215,7 +215,7 @@ impl Graph {
/// Retrieves an immutable reference to a node's data. /// Retrieves an immutable reference to a node's data.
pub fn get_node(&self, id: NodeId) -> Option<&Node> { pub fn get_node(&self, id: NodeId) -> Option<&Node> {
self.nodes.get(id) self.nodes.get(id as usize)
} }
/// Returns an iterator over all nodes in the graph. /// Returns an iterator over all nodes in the graph.
@@ -228,17 +228,17 @@ impl Graph {
self.adjacency_list self.adjacency_list
.iter() .iter()
.enumerate() .enumerate()
.flat_map(|(node_id, intersection)| intersection.edges().map(move |edge| (node_id, edge))) .flat_map(|(node_id, intersection)| intersection.edges().map(move |edge| (node_id as NodeId, 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 as usize)?.edges().find(|edge| edge.target == to)
} }
/// Finds an edge originating from a given node that follows a specific direction. /// Finds an edge originating from a given node that follows a specific direction.
pub fn find_edge_in_direction(&self, from: NodeId, direction: Direction) -> Option<Edge> { pub fn find_edge_in_direction(&self, from: NodeId, direction: Direction) -> Option<Edge> {
self.adjacency_list.get(from)?.get(direction) self.adjacency_list.get(from as usize)?.get(direction)
} }
} }

View File

@@ -2,7 +2,7 @@
use crate::constants::{MapTile, BOARD_CELL_SIZE}; use crate::constants::{MapTile, BOARD_CELL_SIZE};
use crate::error::ParseError; use crate::error::ParseError;
use glam::IVec2; use glam::I8Vec2;
/// Structured representation of parsed ASCII board layout with extracted special positions. /// Structured representation of parsed ASCII board layout with extracted special positions.
/// ///
@@ -15,11 +15,11 @@ pub struct ParsedMap {
/// 2D array of tiles converted from ASCII characters /// 2D array of tiles converted from ASCII characters
pub tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize], pub tiles: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
/// Two positions marking the ghost house entrance (represented by '=' characters) /// Two positions marking the ghost house entrance (represented by '=' characters)
pub house_door: [Option<IVec2>; 2], pub house_door: [Option<I8Vec2>; 2],
/// Two positions marking tunnel portals for wraparound teleportation ('T' characters) /// Two positions marking tunnel portals for wraparound teleportation ('T' characters)
pub tunnel_ends: [Option<IVec2>; 2], pub tunnel_ends: [Option<I8Vec2>; 2],
/// Starting position for Pac-Man (marked by 'X' character in the layout) /// Starting position for Pac-Man (marked by 'X' character in the layout)
pub pacman_start: Option<IVec2>, pub pacman_start: Option<I8Vec2>,
} }
/// Parser for converting raw board layouts into structured map data. /// Parser for converting raw board layouts into structured map data.
@@ -88,7 +88,7 @@ impl MapTileParser {
let mut tiles = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize]; let mut tiles = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize];
let mut house_door = [None; 2]; let mut house_door = [None; 2];
let mut tunnel_ends = [None; 2]; let mut tunnel_ends = [None; 2];
let mut pacman_start: Option<IVec2> = None; let mut pacman_start: Option<I8Vec2> = None;
for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) { for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) {
for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) { for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) {
@@ -98,16 +98,16 @@ impl MapTileParser {
match tile { match tile {
MapTile::Tunnel => { MapTile::Tunnel => {
if tunnel_ends[0].is_none() { if tunnel_ends[0].is_none() {
tunnel_ends[0] = Some(IVec2::new(x as i32, y as i32)); tunnel_ends[0] = Some(I8Vec2::new(x as i8, y as i8));
} else { } else {
tunnel_ends[1] = Some(IVec2::new(x as i32, y as i32)); tunnel_ends[1] = Some(I8Vec2::new(x as i8, y as i8));
} }
} }
MapTile::Wall if character == '=' => { MapTile::Wall if character == '=' => {
if house_door[0].is_none() { if house_door[0].is_none() {
house_door[0] = Some(IVec2::new(x as i32, y as i32)); house_door[0] = Some(I8Vec2::new(x as i8, y as i8));
} else { } else {
house_door[1] = Some(IVec2::new(x as i32, y as i32)); house_door[1] = Some(I8Vec2::new(x as i8, y as i8));
} }
} }
_ => {} _ => {}
@@ -115,7 +115,7 @@ impl MapTileParser {
// Track Pac-Man's starting position // Track Pac-Man's starting position
if character == 'X' { if character == 'X' {
pacman_start = Some(IVec2::new(x as i32, y as i32)); pacman_start = Some(I8Vec2::new(x as i8, y as i8));
} }
tiles[x][y] = tile; tiles[x][y] = tile;

View File

@@ -3,7 +3,7 @@ use std::cmp::Ordering;
use crate::constants::BOARD_PIXEL_OFFSET; use crate::constants::BOARD_PIXEL_OFFSET;
use crate::map::builder::Map; use crate::map::builder::Map;
use crate::systems::{Collider, CursorPosition, Position, SystemTimings}; use crate::systems::{Collider, CursorPosition, NodeId, Position, SystemTimings};
use bevy_ecs::resource::Resource; use bevy_ecs::resource::Resource;
use bevy_ecs::system::{NonSendMut, Query, Res}; use bevy_ecs::system::{NonSendMut, Query, Res};
use glam::{IVec2, UVec2, Vec2}; use glam::{IVec2, UVec2, Vec2};
@@ -185,7 +185,7 @@ pub fn debug_render_system(
// Render node ID if a node is highlighted // Render node ID if a node is highlighted
if let Some(closest_node_id) = closest_node { if let Some(closest_node_id) = closest_node {
let node = map.graph.get_node(closest_node_id).unwrap(); let node = map.graph.get_node(closest_node_id as NodeId).unwrap();
let pos = transform_position_with_offset(node.position, scale); let pos = transform_position_with_offset(node.position, scale);
let surface = font let surface = font

View File

@@ -36,7 +36,7 @@ pub fn ghost_movement_system(
loop { loop {
match *position { match *position {
Position::Stopped { node: current_node } => { Position::Stopped { node: current_node } => {
let intersection = &map.graph.adjacency_list[current_node]; let intersection = &map.graph.adjacency_list[current_node as usize];
let opposite = velocity.direction.opposite(); let opposite = velocity.direction.opposite();
let mut non_opposite_options: SmallVec<[Edge; 3]> = SmallVec::new(); let mut non_opposite_options: SmallVec<[Edge; 3]> = SmallVec::new();
@@ -159,8 +159,11 @@ pub fn eaten_ghost_system(
velocity.direction = direction; velocity.direction = direction;
*position = Position::Moving { *position = Position::Moving {
from: current_node, from: current_node,
to: map.graph.adjacency_list[current_node].get(direction).unwrap().target, to: map.graph.adjacency_list[current_node as usize].get(direction).unwrap().target,
remaining_distance: map.graph.adjacency_list[current_node].get(direction).unwrap().distance, remaining_distance: map.graph.adjacency_list[current_node as usize]
.get(direction)
.unwrap()
.distance,
}; };
} }
} }
@@ -186,8 +189,8 @@ pub fn eaten_ghost_system(
velocity.direction = next_direction; velocity.direction = next_direction;
*position = Position::Moving { *position = Position::Moving {
from: to, from: to,
to: map.graph.adjacency_list[to].get(next_direction).unwrap().target, to: map.graph.adjacency_list[to as usize].get(next_direction).unwrap().target,
remaining_distance: map.graph.adjacency_list[to].get(next_direction).unwrap().distance, remaining_distance: map.graph.adjacency_list[to as usize].get(next_direction).unwrap().distance,
}; };
} }
} }
@@ -202,7 +205,11 @@ pub fn eaten_ghost_system(
/// Helper function to find the direction from a node towards a target node. /// Helper function to find the direction from a node towards a target node.
/// Uses simple greedy pathfinding - prefers straight lines when possible. /// Uses simple greedy pathfinding - prefers straight lines when possible.
fn find_direction_to_target(map: &Map, from_node: usize, target_node: usize) -> Option<Direction> { fn find_direction_to_target(
map: &Map,
from_node: crate::systems::movement::NodeId,
target_node: crate::systems::movement::NodeId,
) -> Option<Direction> {
let from_pos = map.graph.get_node(from_node).unwrap().position; let from_pos = map.graph.get_node(from_node).unwrap().position;
let target_pos = map.graph.get_node(target_node).unwrap().position; let target_pos = map.graph.get_node(target_node).unwrap().position;
@@ -224,7 +231,7 @@ fn find_direction_to_target(map: &Map, from_node: usize, target_node: usize) ->
// Return first available direction towards target // Return first available direction towards target
for direction in preferred_dirs { for direction in preferred_dirs {
if let Some(edge) = map.graph.adjacency_list[from_node].get(direction) { if let Some(edge) = map.graph.adjacency_list[from_node as usize].get(direction) {
if edge.traversal_flags.contains(TraversalFlags::GHOST) { if edge.traversal_flags.contains(TraversalFlags::GHOST) {
return Some(direction); return Some(direction);
} }

View File

@@ -8,7 +8,7 @@ use glam::Vec2;
/// ///
/// Nodes represent discrete movement targets in the maze. The index directly corresponds to the node's position in the /// Nodes represent discrete movement targets in the maze. The index directly corresponds to the node's position in the
/// graph's internal storage arrays. /// graph's internal storage arrays.
pub type NodeId = usize; pub type NodeId = u16;
/// A component that represents the speed and cardinal direction of an entity. /// A component that represents the speed and cardinal direction of an entity.
/// Speed is static, only applied when the entity has an edge to traverse. /// Speed is static, only applied when the entity has an edge to traverse.
@@ -57,7 +57,7 @@ impl Position {
let pos = match &self { let pos = match &self {
Position::Stopped { node } => { Position::Stopped { node } => {
// Entity is stationary at a node // Entity is stationary at a node
let node = graph.get_node(*node).ok_or(EntityError::NodeNotFound(*node))?; let node = graph.get_node(*node).ok_or(EntityError::NodeNotFound(*node as usize))?;
node.position node.position
} }
Position::Moving { Position::Moving {
@@ -66,11 +66,12 @@ impl Position {
remaining_distance, remaining_distance,
} => { } => {
// Entity is traveling between nodes // Entity is traveling between nodes
let from_node = graph.get_node(*from).ok_or(EntityError::NodeNotFound(*from))?; let from_node = graph.get_node(*from).ok_or(EntityError::NodeNotFound(*from as usize))?;
let to_node = graph.get_node(*to).ok_or(EntityError::NodeNotFound(*to))?; let to_node = graph.get_node(*to).ok_or(EntityError::NodeNotFound(*to as usize))?;
let edge = graph let edge = graph.find_edge(*from, *to).ok_or(EntityError::EdgeNotFound {
.find_edge(*from, *to) from: *from as usize,
.ok_or(EntityError::EdgeNotFound { from: *from, to: *to })?; to: *to as usize,
})?;
// For zero-distance edges (tunnels), progress >= 1.0 means we're at the target // For zero-distance edges (tunnels), progress >= 1.0 means we're at the target
if edge.distance == 0.0 { if edge.distance == 0.0 {

View File

@@ -3,24 +3,21 @@ use glam::U16Vec2;
use sdl2::pixels::Color; use sdl2::pixels::Color;
use sdl2::rect::Rect; use sdl2::rect::Rect;
use sdl2::render::{Canvas, RenderTarget, Texture}; use sdl2::render::{Canvas, RenderTarget, Texture};
use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use crate::error::TextureError; use crate::error::TextureError;
/// Atlas frame mapping data loaded from JSON metadata files. /// Atlas frame mapping data loaded from JSON metadata files.
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug)]
pub struct AtlasMapper { pub struct AtlasMapper {
/// Mapping from sprite name to frame bounds within the atlas texture /// Mapping from sprite name to frame bounds within the atlas texture
pub frames: HashMap<String, MapperFrame>, pub frames: HashMap<String, MapperFrame>,
} }
#[derive(Copy, Clone, Debug, Deserialize)] #[derive(Copy, Clone, Debug)]
pub struct MapperFrame { pub struct MapperFrame {
pub x: u16, pub pos: U16Vec2,
pub y: u16, pub size: U16Vec2,
pub width: u16,
pub height: u16,
} }
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
@@ -108,8 +105,8 @@ impl SpriteAtlas {
/// for repeated use in animations and entity sprites. /// for repeated use in animations and entity sprites.
pub fn get_tile(&self, name: &str) -> Option<AtlasTile> { pub fn get_tile(&self, name: &str) -> Option<AtlasTile> {
self.tiles.get(name).map(|frame| AtlasTile { self.tiles.get(name).map(|frame| AtlasTile {
pos: U16Vec2::new(frame.x, frame.y), pos: frame.pos,
size: U16Vec2::new(frame.width, frame.height), size: frame.size,
color: None, color: None,
}) })
} }

View File

@@ -5,7 +5,8 @@ use pacman::{
events::GameEvent, events::GameEvent,
map::builder::Map, map::builder::Map,
systems::{ systems::{
check_collision, collision_system, Collider, EntityType, Ghost, GhostCollider, ItemCollider, PacmanCollider, Position, check_collision, collision_system, Collider, EntityType, Ghost, GhostCollider, ItemCollider, NodeId, PacmanCollider,
Position,
}, },
}; };
@@ -59,7 +60,7 @@ fn spawn_test_ghost(world: &mut World) -> Entity {
fn spawn_test_ghost_at_node(world: &mut World, node: usize) -> Entity { fn spawn_test_ghost_at_node(world: &mut World, node: usize) -> Entity {
world world
.spawn(( .spawn((
Position::Stopped { node }, Position::Stopped { node: node as NodeId },
Collider { size: 12.0 }, Collider { size: 12.0 },
GhostCollider, GhostCollider,
Ghost::Blinky, Ghost::Blinky,

View File

@@ -1,4 +1,4 @@
use glam::IVec2; use glam::I8Vec2;
use pacman::map::direction::*; use pacman::map::direction::*;
#[test] #[test]
@@ -18,14 +18,14 @@ fn test_direction_opposite() {
#[test] #[test]
fn test_direction_as_ivec2() { fn test_direction_as_ivec2() {
let test_cases = [ let test_cases = [
(Direction::Up, -IVec2::Y), (Direction::Up, -I8Vec2::Y),
(Direction::Down, IVec2::Y), (Direction::Down, I8Vec2::Y),
(Direction::Left, -IVec2::X), (Direction::Left, -I8Vec2::X),
(Direction::Right, IVec2::X), (Direction::Right, I8Vec2::X),
]; ];
for (dir, expected) in test_cases { for (dir, expected) in test_cases {
assert_eq!(dir.as_ivec2(), expected); assert_eq!(dir.as_ivec2(), expected);
assert_eq!(IVec2::from(dir), expected); assert_eq!(I8Vec2::from(dir), expected);
} }
} }

View File

@@ -26,8 +26,10 @@ fn test_map_node_positions() {
for (grid_pos, &node_id) in &map.grid_to_node { for (grid_pos, &node_id) in &map.grid_to_node {
let node = map.graph.get_node(node_id).unwrap(); let node = map.graph.get_node(node_id).unwrap();
let expected_pos = Vec2::new((grid_pos.x * CELL_SIZE as i32) as f32, (grid_pos.y * CELL_SIZE as i32) as f32) let expected_pos = Vec2::new(
+ Vec2::splat(CELL_SIZE as f32 / 2.0); (grid_pos.x as i32 * CELL_SIZE as i32) as f32,
(grid_pos.y as i32 * CELL_SIZE as i32) as f32,
) + Vec2::splat(CELL_SIZE as f32 / 2.0);
assert_eq!(node.position, expected_pos); assert_eq!(node.position, expected_pos);
} }

View File

@@ -5,21 +5,41 @@ use std::time::Duration;
fn test_timing_statistics() { fn test_timing_statistics() {
let timings = SystemTimings::default(); let timings = SystemTimings::default();
// Add some test data // 10ms average, 2ms std dev
timings.add_timing(SystemId::PlayerControls, Duration::from_millis(10)); timings.add_timing(SystemId::PlayerControls, Duration::from_millis(10));
timings.add_timing(SystemId::PlayerControls, Duration::from_millis(12)); timings.add_timing(SystemId::PlayerControls, Duration::from_millis(12));
timings.add_timing(SystemId::PlayerControls, Duration::from_millis(8)); timings.add_timing(SystemId::PlayerControls, Duration::from_millis(8));
// 2ms average, 1ms std dev
timings.add_timing(SystemId::Blinking, Duration::from_millis(3));
timings.add_timing(SystemId::Blinking, Duration::from_millis(2));
timings.add_timing(SystemId::Blinking, Duration::from_millis(1));
fn close_enough(a: Duration, b: Duration) -> bool {
if a > b {
a - b < Duration::from_micros(500) // 0.1ms
} else {
b - a < Duration::from_micros(500)
}
}
let stats = timings.get_stats(); let stats = timings.get_stats();
let (avg, std_dev) = stats.get(&SystemId::PlayerControls).unwrap(); let (avg, std_dev) = stats.get(&SystemId::PlayerControls).unwrap();
// Average should be 10ms, standard deviation should be small // Average should be 10ms, standard deviation should be small
assert!((avg.as_millis() as f64 - 10.0).abs() < 1.0); assert!(close_enough(*avg, Duration::from_millis(10)), "avg: {:?}", avg);
assert!(std_dev.as_millis() > 0); assert!(close_enough(*std_dev, Duration::from_millis(2)), "std_dev: {:?}", std_dev);
let (total_avg, total_std) = timings.get_total_stats(); let (total_avg, total_std) = timings.get_total_stats();
assert!((total_avg.as_millis() as f64 - 10.0).abs() < 1.0); assert!(
assert!(total_std.as_millis() > 0); close_enough(total_avg, Duration::from_millis(18)),
"total_avg: {:?}",
total_avg
);
assert!(
close_enough(total_std, Duration::from_millis(12)),
"total_std: {:?}",
total_std
);
} }
// #[test] // #[test]

View File

@@ -13,10 +13,8 @@ fn test_sprite_atlas_basic() {
frames.insert( frames.insert(
"test".to_string(), "test".to_string(),
MapperFrame { MapperFrame {
x: 10, pos: U16Vec2::new(10, 20),
y: 20, size: U16Vec2::new(32, 64),
width: 32,
height: 64,
}, },
); );
@@ -38,19 +36,15 @@ fn test_sprite_atlas_multiple_tiles() {
frames.insert( frames.insert(
"tile1".to_string(), "tile1".to_string(),
MapperFrame { MapperFrame {
x: 0, pos: U16Vec2::new(0, 0),
y: 0, size: U16Vec2::new(32, 32),
width: 32,
height: 32,
}, },
); );
frames.insert( frames.insert(
"tile2".to_string(), "tile2".to_string(),
MapperFrame { MapperFrame {
x: 32, pos: U16Vec2::new(32, 0),
y: 0, size: U16Vec2::new(64, 64),
width: 64,
height: 64,
}, },
); );