refactor: optimize debug system, remove redundant code & tests

This commit is contained in:
2025-08-16 13:41:15 -05:00
parent 90adaf9e84
commit f940f01d9b
10 changed files with 82 additions and 279 deletions

View File

@@ -218,11 +218,6 @@ impl Graph {
self.nodes.get(id)
}
/// Returns the total number of nodes in the graph.
pub fn node_count(&self) -> usize {
self.nodes.len()
}
/// Returns an iterator over all nodes in the graph.
pub fn nodes(&self) -> impl Iterator<Item = &Node> {
self.nodes.iter()

View File

@@ -47,7 +47,7 @@ use crate::{
constants,
events::GameCommand,
map::render::MapRenderer,
systems::{debug::CursorPosition, input::Bindings},
systems::input::{Bindings, CursorPosition},
texture::sprite::{AtlasMapper, SpriteAtlas},
};

View File

@@ -3,12 +3,9 @@ use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
use crate::entity::direction::Direction;
use crate::entity::graph::{Graph, Node, TraversalFlags};
use crate::map::parser::MapTileParser;
use crate::map::render::MapRenderer;
use crate::systems::movement::NodeId;
use crate::texture::sprite::SpriteAtlas;
use bevy_ecs::resource::Resource;
use glam::{IVec2, Vec2};
use sdl2::render::{Canvas, RenderTarget};
use std::collections::{HashMap, VecDeque};
use tracing::debug;
@@ -165,20 +162,6 @@ impl Map {
})
}
/// Renders a debug visualization with cursor-based highlighting.
///
/// This function provides interactive debugging by highlighting the nearest node
/// to the cursor, showing its ID, and highlighting its connections.
pub fn debug_render_with_cursor<T: RenderTarget>(
&self,
canvas: &mut Canvas<T>,
text_renderer: &mut crate::texture::text::TextTexture,
atlas: &mut SpriteAtlas,
cursor_pos: glam::Vec2,
) -> GameResult<()> {
MapRenderer::debug_render_with_cursor(&self.graph, canvas, text_renderer, atlas, cursor_pos)
}
/// Builds the house structure in the graph.
fn build_house(
graph: &mut Graph,

View File

@@ -3,14 +3,10 @@
use crate::constants::{BOARD_CELL_OFFSET, CELL_SIZE};
use crate::map::layout::TILE_MAP;
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
use crate::texture::text::TextTexture;
use glam::Vec2;
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
use sdl2::rect::Rect;
use sdl2::render::{Canvas, RenderTarget};
use crate::error::{EntityError, GameError, GameResult};
/// Handles rendering operations for the map.
pub struct MapRenderer;
@@ -37,111 +33,4 @@ impl MapRenderer {
}
}
}
/// Renders a debug visualization with cursor-based highlighting.
///
/// This function provides interactive debugging by highlighting the nearest node
/// to the cursor, showing its ID, and highlighting its connections.
pub fn debug_render_with_cursor<T: RenderTarget>(
graph: &crate::entity::graph::Graph,
canvas: &mut Canvas<T>,
text_renderer: &mut TextTexture,
atlas: &mut SpriteAtlas,
cursor_pos: Vec2,
) -> GameResult<()> {
// Find the nearest node to the cursor
let nearest_node = Self::find_nearest_node(graph, cursor_pos);
// Draw all connections in blue
canvas.set_draw_color(Color::RGB(0, 0, 128)); // Dark blue for regular connections
for i in 0..graph.node_count() {
let node = graph.get_node(i).ok_or(GameError::Entity(EntityError::NodeNotFound(i)))?;
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
for edge in graph.adjacency_list[i].edges() {
let end_pos = graph
.get_node(edge.target)
.ok_or(GameError::Entity(EntityError::NodeNotFound(edge.target)))?
.position
+ crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
canvas
.draw_line((pos.x as i32, pos.y as i32), (end_pos.x as i32, end_pos.y as i32))
.map_err(|e| GameError::Sdl(e.to_string()))?;
}
}
// Draw all nodes in green
canvas.set_draw_color(Color::RGB(0, 128, 0)); // Dark green for regular nodes
for i in 0..graph.node_count() {
let node = graph.get_node(i).ok_or(GameError::Entity(EntityError::NodeNotFound(i)))?;
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
canvas
.fill_rect(Rect::new(0, 0, 3, 3).centered_on(Point::new(pos.x as i32, pos.y as i32)))
.map_err(|e| GameError::Sdl(e.to_string()))?;
}
// Highlight connections from the nearest node in bright blue
if let Some(nearest_id) = nearest_node {
let nearest_pos = graph
.get_node(nearest_id)
.ok_or(GameError::Entity(EntityError::NodeNotFound(nearest_id)))?
.position
+ crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
canvas.set_draw_color(Color::RGB(0, 255, 255)); // Bright cyan for highlighted connections
for edge in graph.adjacency_list[nearest_id].edges() {
let end_pos = graph
.get_node(edge.target)
.ok_or(GameError::Entity(EntityError::NodeNotFound(edge.target)))?
.position
+ crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
canvas
.draw_line(
(nearest_pos.x as i32, nearest_pos.y as i32),
(end_pos.x as i32, end_pos.y as i32),
)
.map_err(|e| GameError::Sdl(e.to_string()))?;
}
// Highlight the nearest node in bright green
canvas.set_draw_color(Color::RGB(0, 255, 0)); // Bright green for highlighted node
canvas
.fill_rect(Rect::new(0, 0, 5, 5).centered_on(Point::new(nearest_pos.x as i32, nearest_pos.y as i32)))
.map_err(|e| GameError::Sdl(e.to_string()))?;
// Draw node ID text (small, offset to top right)
text_renderer.set_scale(0.5); // Small text
let id_text = format!("#{nearest_id}");
let text_pos = glam::UVec2::new(
(nearest_pos.x + 4.0) as u32, // Offset to the right
(nearest_pos.y - 6.0) as u32, // Offset to the top
);
if let Err(e) = text_renderer.render(canvas, atlas, &id_text, text_pos) {
tracing::error!("Failed to render node ID text: {}", e);
}
}
Ok(())
}
/// Finds the nearest node to the given cursor position.
pub fn find_nearest_node(graph: &crate::entity::graph::Graph, cursor_pos: Vec2) -> Option<usize> {
let mut nearest_id = None;
let mut nearest_distance = f32::INFINITY;
for i in 0..graph.node_count() {
if let Some(node) = graph.get_node(i) {
let node_pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
let distance = cursor_pos.distance(node_pos);
if distance < nearest_distance {
nearest_distance = distance;
nearest_id = Some(i);
}
}
}
nearest_id
}
}

View File

@@ -4,19 +4,17 @@ use std::cmp::Ordering;
use crate::constants::BOARD_PIXEL_OFFSET;
use crate::map::builder::Map;
use crate::systems::components::Collider;
use crate::systems::input::CursorPosition;
use crate::systems::movement::Position;
use crate::systems::profiling::SystemTimings;
use crate::systems::render::BackbufferResource;
use bevy_ecs::prelude::*;
use glam::Vec2;
use glam::{IVec2, UVec2, Vec2};
use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::rect::{Point, Rect};
use sdl2::render::{Canvas, Texture, TextureCreator};
use sdl2::video::{Window, WindowContext};
#[derive(Resource, Default, Debug, Copy, Clone)]
pub struct CursorPosition(pub Vec2);
#[derive(Resource, Default, Debug, Copy, Clone, PartialEq)]
pub enum DebugState {
#[default]
@@ -38,41 +36,9 @@ impl DebugState {
/// Resource to hold the debug texture for persistent rendering
pub struct DebugTextureResource(pub Texture<'static>);
/// Transforms a position from logical canvas coordinates to output canvas coordinates
fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
let scale = scale_x.min(scale_y);
let offset_x = (output_size.0 as f32 - logical_size.0 as f32 * scale) / 2.0;
let offset_y = (output_size.1 as f32 - logical_size.1 as f32 * scale) / 2.0;
let x = (pos.0 * scale + offset_x) as i32;
let y = (pos.1 * scale + offset_y) as i32;
(x, y)
}
/// Transforms a position from logical canvas coordinates to output canvas coordinates (with board offset)
fn transform_position_with_offset(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
let scale = scale_x.min(scale_y);
let offset_x = (output_size.0 as f32 - logical_size.0 as f32 * scale) / 2.0;
let offset_y = (output_size.1 as f32 - logical_size.1 as f32 * scale) / 2.0;
let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale + offset_x) as i32;
let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale + offset_y) as i32;
(x, y)
}
/// Transforms a size from logical canvas coordinates to output canvas coordinates
fn transform_size(size: f32, output_size: (u32, u32), logical_size: (u32, u32)) -> u32 {
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
(size * scale) as u32
fn transform_position_with_offset(pos: Vec2, scale: f32) -> IVec2 {
((pos + BOARD_PIXEL_OFFSET.as_vec2()) * scale).as_ivec2()
}
/// Renders timing information in the top-left corner of the screen
@@ -133,6 +99,7 @@ fn render_timing_display(
}
}
#[allow(clippy::too_many_arguments)]
pub fn debug_render_system(
mut canvas: NonSendMut<&mut Canvas<Window>>,
backbuffer: NonSendMut<BackbufferResource>,
@@ -146,10 +113,8 @@ pub fn debug_render_system(
if *debug_state == DebugState::Off {
return;
}
// Get canvas sizes for coordinate transformation
let output_size = canvas.output_size().unwrap();
let logical_size = canvas.logical_size();
let scale =
(UVec2::from(canvas.output_size().unwrap()).as_vec2() / UVec2::from(canvas.logical_size()).as_vec2()).min_element();
// Copy the current backbuffer to the debug texture
canvas
@@ -166,7 +131,10 @@ pub fn debug_render_system(
// Get texture creator before entering the closure to avoid borrowing conflicts
let mut texture_creator = canvas.texture_creator();
let cursor_world_pos = cursor.0 - BOARD_PIXEL_OFFSET.as_vec2();
let cursor_world_pos = match *cursor {
CursorPosition::None => None,
CursorPosition::Some { position, .. } => Some(position - BOARD_PIXEL_OFFSET.as_vec2()),
};
// Draw debug info on the high-resolution debug texture
canvas
@@ -175,13 +143,16 @@ pub fn debug_render_system(
DebugState::Graph => {
// Find the closest node to the cursor
let closest_node = map
.graph
let closest_node = if let Some(cursor_world_pos) = cursor_world_pos {
map.graph
.nodes()
.map(|node| node.position.distance(cursor_world_pos))
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less))
.map(|(id, _)| id);
.map(|(id, _)| id)
} else {
None
};
debug_canvas.set_draw_color(Color::RED);
for (start_node, end_node) in map.graph.edges() {
@@ -189,34 +160,45 @@ pub fn debug_render_system(
let end_node = map.graph.get_node(end_node.target).unwrap().position;
// Transform positions using common method
let (start_x, start_y) = transform_position_with_offset(
(start_node_model.position.x, start_node_model.position.y),
output_size,
logical_size,
);
let (end_x, end_y) = transform_position_with_offset((end_node.x, end_node.y), output_size, logical_size);
let start = transform_position_with_offset(start_node_model.position, scale);
let end = transform_position_with_offset(end_node, scale);
debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap();
debug_canvas
.draw_line(Point::from((start.x, start.y)), Point::from((end.x, end.y)))
.unwrap();
}
for (id, node) in map.graph.nodes().enumerate() {
let pos = node.position;
// Set color based on whether the node is the closest to the cursor
if Some(id) == closest_node {
debug_canvas.set_draw_color(Color::YELLOW);
debug_canvas.set_draw_color(if Some(id) == closest_node {
Color::YELLOW
} else {
debug_canvas.set_draw_color(Color::BLUE);
}
Color::BLUE
});
// Transform position using common method
let (x, y) = transform_position_with_offset((pos.x, pos.y), output_size, logical_size);
let size = transform_size(4.0, output_size, logical_size);
let pos = transform_position_with_offset(pos, scale);
let size = (3.0 * scale) as u32;
debug_canvas
.fill_rect(Rect::new(x - (size as i32 / 2), y - (size as i32 / 2), size, size))
.fill_rect(Rect::new(pos.x - (size as i32 / 2), pos.y - (size as i32 / 2), size, size))
.unwrap();
}
// 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 pos = transform_position_with_offset(node.position, scale);
let ttf_context = sdl2::ttf::init().unwrap();
let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap();
let surface = font.render(&closest_node_id.to_string()).blended(Color::WHITE).unwrap();
let texture = texture_creator.create_texture_from_surface(&surface).unwrap();
let dest = Rect::new(pos.x + 10, pos.y - 5, texture.query().width, texture.query().height);
debug_canvas.copy(&texture, None, dest).unwrap();
}
}
DebugState::Collision => {
debug_canvas.set_draw_color(Color::GREEN);
@@ -224,39 +206,16 @@ pub fn debug_render_system(
let pos = position.get_pixel_position(&map.graph).unwrap();
// Transform position and size using common methods
let (x, y) = transform_position((pos.x, pos.y), output_size, logical_size);
let size = transform_size(collider.size, output_size, logical_size);
let pos = (pos * scale).as_ivec2();
let size = (collider.size * scale) as u32;
// Center the collision box on the entity
let rect = Rect::new(x - (size as i32 / 2), y - (size as i32 / 2), size, size);
let rect = Rect::from_center(Point::from((pos.x, pos.y)), size, size);
debug_canvas.draw_rect(rect).unwrap();
}
}
_ => {}
}
// Render node ID if a node is highlighted
if let DebugState::Graph = *debug_state {
if let Some(closest_node_id) = map
.graph
.nodes()
.map(|node| node.position.distance(cursor_world_pos))
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less))
.map(|(id, _)| id)
{
let node = map.graph.get_node(closest_node_id).unwrap();
let (x, y) = transform_position_with_offset((node.position.x, node.position.y), output_size, logical_size);
let ttf_context = sdl2::ttf::init().unwrap();
let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap();
let surface = font.render(&closest_node_id.to_string()).blended(Color::WHITE).unwrap();
let texture = texture_creator.create_texture_from_surface(&surface).unwrap();
let dest = Rect::new(x + 10, y - 5, texture.query().width, texture.query().height);
debug_canvas.copy(&texture, None, dest).unwrap();
}
}
// Render timing information in the top-left corner
render_timing_display(debug_canvas, &mut texture_creator, &timings);
})

View File

@@ -3,18 +3,28 @@ use std::collections::{HashMap, HashSet};
use bevy_ecs::{
event::EventWriter,
resource::Resource,
system::{NonSendMut, ResMut},
system::{NonSendMut, Res, ResMut},
};
use glam::Vec2;
use sdl2::{event::Event, keyboard::Keycode, EventPump};
use crate::systems::debug::CursorPosition;
use crate::systems::components::DeltaTime;
use crate::{
entity::direction::Direction,
events::{GameCommand, GameEvent},
};
#[derive(Debug, Clone, Resource)]
#[derive(Resource, Default, Debug, Copy, Clone)]
pub enum CursorPosition {
#[default]
None,
Some {
position: Vec2,
remaining_time: f32,
},
}
#[derive(Resource, Debug, Clone)]
pub struct Bindings {
key_bindings: HashMap<Keycode, GameCommand>,
movement_keys: HashSet<Keycode>,
@@ -63,12 +73,14 @@ impl Default for Bindings {
}
pub fn input_system(
delta_time: Res<DeltaTime>,
mut bindings: ResMut<Bindings>,
mut writer: EventWriter<GameEvent>,
mut pump: NonSendMut<&'static mut EventPump>,
mut cursor: ResMut<CursorPosition>,
) {
let mut movement_key_pressed = false;
let mut cursor_seen = false;
for event in pump.poll_iter() {
match event {
@@ -76,7 +88,11 @@ pub fn input_system(
writer.write(GameEvent::Command(GameCommand::Exit));
}
Event::MouseMotion { x, y, .. } => {
cursor.0 = Vec2::new(x as f32, y as f32);
*cursor = CursorPosition::Some {
position: Vec2::new(x as f32, y as f32),
remaining_time: 0.20,
};
cursor_seen = true;
}
Event::KeyUp {
repeat: false,
@@ -117,4 +133,11 @@ pub fn input_system(
}
}
}
if let (false, CursorPosition::Some { remaining_time, .. }) = (cursor_seen, &mut *cursor) {
*remaining_time -= delta_time.0;
if *remaining_time <= 0.0 {
*cursor = CursorPosition::None;
}
}
}

View File

@@ -1,34 +0,0 @@
use glam::Vec2;
use pacman::entity::graph::{Graph, Node};
use pacman::map::render::MapRenderer;
#[test]
fn test_find_nearest_node() {
let mut graph = Graph::new();
// Add some test nodes
let node1 = graph.add_node(Node {
position: Vec2::new(10.0, 10.0),
});
let node2 = graph.add_node(Node {
position: Vec2::new(50.0, 50.0),
});
let node3 = graph.add_node(Node {
position: Vec2::new(100.0, 100.0),
});
// Test cursor near node1
let cursor_pos = Vec2::new(12.0, 8.0);
let nearest = MapRenderer::find_nearest_node(&graph, cursor_pos);
assert_eq!(nearest, Some(node1));
// Test cursor near node2
let cursor_pos = Vec2::new(45.0, 55.0);
let nearest = MapRenderer::find_nearest_node(&graph, cursor_pos);
assert_eq!(nearest, Some(node2));
// Test cursor near node3
let cursor_pos = Vec2::new(98.0, 102.0);
let nearest = MapRenderer::find_nearest_node(&graph, cursor_pos);
assert_eq!(nearest, Some(node3));
}

View File

@@ -1,12 +0,0 @@
use pacman::constants::RAW_BOARD;
use pacman::map::builder::Map;
mod item;
#[test]
fn test_game_map_creation() {
let map = Map::new(RAW_BOARD).unwrap();
assert!(map.graph.node_count() > 0);
assert!(!map.grid_to_node.is_empty());
}

View File

@@ -29,7 +29,7 @@ fn test_graph_basic_operations() {
position: glam::Vec2::new(16.0, 0.0),
});
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.nodes().count(), 2);
assert!(graph.get_node(node1).is_some());
assert!(graph.get_node(node2).is_some());
assert!(graph.get_node(999).is_none());
@@ -102,7 +102,7 @@ fn should_add_connected_node() {
)
.unwrap();
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.nodes().count(), 2);
let edge = graph.find_edge(node1, node2);
assert!(edge.is_some());
assert_eq!(edge.unwrap().direction, Direction::Right);

View File

@@ -6,7 +6,7 @@ use pacman::map::builder::Map;
fn test_map_creation() {
let map = Map::new(RAW_BOARD).unwrap();
assert!(map.graph.node_count() > 0);
assert!(map.graph.nodes().count() > 0);
assert!(!map.grid_to_node.is_empty());
// Check that some connections were made