Compare commits

..

5 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
Ryan Walters
1206cf9ad1 feat: implement high score text rendering 2025-09-01 12:13:18 -05:00
18 changed files with 153 additions and 118 deletions

2
Cargo.lock generated
View File

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

View File

@@ -9,16 +9,13 @@ edition = "2021"
tracing = { version = "0.1.41", features = ["max_level_debug", "release_max_level_debug"]}
tracing-error = "0.2.0"
tracing-subscriber = {version = "0.3.17", features = ["env-filter"]}
lazy_static = "1.5.0"
sdl2 = { version = "0.38.0", features = ["image", "ttf"] }
spin_sleep = "1.3.2"
rand = { version = "0.9.2", default-features = false, features = ["small_rng", "os_rng"] }
pathfinding = "4.14"
once_cell = "1.21.3"
thiserror = "2.0.14"
anyhow = "1.0"
glam = "0.30.5"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.142"
smallvec = "1.15.1"
strum = "0.27.2"
@@ -71,3 +68,6 @@ libc = "0.2.175"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
phf = { version = "0.12.1", features = ["macros"] }
[package.metadata.cargo-machete]
ignored = ["phf"]

View File

@@ -19,6 +19,15 @@ struct MapperFrame {
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() {
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("atlas_data.rs");
let mut file = BufWriter::new(File::create(&path).unwrap());
@@ -37,12 +46,7 @@ fn main() {
.unwrap();
for (name, frame) in atlas_mapper.frames {
writeln!(
&mut file,
" \"{}\" => MapperFrame {{ x: {}, y: {}, width: {}, height: {} }},",
name, frame.x, frame.y, frame.width, frame.height
)
.unwrap();
writeln!(&mut file, " \"{}\" => {},", name, frame.to_u16vec2_format()).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::profiling::SystemId;
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::{
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,
@@ -303,7 +303,7 @@ impl Game {
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/energizer.png".to_string())))?;
// 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>()
.iter_nodes()
.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::systems::movement::NodeId;
use bevy_ecs::resource::Resource;
use glam::{IVec2, Vec2};
use glam::{I8Vec2, IVec2, Vec2};
use std::collections::{HashMap, VecDeque};
use tracing::debug;
@@ -38,7 +38,7 @@ pub struct Map {
/// Connected graph of navigable positions.
pub graph: Graph,
/// 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
pub start_positions: NodePositions,
/// 2D array of tile types for collision detection and rendering
@@ -76,8 +76,8 @@ impl Map {
let mut queue = VecDeque::new();
queue.push_back(start_pos);
let pos = Vec2::new(
(start_pos.x * CELL_SIZE as i32) as f32,
(start_pos.y * CELL_SIZE as i32) as f32,
(start_pos.x as i32 * CELL_SIZE as i32) as f32,
(start_pos.y as i32 * CELL_SIZE as i32) as f32,
) + cell_offset;
let node_id = graph.add_node(Node { position: pos });
grid_to_node.insert(start_pos, node_id);
@@ -89,9 +89,9 @@ impl Map {
// Skip if the new position is out of bounds
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 >= BOARD_CELL_SIZE.y as i32
|| new_position.y as i32 >= BOARD_CELL_SIZE.y as i32
{
continue;
}
@@ -108,8 +108,8 @@ impl Map {
) {
// Add the new position to the graph/queue
let pos = Vec2::new(
(new_position.x * CELL_SIZE as i32) as f32,
(new_position.y * CELL_SIZE as i32) as f32,
(new_position.x as i32 * CELL_SIZE as i32) as f32,
(new_position.y as i32 * CELL_SIZE as i32) as f32,
) + cell_offset;
let new_node_id = graph.add_node(Node { position: pos });
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 dir in Direction::DIRECTIONS {
// 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();
// If the neighbor exists, connect the node to it
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.
fn build_house(
graph: &mut Graph,
grid_to_node: &HashMap<IVec2, NodeId>,
house_door: &[Option<IVec2>; 2],
) -> GameResult<(usize, usize, usize, usize)> {
grid_to_node: &HashMap<I8Vec2, NodeId>,
house_door: &[Option<I8Vec2>; 2],
) -> GameResult<(NodeId, NodeId, NodeId, NodeId)> {
// Calculate the position of the house entrance node
let (house_entrance_node_id, house_entrance_node_position) = {
// Translate the grid positions to the actual node ids
@@ -222,10 +222,13 @@ impl Map {
// Calculate the position of the house node
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
.get_node(*right_node)
.ok_or(MapError::NodeNotFound(*right_node))?
.ok_or(MapError::NodeNotFound(*right_node as usize))?
.position;
let house_node = graph.add_node(Node {
position: left_pos.lerp(right_pos, 0.5),
@@ -249,10 +252,10 @@ impl Map {
// Place the nodes at, above, and below the center position
let center_node_id = graph.add_node(Node { position: center_pos });
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 {
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
@@ -268,7 +271,7 @@ impl Map {
// Calculate the position of the center line's center node
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
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
let (left_center_node_id, _) = create_house_line(
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
let (right_center_node_id, _) = create_house_line(
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}");
@@ -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.
fn build_tunnels(
graph: &mut Graph,
grid_to_node: &HashMap<IVec2, NodeId>,
tunnel_ends: &[Option<IVec2>; 2],
grid_to_node: &HashMap<I8Vec2, NodeId>,
tunnel_ends: &[Option<I8Vec2>; 2],
) -> GameResult<()> {
// Create the hidden tunnel nodes
let left_tunnel_hidden_node_id = {
@@ -353,7 +356,7 @@ impl Map {
Direction::Left,
Node {
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| {
@@ -378,7 +381,7 @@ impl Map {
Direction::Right,
Node {
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| {

View File

@@ -1,4 +1,4 @@
use glam::IVec2;
use glam::I8Vec2;
use strum_macros::AsRefStr;
/// The four cardinal directions.
@@ -28,8 +28,8 @@ impl Direction {
}
}
/// Returns the direction as an IVec2.
pub fn as_ivec2(self) -> IVec2 {
/// Returns the direction as an I8Vec2.
pub fn as_ivec2(self) -> I8Vec2 {
self.into()
}
@@ -45,13 +45,13 @@ impl Direction {
}
}
impl From<Direction> for IVec2 {
impl From<Direction> for I8Vec2 {
fn from(dir: Direction) -> Self {
match dir {
Direction::Up => -IVec2::Y,
Direction::Down => IVec2::Y,
Direction::Left => -IVec2::X,
Direction::Right => IVec2::X,
Direction::Up => -I8Vec2::Y,
Direction::Down => I8Vec2::Y,
Direction::Left => -I8Vec2::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.
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.adjacency_list.push(Intersection::default());
id
@@ -129,10 +129,10 @@ impl Graph {
distance: Option<f32>,
direction: Direction,
) -> Result<(), &'static str> {
if from >= self.adjacency_list.len() {
if from as usize >= self.adjacency_list.len() {
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.");
}
@@ -178,8 +178,8 @@ impl Graph {
}
None => {
// If no distance is provided, calculate it based on the positions of the nodes
let from_pos = self.nodes[from].position;
let to_pos = self.nodes[to].position;
let from_pos = self.nodes[from as usize].position;
let to_pos = self.nodes[to as usize].position;
from_pos.distance(to_pos)
}
},
@@ -187,11 +187,11 @@ impl Graph {
traversal_flags,
};
if from >= self.adjacency_list.len() {
if from as usize >= self.adjacency_list.len() {
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
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.
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.
@@ -228,17 +228,17 @@ impl Graph {
self.adjacency_list
.iter()
.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.
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.
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::error::ParseError;
use glam::IVec2;
use glam::I8Vec2;
/// 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
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)
pub house_door: [Option<IVec2>; 2],
pub house_door: [Option<I8Vec2>; 2],
/// 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)
pub pacman_start: Option<IVec2>,
pub pacman_start: Option<I8Vec2>,
}
/// 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 house_door = [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 (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) {
@@ -98,16 +98,16 @@ impl MapTileParser {
match tile {
MapTile::Tunnel => {
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 {
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 == '=' => {
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 {
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
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;

View File

@@ -3,7 +3,7 @@ use std::cmp::Ordering;
use crate::constants::BOARD_PIXEL_OFFSET;
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::system::{NonSendMut, Query, Res};
use glam::{IVec2, UVec2, Vec2};
@@ -185,7 +185,7 @@ pub fn debug_render_system(
// Render node ID if a node is highlighted
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 surface = font

View File

@@ -36,7 +36,7 @@ pub fn ghost_movement_system(
loop {
match *position {
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 mut non_opposite_options: SmallVec<[Edge; 3]> = SmallVec::new();
@@ -159,8 +159,11 @@ pub fn eaten_ghost_system(
velocity.direction = direction;
*position = Position::Moving {
from: current_node,
to: map.graph.adjacency_list[current_node].get(direction).unwrap().target,
remaining_distance: map.graph.adjacency_list[current_node].get(direction).unwrap().distance,
to: map.graph.adjacency_list[current_node as usize].get(direction).unwrap().target,
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;
*position = Position::Moving {
from: to,
to: map.graph.adjacency_list[to].get(next_direction).unwrap().target,
remaining_distance: map.graph.adjacency_list[to].get(next_direction).unwrap().distance,
to: map.graph.adjacency_list[to as usize].get(next_direction).unwrap().target,
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.
/// 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 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
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) {
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
/// 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.
/// Speed is static, only applied when the entity has an edge to traverse.
@@ -57,7 +57,7 @@ impl Position {
let pos = match &self {
Position::Stopped { 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
}
Position::Moving {
@@ -66,11 +66,12 @@ impl Position {
remaining_distance,
} => {
// Entity is traveling between nodes
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 })?;
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 as usize))?;
let edge = graph.find_edge(*from, *to).ok_or(EntityError::EdgeNotFound {
from: *from as usize,
to: *to as usize,
})?;
// For zero-distance edges (tunnels), progress >= 1.0 means we're at the target
if edge.distance == 0.0 {

View File

@@ -97,7 +97,7 @@ pub fn hud_render_system(
errors.write(TextureError::RenderFailed(format!("Failed to render lives text: {}", e)).into());
}
// Render score text in yellow (Pac-Man's color)
// Render score text
let score_text = format!("{:02}", score.0);
let score_offset = 7 - (score_text.len() as i32);
let score_position = glam::UVec2::new(4 + 8 * score_offset as u32, 10); // x_offset + score_offset * 8, 8 + y_offset
@@ -106,6 +106,14 @@ pub fn hud_render_system(
errors.write(TextureError::RenderFailed(format!("Failed to render score text: {}", e)).into());
}
// Render high score text
let high_score_text = format!("{:02}", score.0);
let high_score_offset = 17 - (high_score_text.len() as i32);
let high_score_position = glam::UVec2::new(4 + 8 * high_score_offset as u32, 10); // x_offset + score_offset * 8, 8 + y_offset
if let Err(e) = text_renderer.render(canvas, &mut atlas, &high_score_text, high_score_position) {
errors.write(TextureError::RenderFailed(format!("Failed to render high score text: {}", e)).into());
}
// Render text based on StartupSequence stage
if matches!(
*startup,

View File

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

View File

@@ -5,7 +5,8 @@ use pacman::{
events::GameEvent,
map::builder::Map,
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 {
world
.spawn((
Position::Stopped { node },
Position::Stopped { node: node as NodeId },
Collider { size: 12.0 },
GhostCollider,
Ghost::Blinky,

View File

@@ -1,4 +1,4 @@
use glam::IVec2;
use glam::I8Vec2;
use pacman::map::direction::*;
#[test]
@@ -18,14 +18,14 @@ fn test_direction_opposite() {
#[test]
fn test_direction_as_ivec2() {
let test_cases = [
(Direction::Up, -IVec2::Y),
(Direction::Down, IVec2::Y),
(Direction::Left, -IVec2::X),
(Direction::Right, IVec2::X),
(Direction::Up, -I8Vec2::Y),
(Direction::Down, I8Vec2::Y),
(Direction::Left, -I8Vec2::X),
(Direction::Right, I8Vec2::X),
];
for (dir, expected) in test_cases {
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 {
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)
+ Vec2::splat(CELL_SIZE as f32 / 2.0);
let expected_pos = Vec2::new(
(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);
}

View File

@@ -5,21 +5,41 @@ use std::time::Duration;
fn test_timing_statistics() {
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(12));
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 (avg, std_dev) = stats.get(&SystemId::PlayerControls).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);
assert!(close_enough(*avg, Duration::from_millis(10)), "avg: {:?}", avg);
assert!(close_enough(*std_dev, Duration::from_millis(2)), "std_dev: {:?}", std_dev);
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);
assert!(
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]

View File

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