Compare commits

...

1 Commits

Author SHA1 Message Date
90adaf9e84 feat: add cursor-based node highlighting for debug 2025-08-16 12:26:24 -05:00
4 changed files with 75 additions and 25 deletions

View File

@@ -47,7 +47,7 @@ use crate::{
constants, constants,
events::GameCommand, events::GameCommand,
map::render::MapRenderer, map::render::MapRenderer,
systems::input::Bindings, systems::{debug::CursorPosition, input::Bindings},
texture::sprite::{AtlasMapper, SpriteAtlas}, texture::sprite::{AtlasMapper, SpriteAtlas},
}; };
@@ -198,6 +198,7 @@ impl Game {
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());
world.insert_resource(CursorPosition::default());
world.add_observer( world.add_observer(
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| { |event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {

View File

@@ -1,4 +1,6 @@
//! Debug rendering system //! Debug rendering system
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::components::Collider; use crate::systems::components::Collider;
@@ -6,11 +8,15 @@ use crate::systems::movement::Position;
use crate::systems::profiling::SystemTimings; use crate::systems::profiling::SystemTimings;
use crate::systems::render::BackbufferResource; use crate::systems::render::BackbufferResource;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use glam::Vec2;
use sdl2::pixels::Color; use sdl2::pixels::Color;
use sdl2::rect::Rect; use sdl2::rect::Rect;
use sdl2::render::{Canvas, Texture, TextureCreator}; use sdl2::render::{Canvas, Texture, TextureCreator};
use sdl2::video::{Window, WindowContext}; use sdl2::video::{Window, WindowContext};
#[derive(Resource, Default, Debug, Copy, Clone)]
pub struct CursorPosition(pub Vec2);
#[derive(Resource, Default, Debug, Copy, Clone, PartialEq)] #[derive(Resource, Default, Debug, Copy, Clone, PartialEq)]
pub enum DebugState { pub enum DebugState {
#[default] #[default]
@@ -36,10 +42,13 @@ pub struct DebugTextureResource(pub Texture<'static>);
fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) { 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_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_y = output_size.1 as f32 / logical_size.1 as f32;
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio let scale = scale_x.min(scale_y);
let x = (pos.0 * scale) as i32; let offset_x = (output_size.0 as f32 - logical_size.0 as f32 * scale) / 2.0;
let y = (pos.1 * scale) as i32; 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) (x, y)
} }
@@ -47,10 +56,13 @@ fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u
fn transform_position_with_offset(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) { 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_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_y = output_size.1 as f32 / logical_size.1 as f32;
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio let scale = scale_x.min(scale_y);
let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale) as i32; let offset_x = (output_size.0 as f32 - logical_size.0 as f32 * scale) / 2.0;
let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale) as i32; 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) (x, y)
} }
@@ -129,6 +141,7 @@ pub fn debug_render_system(
timings: Res<SystemTimings>, timings: Res<SystemTimings>,
map: Res<Map>, map: Res<Map>,
colliders: Query<(&Collider, &Position)>, colliders: Query<(&Collider, &Position)>,
cursor: Res<CursorPosition>,
) { ) {
if *debug_state == DebugState::Off { if *debug_state == DebugState::Off {
return; return;
@@ -153,28 +166,49 @@ pub fn debug_render_system(
// Get texture creator before entering the closure to avoid borrowing conflicts // Get texture creator before entering the closure to avoid borrowing conflicts
let mut texture_creator = canvas.texture_creator(); let mut texture_creator = canvas.texture_creator();
let cursor_world_pos = cursor.0 - BOARD_PIXEL_OFFSET.as_vec2();
// Draw debug info on the high-resolution debug texture // Draw debug info on the high-resolution debug texture
canvas canvas
.with_texture_canvas(&mut debug_texture.0, |debug_canvas| { .with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
match *debug_state { match *debug_state {
DebugState::Graph => { DebugState::Graph => {
// Find the closest node to the cursor
let closest_node = 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);
debug_canvas.set_draw_color(Color::RED); debug_canvas.set_draw_color(Color::RED);
for (start_node, end_node) in map.graph.edges() { for (start_node, end_node) in map.graph.edges() {
let start_node = map.graph.get_node(start_node).unwrap().position; let start_node_model = map.graph.get_node(start_node).unwrap();
let end_node = map.graph.get_node(end_node.target).unwrap().position; let end_node = map.graph.get_node(end_node.target).unwrap().position;
// Transform positions using common method // Transform positions using common method
let (start_x, start_y) = let (start_x, start_y) = transform_position_with_offset(
transform_position_with_offset((start_node.x, start_node.y), output_size, logical_size); (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 (end_x, end_y) = transform_position_with_offset((end_node.x, end_node.y), output_size, logical_size);
debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap(); debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap();
} }
debug_canvas.set_draw_color(Color::BLUE); for (id, node) in map.graph.nodes().enumerate() {
for node in map.graph.nodes() {
let pos = node.position; 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);
} else {
debug_canvas.set_draw_color(Color::BLUE);
}
// Transform position using common method // Transform position using common method
let (x, y) = transform_position_with_offset((pos.x, pos.y), output_size, logical_size); 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 size = transform_size(4.0, output_size, logical_size);
@@ -201,6 +235,28 @@ pub fn debug_render_system(
_ => {} _ => {}
} }
// 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 information in the top-left corner
render_timing_display(debug_canvas, &mut texture_creator, &timings); render_timing_display(debug_canvas, &mut texture_creator, &timings);
}) })

View File

@@ -5,8 +5,10 @@ use bevy_ecs::{
resource::Resource, resource::Resource,
system::{NonSendMut, ResMut}, system::{NonSendMut, ResMut},
}; };
use glam::Vec2;
use sdl2::{event::Event, keyboard::Keycode, EventPump}; use sdl2::{event::Event, keyboard::Keycode, EventPump};
use crate::systems::debug::CursorPosition;
use crate::{ use crate::{
entity::direction::Direction, entity::direction::Direction,
events::{GameCommand, GameEvent}, events::{GameCommand, GameEvent},
@@ -64,6 +66,7 @@ pub fn input_system(
mut bindings: ResMut<Bindings>, mut bindings: ResMut<Bindings>,
mut writer: EventWriter<GameEvent>, mut writer: EventWriter<GameEvent>,
mut pump: NonSendMut<&'static mut EventPump>, mut pump: NonSendMut<&'static mut EventPump>,
mut cursor: ResMut<CursorPosition>,
) { ) {
let mut movement_key_pressed = false; let mut movement_key_pressed = false;
@@ -72,6 +75,9 @@ pub fn input_system(
Event::Quit { .. } => { Event::Quit { .. } => {
writer.write(GameEvent::Command(GameCommand::Exit)); writer.write(GameEvent::Command(GameCommand::Exit));
} }
Event::MouseMotion { x, y, .. } => {
cursor.0 = Vec2::new(x as f32, y as f32);
}
Event::KeyUp { Event::KeyUp {
repeat: false, repeat: false,
keycode: Some(key), keycode: Some(key),

View File

@@ -126,19 +126,6 @@ impl Position {
Position::Moving { from, .. } => *from, Position::Moving { from, .. } => *from,
} }
} }
/// Returns the `NodeId` of the destination node, if currently traveling.
pub fn target_node(&self) -> Option<NodeId> {
match self {
Position::Stopped { .. } => None,
Position::Moving { to, .. } => Some(*to),
}
}
/// Returns `true` if the entity is traveling between nodes.
pub fn is_moving(&self) -> bool {
matches!(self, Position::Moving { .. })
}
} }
// pub fn movement_system( // pub fn movement_system(