From 0aa056a0ae13b581b4191198f9c4f34bfa75822d Mon Sep 17 00:00:00 2001 From: Xevion Date: Thu, 14 Aug 2025 18:17:58 -0500 Subject: [PATCH] feat: ecs keyboard interactions --- src/ecs/interact.rs | 134 ++++++++++++++++- src/ecs/mod.rs | 1 + src/entity/traversal.rs | 314 +++++++++++++++++++--------------------- src/game/mod.rs | 19 ++- 4 files changed, 295 insertions(+), 173 deletions(-) diff --git a/src/ecs/interact.rs b/src/ecs/interact.rs index 366ff17..4f51957 100644 --- a/src/ecs/interact.rs +++ b/src/ecs/interact.rs @@ -1,16 +1,144 @@ use bevy_ecs::{ event::{EventReader, EventWriter}, query::With, - system::{Query, ResMut}, + system::{Query, Res, ResMut}, }; use crate::{ - ecs::{GlobalState, PlayerControlled, Velocity}, - error::GameError, + ecs::{DeltaTime, GlobalState, PlayerControlled, Position, Velocity}, + error::{EntityError, GameError}, game::events::GameEvent, input::commands::GameCommand, + map::builder::Map, }; +pub fn movement_system( + map: Res, + delta_time: Res, + mut entities: Query<(&PlayerControlled, &mut Velocity, &mut Position)>, + mut errors: EventWriter, +) { + for (player, mut velocity, mut position) in entities.iter_mut() { + let distance = velocity.speed.unwrap_or(0.0) * delta_time.0; + + // Decrement the remaining frames for the next direction + if let Some((direction, remaining)) = velocity.next_direction { + if remaining > 0 { + velocity.next_direction = Some((direction, remaining - 1)); + } else { + velocity.next_direction = None; + } + } + + match *position { + Position::AtNode(node_id) => { + // We're not moving, but a buffered direction is available. + if let Some((next_direction, _)) = velocity.next_direction { + if let Some(edge) = map.graph.find_edge_in_direction(node_id, next_direction) { + // if edge.permissions.can_traverse(edge) { + // // Start moving in that direction + *position = Position::BetweenNodes { + from: node_id, + to: edge.target, + traversed: distance, + }; + velocity.direction = next_direction; + // } else { + // return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement( + // format!( + // "Cannot traverse edge from {} to {} in direction {:?}", + // node_id, edge.target, next_direction + // ), + // ))); + // } + } else { + errors.write( + EntityError::InvalidMovement(format!( + "No edge found in direction {:?} from node {}", + next_direction, node_id + )) + .into(), + ); + } + + velocity.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it + } + } + Position::BetweenNodes { from, to, traversed } => { + // There is no point in any of the next logic if we don't travel at all + if distance <= 0.0 { + return; + } + + let edge = map + .graph + .find_edge(from, to) + .ok_or_else(|| { + errors.write( + EntityError::InvalidMovement(format!( + "Inconsistent state: Traverser is on a non-existent edge from {} to {}.", + from, to + )) + .into(), + ); + return; + }) + .unwrap(); + + let new_traversed = traversed + distance; + + if new_traversed < edge.distance { + // Still on the same edge, just update the distance. + *position = Position::BetweenNodes { + from, + to, + traversed: new_traversed, + }; + } else { + let overflow = new_traversed - edge.distance; + let mut moved = false; + + // If we buffered a direction, try to find an edge in that direction + if let Some((next_dir, _)) = velocity.next_direction { + if let Some(edge) = map.graph.find_edge_in_direction(to, next_dir) { + // if edge.permissions.can_traverse(edge) { + // *position = Position::BetweenNodes { + // from: to, + // to: edge.target, + // traversed: overflow, + // }; + + velocity.direction = next_dir; // Remember our new direction + velocity.next_direction = None; // Consume the buffered direction + moved = true; + // } + } + } + + // If we didn't move, try to continue in the current direction + if !moved { + if let Some(edge) = map.graph.find_edge_in_direction(to, velocity.direction) { + // if edge.permissions.can_traverse(edge) { + *position = Position::BetweenNodes { + from: to, + to: edge.target, + traversed: overflow, + }; + // } else { + // *position = Position::AtNode(to); + // velocity.next_direction = None; + // } + } else { + *position = Position::AtNode(to); + velocity.next_direction = None; + } + } + } + } + } + } +} + // Handles pub fn interact_system( mut events: EventReader, diff --git a/src/ecs/mod.rs b/src/ecs/mod.rs index 4ab9508..13160fa 100644 --- a/src/ecs/mod.rs +++ b/src/ecs/mod.rs @@ -125,6 +125,7 @@ impl Position { #[derive(Default, Component)] pub struct Velocity { pub direction: Direction, + pub next_direction: Option<(Direction, u8)>, pub speed: Option, } diff --git a/src/entity/traversal.rs b/src/entity/traversal.rs index 273303e..5b7b46d 100644 --- a/src/entity/traversal.rs +++ b/src/entity/traversal.rs @@ -1,181 +1,161 @@ -// use tracing::error; +use tracing::error; -// use crate::error::GameResult; +use crate::ecs::{NodeId, Position}; +use crate::error::GameResult; -// use super::direction::Direction; -// use super::graph::{Edge, Graph, NodeId}; +use super::direction::Direction; +use super::graph::{Edge, Graph}; -// /// Manages an entity's movement through the graph. -// /// -// /// A `Traverser` encapsulates the state of an entity's position and direction, -// /// providing a way to advance along the graph's paths based on a given distance. -// /// It also handles direction changes, buffering the next intended direction. -// pub struct Traverser { -// /// The current position of the traverser in the graph. -// pub position: Position, -// /// The current direction of movement. -// pub direction: Direction, -// /// Buffered direction change with remaining frame count for timing. -// /// -// /// The `u8` value represents the number of frames remaining before -// /// the buffered direction expires. This allows for responsive controls -// /// by storing direction changes for a limited time. -// pub next_direction: Option<(Direction, u8)>, -// } +/// Manages an entity's movement through the graph. +/// +/// A `Traverser` encapsulates the state of an entity's position and direction, +/// providing a way to advance along the graph's paths based on a given distance. +/// It also handles direction changes, buffering the next intended direction. +pub struct Traverser { + /// The current position of the traverser in the graph. + pub position: Position, + /// The current direction of movement. + pub direction: Direction, + /// Buffered direction change with remaining frame count for timing. + /// + /// The `u8` value represents the number of frames remaining before + /// the buffered direction expires. This allows for responsive controls + /// by storing direction changes for a limited time. + pub next_direction: Option<(Direction, u8)>, +} -// impl Traverser { -// /// Creates a new traverser starting at the given node ID. -// /// -// /// The traverser will immediately attempt to start moving in the initial direction. -// pub fn new(graph: &Graph, start_node: NodeId, initial_direction: Direction, can_traverse: &F) -> Self -// where -// F: Fn(Edge) -> bool, -// { -// let mut traverser = Traverser { -// position: Position::AtNode(start_node), -// direction: initial_direction, -// next_direction: Some((initial_direction, 1)), -// }; +impl Traverser { + /// Sets the next direction for the traverser to take. + /// + /// The direction is buffered and will be applied at the next opportunity, + /// typically when the traverser reaches a new node. This allows for responsive + /// controls, as the new direction is stored for a limited time. + pub fn set_next_direction(&mut self, new_direction: Direction) { + if self.direction != new_direction { + self.next_direction = Some((new_direction, 30)); + } + } -// // This will kickstart the traverser into motion -// if let Err(e) = traverser.advance(graph, 0.0, can_traverse) { -// error!("Traverser initialization error: {}", e); -// } + /// Advances the traverser along the graph by a specified distance. + /// + /// This method updates the traverser's position based on its current state + /// and the distance to travel. + /// + /// - If at a node, it checks for a buffered direction to start moving. + /// - If between nodes, it moves along the current edge. + /// - If it reaches a node, it attempts to transition to a new edge based on + /// the buffered direction or by continuing straight. + /// - If no valid move is possible, it stops at the node. + /// + /// Returns an error if the movement is invalid (e.g., trying to move in an impossible direction). + pub fn advance(&mut self, graph: &Graph, distance: f32, can_traverse: &F) -> GameResult<()> + where + F: Fn(Edge) -> bool, + { + // Decrement the remaining frames for the next direction + if let Some((direction, remaining)) = self.next_direction { + if remaining > 0 { + self.next_direction = Some((direction, remaining - 1)); + } else { + self.next_direction = None; + } + } -// traverser -// } + match self.position { + Position::AtNode(node_id) => { + // We're not moving, but a buffered direction is available. + if let Some((next_direction, _)) = self.next_direction { + if let Some(edge) = graph.find_edge_in_direction(node_id, next_direction) { + if can_traverse(edge) { + // Start moving in that direction + self.position = Position::BetweenNodes { + from: node_id, + to: edge.target, + traversed: distance.max(0.0), + }; + self.direction = next_direction; + } else { + return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement( + format!( + "Cannot traverse edge from {} to {} in direction {:?}", + node_id, edge.target, next_direction + ), + ))); + } + } else { + return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement( + format!("No edge found in direction {:?} from node {}", next_direction, node_id), + ))); + } -// /// Sets the next direction for the traverser to take. -// /// -// /// The direction is buffered and will be applied at the next opportunity, -// /// typically when the traverser reaches a new node. This allows for responsive -// /// controls, as the new direction is stored for a limited time. -// pub fn set_next_direction(&mut self, new_direction: Direction) { -// if self.direction != new_direction { -// self.next_direction = Some((new_direction, 30)); -// } -// } + self.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it + } + } + Position::BetweenNodes { from, to, traversed } => { + // There is no point in any of the next logic if we don't travel at all + if distance <= 0.0 { + return Ok(()); + } -// /// Advances the traverser along the graph by a specified distance. -// /// -// /// This method updates the traverser's position based on its current state -// /// and the distance to travel. -// /// -// /// - If at a node, it checks for a buffered direction to start moving. -// /// - If between nodes, it moves along the current edge. -// /// - If it reaches a node, it attempts to transition to a new edge based on -// /// the buffered direction or by continuing straight. -// /// - If no valid move is possible, it stops at the node. -// /// -// /// Returns an error if the movement is invalid (e.g., trying to move in an impossible direction). -// pub fn advance(&mut self, graph: &Graph, distance: f32, can_traverse: &F) -> GameResult<()> -// where -// F: Fn(Edge) -> bool, -// { -// // Decrement the remaining frames for the next direction -// if let Some((direction, remaining)) = self.next_direction { -// if remaining > 0 { -// self.next_direction = Some((direction, remaining - 1)); -// } else { -// self.next_direction = None; -// } -// } + let edge = graph.find_edge(from, to).ok_or_else(|| { + crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(format!( + "Inconsistent state: Traverser is on a non-existent edge from {} to {}.", + from, to + ))) + })?; -// match self.position { -// Position::AtNode(node_id) => { -// // We're not moving, but a buffered direction is available. -// if let Some((next_direction, _)) = self.next_direction { -// if let Some(edge) = graph.find_edge_in_direction(node_id, next_direction) { -// if can_traverse(edge) { -// // Start moving in that direction -// self.position = Position::BetweenNodes { -// from: node_id, -// to: edge.target, -// traversed: distance.max(0.0), -// }; -// self.direction = next_direction; -// } else { -// return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement( -// format!( -// "Cannot traverse edge from {} to {} in direction {:?}", -// node_id, edge.target, next_direction -// ), -// ))); -// } -// } else { -// return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement( -// format!("No edge found in direction {:?} from node {}", next_direction, node_id), -// ))); -// } + let new_traversed = traversed + distance; -// self.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it -// } -// } -// Position::BetweenNodes { from, to, traversed } => { -// // There is no point in any of the next logic if we don't travel at all -// if distance <= 0.0 { -// return Ok(()); -// } + if new_traversed < edge.distance { + // Still on the same edge, just update the distance. + self.position = Position::BetweenNodes { + from, + to, + traversed: new_traversed, + }; + } else { + let overflow = new_traversed - edge.distance; + let mut moved = false; -// let edge = graph.find_edge(from, to).ok_or_else(|| { -// crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(format!( -// "Inconsistent state: Traverser is on a non-existent edge from {} to {}.", -// from, to -// ))) -// })?; + // If we buffered a direction, try to find an edge in that direction + if let Some((next_dir, _)) = self.next_direction { + if let Some(edge) = graph.find_edge_in_direction(to, next_dir) { + if can_traverse(edge) { + self.position = Position::BetweenNodes { + from: to, + to: edge.target, + traversed: overflow, + }; -// let new_traversed = traversed + distance; + self.direction = next_dir; // Remember our new direction + self.next_direction = None; // Consume the buffered direction + moved = true; + } + } + } -// if new_traversed < edge.distance { -// // Still on the same edge, just update the distance. -// self.position = Position::BetweenNodes { -// from, -// to, -// traversed: new_traversed, -// }; -// } else { -// let overflow = new_traversed - edge.distance; -// let mut moved = false; + // If we didn't move, try to continue in the current direction + if !moved { + if let Some(edge) = graph.find_edge_in_direction(to, self.direction) { + if can_traverse(edge) { + self.position = Position::BetweenNodes { + from: to, + to: edge.target, + traversed: overflow, + }; + } else { + self.position = Position::AtNode(to); + self.next_direction = None; + } + } else { + self.position = Position::AtNode(to); + self.next_direction = None; + } + } + } + } + } -// // If we buffered a direction, try to find an edge in that direction -// if let Some((next_dir, _)) = self.next_direction { -// if let Some(edge) = graph.find_edge_in_direction(to, next_dir) { -// if can_traverse(edge) { -// self.position = Position::BetweenNodes { -// from: to, -// to: edge.target, -// traversed: overflow, -// }; - -// self.direction = next_dir; // Remember our new direction -// self.next_direction = None; // Consume the buffered direction -// moved = true; -// } -// } -// } - -// // If we didn't move, try to continue in the current direction -// if !moved { -// if let Some(edge) = graph.find_edge_in_direction(to, self.direction) { -// if can_traverse(edge) { -// self.position = Position::BetweenNodes { -// from: to, -// to: edge.target, -// traversed: overflow, -// }; -// } else { -// self.position = Position::AtNode(to); -// self.next_direction = None; -// } -// } else { -// self.position = Position::AtNode(to); -// self.next_direction = None; -// } -// } -// } -// } -// } - -// Ok(()) -// } -// } + Ok(()) + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs index 2df3894..d07d55a 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -3,7 +3,7 @@ include!(concat!(env!("OUT_DIR"), "/atlas_data.rs")); use crate::constants::CANVAS_SIZE; -use crate::ecs::interact::interact_system; +use crate::ecs::interact::{interact_system, movement_system}; use crate::ecs::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource}; use crate::ecs::{DeltaTime, DirectionalAnimated, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity}; use crate::entity::direction::Direction; @@ -132,7 +132,11 @@ impl Game { let player = PlayerBundle { player: PlayerControlled, position: Position::AtNode(pacman_start_node), - velocity: Velocity::default(), + velocity: Velocity { + direction: Direction::Up, + next_direction: None, + speed: Some(1.0), + }, sprite: Renderable { sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png") .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?, @@ -164,7 +168,16 @@ impl Game { }, }); - schedule.add_systems((handle_input, interact_system, directional_render_system, render_system).chain()); + schedule.add_systems( + ( + handle_input, + interact_system, + movement_system, + directional_render_system, + render_system, + ) + .chain(), + ); // Spawn player world.spawn(player);