Compare commits

..

2 Commits

Author SHA1 Message Date
90adaf9e84 feat: add cursor-based node highlighting for debug 2025-08-16 12:26:24 -05:00
2140fbec1b fix: allow key holddown 2025-08-16 12:00:58 -05:00
6 changed files with 147 additions and 81 deletions

View File

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

View File

@@ -1,4 +1,6 @@
//! Debug rendering system
use std::cmp::Ordering;
use crate::constants::BOARD_PIXEL_OFFSET;
use crate::map::builder::Map;
use crate::systems::components::Collider;
@@ -6,11 +8,15 @@ use crate::systems::movement::Position;
use crate::systems::profiling::SystemTimings;
use crate::systems::render::BackbufferResource;
use bevy_ecs::prelude::*;
use glam::Vec2;
use sdl2::pixels::Color;
use sdl2::rect::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]
@@ -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) {
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
let scale = scale_x.min(scale_y);
let x = (pos.0 * scale) as i32;
let y = (pos.1 * scale) as i32;
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)
}
@@ -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) {
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
let scale = scale_x.min(scale_y);
let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale) as i32;
let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale) as i32;
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)
}
@@ -129,6 +141,7 @@ pub fn debug_render_system(
timings: Res<SystemTimings>,
map: Res<Map>,
colliders: Query<(&Collider, &Position)>,
cursor: Res<CursorPosition>,
) {
if *debug_state == DebugState::Off {
return;
@@ -153,28 +166,49 @@ 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();
// Draw debug info on the high-resolution debug texture
canvas
.with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
match *debug_state {
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);
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;
// Transform positions using common method
let (start_x, start_y) =
transform_position_with_offset((start_node.x, start_node.y), output_size, logical_size);
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);
debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap();
}
debug_canvas.set_draw_color(Color::BLUE);
for node in map.graph.nodes() {
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);
} else {
debug_canvas.set_draw_color(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);
@@ -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_display(debug_canvas, &mut texture_creator, &timings);
})

View File

@@ -18,9 +18,9 @@ use crate::{
pub fn ghost_movement_system(
map: Res<Map>,
delta_time: Res<DeltaTime>,
mut ghosts: Query<(&mut Ghost, &mut Velocity, &mut Position)>,
mut ghosts: Query<(&Ghost, &mut Velocity, &mut Position)>,
) {
for (mut 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;
loop {
match *position {
@@ -32,10 +32,10 @@ pub fn ghost_movement_system(
// Collect all available directions that ghosts can traverse
for edge in Direction::DIRECTIONS.iter().flat_map(|d| intersection.get(*d)) {
if edge.traversal_flags.contains(crate::entity::graph::TraversalFlags::GHOST) {
if edge.direction != opposite {
non_opposite_options.push(edge);
}
if edge.traversal_flags.contains(crate::entity::graph::TraversalFlags::GHOST)
&& edge.direction != opposite
{
non_opposite_options.push(edge);
}
}
@@ -67,39 +67,3 @@ pub fn ghost_movement_system(
}
}
}
/// Chooses a random available direction for a ghost at an intersection.
///
/// This function mirrors the behavior from the old ghost implementation,
/// preferring not to reverse direction unless it's the only option.
fn choose_random_direction(map: &Map, velocity: &mut Velocity, position: &Position) {
let current_node = position.current_node();
let intersection = &map.graph.adjacency_list[current_node];
// Collect all available directions that ghosts can traverse
let mut available_directions = SmallVec::<[Direction; 4]>::new();
for direction in Direction::DIRECTIONS {
if let Some(edge) = intersection.get(direction) {
// Check if ghosts can traverse this edge
if edge.traversal_flags.contains(crate::entity::graph::TraversalFlags::GHOST) {
available_directions.push(direction);
}
}
}
// Choose a random direction (avoid reversing unless necessary)
if !available_directions.is_empty() {
let mut rng = SmallRng::from_os_rng();
// Filter out the opposite direction if possible, but allow it if we have limited options
let opposite = velocity.direction.opposite();
let filtered_directions: Vec<_> = available_directions
.iter()
.filter(|&&dir| dir != opposite || available_directions.len() <= 2)
.collect();
if let Some(&random_direction) = filtered_directions.choose(&mut rng) {
velocity.direction = *random_direction;
}
}
}

View File

@@ -1,8 +1,14 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use bevy_ecs::{event::EventWriter, prelude::Res, resource::Resource, system::NonSendMut};
use bevy_ecs::{
event::EventWriter,
resource::Resource,
system::{NonSendMut, ResMut},
};
use glam::Vec2;
use sdl2::{event::Event, keyboard::Keycode, EventPump};
use crate::systems::debug::CursorPosition;
use crate::{
entity::direction::Direction,
events::{GameCommand, GameEvent},
@@ -11,6 +17,8 @@ use crate::{
#[derive(Debug, Clone, Resource)]
pub struct Bindings {
key_bindings: HashMap<Keycode, GameCommand>,
movement_keys: HashSet<Keycode>,
last_movement_key: Option<Keycode>,
}
impl Default for Bindings {
@@ -35,23 +43,78 @@ impl Default for Bindings {
key_bindings.insert(Keycode::Escape, GameCommand::Exit);
key_bindings.insert(Keycode::Q, GameCommand::Exit);
Self { key_bindings }
let movement_keys = HashSet::from([
Keycode::W,
Keycode::A,
Keycode::S,
Keycode::D,
Keycode::Up,
Keycode::Down,
Keycode::Left,
Keycode::Right,
]);
Self {
key_bindings,
movement_keys,
last_movement_key: None,
}
}
}
pub fn input_system(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>, mut pump: NonSendMut<&'static mut EventPump>) {
pub fn input_system(
mut bindings: ResMut<Bindings>,
mut writer: EventWriter<GameEvent>,
mut pump: NonSendMut<&'static mut EventPump>,
mut cursor: ResMut<CursorPosition>,
) {
let mut movement_key_pressed = false;
for event in pump.poll_iter() {
match event {
Event::Quit { .. } => {
writer.write(GameEvent::Command(GameCommand::Exit));
}
Event::KeyDown { keycode: Some(key), .. } => {
Event::MouseMotion { x, y, .. } => {
cursor.0 = Vec2::new(x as f32, y as f32);
}
Event::KeyUp {
repeat: false,
keycode: Some(key),
..
} => {
// If the last movement key was released, then forget it.
if let Some(last_movement_key) = bindings.last_movement_key {
if last_movement_key == key {
bindings.last_movement_key = None;
}
}
}
Event::KeyDown {
keycode: Some(key),
repeat: false,
..
} => {
let command = bindings.key_bindings.get(&key).copied();
if let Some(command) = command {
writer.write(GameEvent::Command(command));
}
if bindings.movement_keys.contains(&key) {
movement_key_pressed = true;
bindings.last_movement_key = Some(key);
}
}
_ => {}
}
}
if let Some(last_movement_key) = bindings.last_movement_key {
if !movement_key_pressed {
let command = bindings.key_bindings.get(&last_movement_key).copied();
if let Some(command) = command {
writer.write(GameEvent::Command(command));
}
}
}
}

View File

@@ -1,12 +1,7 @@
use crate::entity::direction::Direction;
use crate::entity::graph::Graph;
use crate::entity::{direction::Direction, graph::Edge};
use crate::error::{EntityError, GameError, GameResult};
use crate::map::builder::Map;
use crate::systems::components::{DeltaTime, EntityType, PlayerControlled};
use crate::error::{EntityError, GameResult};
use bevy_ecs::component::Component;
use bevy_ecs::event::EventWriter;
use bevy_ecs::query::With;
use bevy_ecs::system::{Query, Res};
use glam::Vec2;
/// A unique identifier for a node, represented by its index in the graph's storage.
@@ -131,19 +126,6 @@ impl Position {
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(

View File

@@ -23,7 +23,7 @@ pub fn player_control_system(
mut state: ResMut<GlobalState>,
mut debug_state: ResMut<DebugState>,
mut audio_state: ResMut<AudioState>,
mut players: Query<(&mut BufferedDirection), With<PlayerControlled>>,
mut players: Query<&mut BufferedDirection, With<PlayerControlled>>,
mut errors: EventWriter<GameError>,
) {
// Get the player's movable component (ensuring there is only one player)
@@ -73,7 +73,7 @@ pub fn player_movement_system(
map: Res<Map>,
delta_time: Res<DeltaTime>,
mut entities: Query<(&mut Position, &mut Velocity, &mut BufferedDirection), With<PlayerControlled>>,
mut errors: EventWriter<GameError>,
// mut errors: EventWriter<GameError>,
) {
for (mut position, mut velocity, mut buffered_direction) in entities.iter_mut() {
// Decrement the buffered direction remaining time