feat: ecs keyboard interactions

This commit is contained in:
2025-08-14 18:17:58 -05:00
parent b270318640
commit 0aa056a0ae
4 changed files with 295 additions and 173 deletions

View File

@@ -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<Map>,
delta_time: Res<DeltaTime>,
mut entities: Query<(&PlayerControlled, &mut Velocity, &mut Position)>,
mut errors: EventWriter<GameError>,
) {
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<GameEvent>,

View File

@@ -125,6 +125,7 @@ impl Position {
#[derive(Default, Component)]
pub struct Velocity {
pub direction: Direction,
pub next_direction: Option<(Direction, u8)>,
pub speed: Option<f32>,
}

View File

@@ -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<F>(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<F>(&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<F>(&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(())
}
}

View File

@@ -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);