refactor: improve representation of movement system

This commit is contained in:
2025-08-15 12:50:07 -05:00
parent bd811ee783
commit de86f383bf
8 changed files with 259 additions and 231 deletions

View File

@@ -4,14 +4,18 @@ use crate::systems::components::NodeId;
use super::direction::Direction; use super::direction::Direction;
/// Defines who can traverse a given edge. use bitflags::bitflags;
bitflags! {
/// Defines who can traverse a given edge using flags for fast checking.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EdgePermissions { pub struct TraversalFlags: u8 {
/// Anyone can use this edge. const PACMAN = 1 << 0;
#[default] const GHOST = 1 << 1;
All,
/// Only ghosts can use this edge. /// Convenience flag for edges that all entities can use
GhostsOnly, const ALL = Self::PACMAN.bits() | Self::GHOST.bits();
}
} }
/// Represents a directed edge from one node to another with a given weight (e.g., distance). /// Represents a directed edge from one node to another with a given weight (e.g., distance).
@@ -24,7 +28,7 @@ pub struct Edge {
/// The cardinal direction of this edge. /// The cardinal direction of this edge.
pub direction: Direction, pub direction: Direction,
/// Defines who is allowed to traverse this edge. /// Defines who is allowed to traverse this edge.
pub permissions: EdgePermissions, pub traversal_flags: TraversalFlags,
} }
/// Represents a node in the graph, defined by its position. /// Represents a node in the graph, defined by its position.
@@ -132,8 +136,8 @@ impl Graph {
return Err("To node does not exist."); return Err("To node does not exist.");
} }
let edge_a = self.add_edge(from, to, replace, distance, direction, EdgePermissions::default()); let edge_a = self.add_edge(from, to, replace, distance, direction, TraversalFlags::ALL);
let edge_b = self.add_edge(to, from, replace, distance, direction.opposite(), EdgePermissions::default()); let edge_b = self.add_edge(to, from, replace, distance, direction.opposite(), TraversalFlags::ALL);
if edge_a.is_err() && edge_b.is_err() { if edge_a.is_err() && edge_b.is_err() {
return Err("Failed to connect nodes in both directions."); return Err("Failed to connect nodes in both directions.");
@@ -161,7 +165,7 @@ impl Graph {
replace: bool, replace: bool,
distance: Option<f32>, distance: Option<f32>,
direction: Direction, direction: Direction,
permissions: EdgePermissions, traversal_flags: TraversalFlags,
) -> Result<(), &'static str> { ) -> Result<(), &'static str> {
let edge = Edge { let edge = Edge {
target: to, target: to,
@@ -180,7 +184,7 @@ impl Graph {
} }
}, },
direction, direction,
permissions, traversal_flags,
}; };
if from >= self.adjacency_list.len() { if from >= self.adjacency_list.len() {

View File

@@ -12,8 +12,8 @@ use crate::systems::{
blinking::blinking_system, blinking::blinking_system,
collision::collision_system, collision::collision_system,
components::{ components::{
Collider, CollisionLayer, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider, Collider, CollisionLayer, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider, Movable,
PacmanCollider, PlayerBundle, PlayerControlled, Position, Renderable, Score, ScoreResource, Velocity, MovementState, PacmanCollider, PlayerBundle, PlayerControlled, Position, Renderable, Score, ScoreResource,
}, },
control::player_system, control::player_system,
input::input_system, input::input_system,
@@ -133,11 +133,15 @@ impl Game {
let player = PlayerBundle { let player = PlayerBundle {
player: PlayerControlled, player: PlayerControlled,
position: Position::AtNode(pacman_start_node), position: Position {
velocity: Velocity { node: pacman_start_node,
direction: Direction::Left, edge_progress: None,
next_direction: Some((Direction::Left, 90)), },
movement_state: MovementState::Stopped,
movable: Movable {
speed: 1.15, speed: 1.15,
current_direction: Direction::Left,
requested_direction: Some(Direction::Left), // Start moving left immediately
}, },
sprite: Renderable { sprite: Renderable {
sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png") sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png")
@@ -170,14 +174,14 @@ impl Game {
world.insert_resource(DeltaTime(0f32)); world.insert_resource(DeltaTime(0f32));
world.add_observer( world.add_observer(
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, mut score: ResMut<ScoreResource>| match *event { |event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| match *event {
GameEvent::Command(command) => match command { GameEvent::Command(command) => match command {
GameCommand::Exit => { GameCommand::Exit => {
state.exit = true; state.exit = true;
} }
_ => {} _ => {}
}, },
GameEvent::Collision(a, b) => {} GameEvent::Collision(_a, _b) => {}
}, },
); );
@@ -218,7 +222,10 @@ impl Game {
}; };
let mut item = world.spawn(ItemBundle { let mut item = world.spawn(ItemBundle {
position: Position::AtNode(node_id), position: Position {
node: node_id,
edge_progress: None,
},
sprite: Renderable { sprite: Renderable {
sprite, sprite,
layer: 1, layer: 1,

View File

@@ -1,8 +1,7 @@
//! Map construction and building functionality. //! Map construction and building functionality.
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE}; use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
use crate::entity::direction::Direction; use crate::entity::direction::Direction;
use crate::entity::graph::{EdgePermissions, Graph, Node}; use crate::entity::graph::{Graph, Node, TraversalFlags};
use crate::map::parser::MapTileParser; use crate::map::parser::MapTileParser;
use crate::map::render::MapRenderer; use crate::map::render::MapRenderer;
use crate::systems::components::NodeId; use crate::systems::components::NodeId;
@@ -166,44 +165,6 @@ impl Map {
}) })
} }
/// Generates Item entities for pellets and energizers from the parsed map.
// pub fn generate_items(&self, atlas: &SpriteAtlas) -> GameResult<Vec<Item>> {
// // Pre-load sprites to avoid repeated texture lookups
// let pellet_sprite = SpriteAtlas::get_tile(atlas, "maze/pellet.png")
// .ok_or_else(|| MapError::InvalidConfig("Pellet texture not found".to_string()))?;
// let energizer_sprite = SpriteAtlas::get_tile(atlas, "maze/energizer.png")
// .ok_or_else(|| MapError::InvalidConfig("Energizer texture not found".to_string()))?;
// // Pre-allocate with estimated capacity (typical Pac-Man maps have ~240 pellets + 4 energizers)
// let mut items = Vec::with_capacity(250);
// // Parse the raw board once
// let parsed_map = MapTileParser::parse_board(RAW_BOARD)?;
// let map = parsed_map.tiles;
// // Iterate through the map and collect items more efficiently
// for (x, row) in map.iter().enumerate() {
// for (y, tile) in row.iter().enumerate() {
// match tile {
// MapTile::Pellet | MapTile::PowerPellet => {
// let grid_pos = IVec2::new(x as i32, y as i32);
// if let Some(&node_id) = self.grid_to_node.get(&grid_pos) {
// let (item_type, sprite) = match tile {
// MapTile::Pellet => (ItemType::Pellet, Sprite::new(pellet_sprite)),
// MapTile::PowerPellet => (ItemType::Energizer, Sprite::new(energizer_sprite)),
// _ => unreachable!(), // We already filtered for these types
// };
// items.push(Item::new(node_id, item_type, sprite));
// }
// }
// _ => {}
// }
// }
// }
// Ok(items)
// }
/// Renders a debug visualization with cursor-based highlighting. /// Renders a debug visualization with cursor-based highlighting.
/// ///
/// This function provides interactive debugging by highlighting the nearest node /// This function provides interactive debugging by highlighting the nearest node
@@ -304,7 +265,7 @@ impl Map {
false, false,
None, None,
Direction::Down, Direction::Down,
EdgePermissions::GhostsOnly, TraversalFlags::GHOST,
) )
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {e}")))?; .map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {e}")))?;
@@ -315,7 +276,7 @@ impl Map {
false, false,
None, None,
Direction::Up, Direction::Up,
EdgePermissions::GhostsOnly, TraversalFlags::GHOST,
) )
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {e}")))?; .map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {e}")))?;

View File

@@ -3,7 +3,10 @@ use bitflags::bitflags;
use glam::Vec2; use glam::Vec2;
use crate::{ use crate::{
entity::{direction::Direction, graph::Graph}, entity::{
direction::Direction,
graph::{Graph, TraversalFlags},
},
error::{EntityError, GameResult}, error::{EntityError, GameResult},
texture::{animated::AnimatedTexture, sprite::AtlasTile}, texture::{animated::AnimatedTexture, sprite::AtlasTile},
}; };
@@ -22,6 +25,17 @@ pub enum EntityType {
Wall, Wall,
} }
impl EntityType {
/// Returns the traversal flags for this entity type.
pub fn traversal_flags(&self) -> TraversalFlags {
match self {
EntityType::Player => TraversalFlags::PACMAN,
EntityType::Ghost => TraversalFlags::GHOST,
_ => TraversalFlags::empty(), // Static entities don't traverse
}
}
}
/// A component for entities that have a sprite, with a layer for ordering. /// A component for entities that have a sprite, with a layer for ordering.
/// ///
/// This is intended to be modified by other entities allowing animation. /// This is intended to be modified by other entities allowing animation.
@@ -42,21 +56,36 @@ pub struct DirectionalAnimated {
/// A unique identifier for a node, represented by its index in the graph's storage. /// A unique identifier for a node, represented by its index in the graph's storage.
pub type NodeId = usize; pub type NodeId = usize;
/// Represents the current position of an entity traversing the graph. /// Progress along an edge between two nodes.
/// #[derive(Debug, Clone, Copy, PartialEq)]
/// This enum allows for precise tracking of whether an entity is exactly at a node pub struct EdgeProgress {
/// or moving along an edge between two nodes. pub target_node: NodeId,
/// Progress from 0.0 (at source node) to 1.0 (at target node)
pub progress: f32,
}
/// Pure spatial position component - works for both static and dynamic entities.
#[derive(Component, Debug, Copy, Clone, PartialEq)] #[derive(Component, Debug, Copy, Clone, PartialEq)]
pub enum Position { pub struct Position {
/// The traverser is located exactly at a node. /// The current/primary node this entity is at or traveling from
AtNode(NodeId), pub node: NodeId,
/// The traverser is on an edge between two nodes. /// If Some, entity is traveling between nodes. If None, entity is stationary at node.
BetweenNodes { pub edge_progress: Option<EdgeProgress>,
from: NodeId, }
to: NodeId,
/// The floating-point distance traversed along the edge from the `from` node. /// Explicit movement state - only for entities that can move.
traversed: f32, #[derive(Component, Debug, Clone, Copy, PartialEq)]
}, pub enum MovementState {
Stopped,
Moving { direction: Direction },
}
/// Movement capability and parameters - only for entities that can move.
#[derive(Component, Debug, Clone, Copy)]
pub struct Movable {
pub speed: f32,
pub current_direction: Direction,
pub requested_direction: Option<Direction>,
} }
impl Position { impl Position {
@@ -69,18 +98,26 @@ impl Position {
/// ///
/// Returns an `EntityError` if the node or edge is not found. /// Returns an `EntityError` if the node or edge is not found.
pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult<Vec2> { pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult<Vec2> {
let pos = match self { let pos = match &self.edge_progress {
Position::AtNode(node_id) => { None => {
let node = graph.get_node(*node_id).ok_or(EntityError::NodeNotFound(*node_id))?; // Entity is stationary at a node
let node = graph.get_node(self.node).ok_or(EntityError::NodeNotFound(self.node))?;
node.position node.position
} }
Position::BetweenNodes { from, to, traversed } => { Some(edge_progress) => {
let from_node = graph.get_node(*from).ok_or(EntityError::NodeNotFound(*from))?; // Entity is traveling between nodes
let to_node = graph.get_node(*to).ok_or(EntityError::NodeNotFound(*to))?; let from_node = graph.get_node(self.node).ok_or(EntityError::NodeNotFound(self.node))?;
let edge = graph let to_node = graph
.find_edge(*from, *to) .get_node(edge_progress.target_node)
.ok_or(EntityError::EdgeNotFound { from: *from, to: *to })?; .ok_or(EntityError::NodeNotFound(edge_progress.target_node))?;
from_node.position + (to_node.position - from_node.position) * (traversed / edge.distance)
// For zero-distance edges (tunnels), progress >= 1.0 means we're at the target
if edge_progress.progress >= 1.0 {
to_node.position
} else {
// Interpolate position based on progress
from_node.position + (to_node.position - from_node.position) * edge_progress.progress
}
} }
}; };
@@ -93,49 +130,36 @@ impl Position {
impl Default for Position { impl Default for Position {
fn default() -> Self { fn default() -> Self {
Position::AtNode(0) Position {
node: 0,
edge_progress: None,
}
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
impl Position { impl Position {
/// Returns `true` if the position is exactly at a node. /// Returns `true` if the position is exactly at a node (not traveling).
pub fn is_at_node(&self) -> bool { pub fn is_at_node(&self) -> bool {
matches!(self, Position::AtNode(_)) self.edge_progress.is_none()
} }
/// Returns the `NodeId` of the current or most recently departed node. /// Returns the `NodeId` of the current node (source of travel if moving).
#[allow(clippy::wrong_self_convention)] pub fn current_node(&self) -> NodeId {
pub fn from_node_id(&self) -> NodeId { self.node
match self {
Position::AtNode(id) => *id,
Position::BetweenNodes { from, .. } => *from,
}
} }
/// Returns the `NodeId` of the destination node, if currently on an edge. /// Returns the `NodeId` of the destination node, if currently traveling.
#[allow(clippy::wrong_self_convention)] pub fn target_node(&self) -> Option<NodeId> {
pub fn to_node_id(&self) -> Option<NodeId> { self.edge_progress.as_ref().map(|ep| ep.target_node)
match self {
Position::AtNode(_) => None,
Position::BetweenNodes { to, .. } => Some(*to),
}
} }
/// Returns `true` if the traverser is stopped at a node. /// Returns `true` if the entity is traveling between nodes.
pub fn is_stopped(&self) -> bool { pub fn is_moving(&self) -> bool {
matches!(self, Position::AtNode(_)) self.edge_progress.is_some()
} }
} }
/// A component for entities that have a velocity, with a direction and speed.
#[derive(Default, Component)]
pub struct Velocity {
pub direction: Direction,
pub next_direction: Option<(Direction, u8)>,
pub speed: f32,
}
bitflags! { bitflags! {
#[derive(Component, Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Component, Default, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CollisionLayer: u8 { pub struct CollisionLayer: u8 {
@@ -168,7 +192,8 @@ pub struct Score(pub u32);
pub struct PlayerBundle { pub struct PlayerBundle {
pub player: PlayerControlled, pub player: PlayerControlled,
pub position: Position, pub position: Position,
pub velocity: Velocity, pub movement_state: MovementState,
pub movable: Movable,
pub sprite: Renderable, pub sprite: Renderable,
pub directional_animated: DirectionalAnimated, pub directional_animated: DirectionalAnimated,
pub entity_type: EntityType, pub entity_type: EntityType,

View File

@@ -8,19 +8,19 @@ use bevy_ecs::{
use crate::{ use crate::{
error::GameError, error::GameError,
events::{GameCommand, GameEvent}, events::{GameCommand, GameEvent},
systems::components::{GlobalState, PlayerControlled, Velocity}, systems::components::{GlobalState, Movable, PlayerControlled},
}; };
// Handles // Handles player input and control
pub fn player_system( pub fn player_system(
mut events: EventReader<GameEvent>, mut events: EventReader<GameEvent>,
mut state: ResMut<GlobalState>, mut state: ResMut<GlobalState>,
mut players: Query<&mut Velocity, With<PlayerControlled>>, mut players: Query<&mut Movable, With<PlayerControlled>>,
mut errors: EventWriter<GameError>, mut errors: EventWriter<GameError>,
) { ) {
// Get the player's velocity (handling to ensure there is only one player) // Get the player's movable component (ensuring there is only one player)
let mut velocity = match players.single_mut() { let mut movable = match players.single_mut() {
Ok(velocity) => velocity, Ok(movable) => movable,
Err(e) => { Err(e) => {
errors.write(GameError::InvalidState(format!("No/multiple entities queried for player system: {}", e)).into()); errors.write(GameError::InvalidState(format!("No/multiple entities queried for player system: {}", e)).into());
return; return;
@@ -32,7 +32,7 @@ pub fn player_system(
match event { match event {
GameEvent::Command(command) => match command { GameEvent::Command(command) => match command {
GameCommand::MovePlayer(direction) => { GameCommand::MovePlayer(direction) => {
velocity.next_direction = Some((*direction, 90)); movable.requested_direction = Some(*direction);
} }
GameCommand::Exit => { GameCommand::Exit => {
state.exit = true; state.exit = true;

View File

@@ -1,131 +1,160 @@
use crate::entity::graph::{Edge, EdgePermissions}; use crate::entity::graph::Edge;
use crate::error::{EntityError, GameError}; use crate::error::{EntityError, GameError};
use crate::map::builder::Map; use crate::map::builder::Map;
use crate::systems::components::{DeltaTime, EntityType, Position, Velocity}; use crate::systems::components::{DeltaTime, EdgeProgress, EntityType, Movable, MovementState, Position};
use bevy_ecs::event::EventWriter; use bevy_ecs::event::EventWriter;
use bevy_ecs::system::{Query, Res}; use bevy_ecs::system::{Query, Res};
fn can_traverse(entity_type: EntityType, edge: Edge) -> bool { fn can_traverse(entity_type: EntityType, edge: Edge) -> bool {
match entity_type { let entity_flags = entity_type.traversal_flags();
EntityType::Player => matches!(edge.permissions, EdgePermissions::All), edge.traversal_flags.contains(entity_flags)
EntityType::Ghost => matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly),
_ => matches!(edge.permissions, EdgePermissions::All),
}
} }
pub fn movement_system( pub fn movement_system(
map: Res<Map>, map: Res<Map>,
delta_time: Res<DeltaTime>, delta_time: Res<DeltaTime>,
mut entities: Query<(&mut Velocity, &mut Position, &EntityType)>, mut entities: Query<(&mut MovementState, &mut Movable, &mut Position, &EntityType)>,
mut errors: EventWriter<GameError>, mut errors: EventWriter<GameError>,
) { ) {
for (mut velocity, mut position, entity_type) in entities.iter_mut() { for (mut movement_state, mut movable, mut position, entity_type) in entities.iter_mut() {
let distance = velocity.speed * 60.0 * delta_time.0; let distance = movable.speed * 60.0 * delta_time.0;
// Decrement the remaining frames for the next direction match *movement_state {
if let Some((direction, remaining)) = velocity.next_direction { MovementState::Stopped => {
if remaining > 0 { // Check if we have a requested direction to start moving
velocity.next_direction = Some((direction, remaining - 1)); if let Some(requested_direction) = movable.requested_direction {
} else { if let Some(edge) = map.graph.find_edge_in_direction(position.node, requested_direction) {
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 can_traverse(*entity_type, edge) { if can_traverse(*entity_type, edge) {
// Start moving in that direction // Start moving in the requested direction
*position = Position::BetweenNodes { let progress = if edge.distance > 0.0 {
from: node_id, distance / edge.distance
to: edge.target, } else {
traversed: distance, // Zero-distance edge (tunnels) - immediately teleport
tracing::debug!("Entity entering tunnel from node {} to node {}", position.node, edge.target);
1.0
};
position.edge_progress = Some(EdgeProgress {
target_node: edge.target,
progress,
});
movable.current_direction = requested_direction;
movable.requested_direction = None;
*movement_state = MovementState::Moving {
direction: requested_direction,
}; };
velocity.direction = next_direction;
velocity.next_direction = None;
} }
} else { } else {
errors.write( errors.write(
EntityError::InvalidMovement(format!( EntityError::InvalidMovement(format!(
"No edge found in direction {:?} from node {}", "No edge found in direction {:?} from node {}",
next_direction, node_id requested_direction, position.node
)) ))
.into(), .into(),
); );
} }
} }
} }
Position::BetweenNodes { from, to, traversed } => { MovementState::Moving { direction } => {
// There is no point in any of the next logic if we don't travel at all // Continue moving or handle node transitions
if distance <= 0.0 { let current_node = position.node;
return; if let Some(edge_progress) = &mut position.edge_progress {
// Extract target node before mutable operations
let target_node = edge_progress.target_node;
// Get the current edge for distance calculation
let edge = map.graph.find_edge(current_node, target_node);
if let Some(edge) = edge {
// Update progress along the edge
if edge.distance > 0.0 {
edge_progress.progress += distance / edge.distance;
} else {
// Zero-distance edge (tunnels) - immediately complete
edge_progress.progress = 1.0;
} }
let edge = map if edge_progress.progress >= 1.0 {
.graph // Reached the target node
.find_edge(from, to) let overflow = if edge.distance > 0.0 {
.ok_or_else(|| { (edge_progress.progress - 1.0) * edge.distance
} else {
// Zero-distance edge - use remaining distance for overflow
distance
};
position.node = target_node;
position.edge_progress = None;
let mut continued_moving = false;
// Try to use requested direction first
if let Some(requested_direction) = movable.requested_direction {
if let Some(next_edge) = map.graph.find_edge_in_direction(position.node, requested_direction) {
if can_traverse(*entity_type, next_edge) {
let next_progress = if next_edge.distance > 0.0 {
overflow / next_edge.distance
} else {
// Zero-distance edge - immediately complete
1.0
};
position.edge_progress = Some(EdgeProgress {
target_node: next_edge.target,
progress: next_progress,
});
movable.current_direction = requested_direction;
movable.requested_direction = None;
*movement_state = MovementState::Moving {
direction: requested_direction,
};
continued_moving = true;
}
}
}
// If no requested direction or it failed, try to continue in current direction
if !continued_moving {
if let Some(next_edge) = map.graph.find_edge_in_direction(position.node, direction) {
if can_traverse(*entity_type, next_edge) {
let next_progress = if next_edge.distance > 0.0 {
overflow / next_edge.distance
} else {
// Zero-distance edge - immediately complete
1.0
};
position.edge_progress = Some(EdgeProgress {
target_node: next_edge.target,
progress: next_progress,
});
// Keep current direction and movement state
continued_moving = true;
}
}
}
// If we couldn't continue moving, stop
if !continued_moving {
*movement_state = MovementState::Stopped;
movable.requested_direction = None;
}
}
} else {
// Edge not found - this is an inconsistent state
errors.write( errors.write(
EntityError::InvalidMovement(format!( EntityError::InvalidMovement(format!(
"Inconsistent state: Traverser is on a non-existent edge from {} to {}.", "Inconsistent state: Moving on non-existent edge from {} to {}",
from, to current_node, target_node
)) ))
.into(), .into(),
); );
return; *movement_state = MovementState::Stopped;
}) position.edge_progress = None;
.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 can_traverse(*entity_type, 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 can_traverse(*entity_type, edge) {
*position = Position::BetweenNodes {
from: to,
to: edge.target,
traversed: overflow,
};
} else {
*position = Position::AtNode(to);
velocity.next_direction = None;
} }
} else { } else {
*position = Position::AtNode(to); // Movement state says moving but no edge progress - this shouldn't happen
velocity.next_direction = None; errors.write(EntityError::InvalidMovement("Entity in Moving state but no edge progress".to_string()).into());
} *movement_state = MovementState::Stopped;
}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use crate::error::{GameError, TextureError}; use crate::error::{GameError, TextureError};
use crate::map::builder::Map; use crate::map::builder::Map;
use crate::systems::components::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity}; use crate::systems::components::{DeltaTime, DirectionalAnimated, Movable, MovementState, Position, Renderable};
use crate::texture::sprite::SpriteAtlas; use crate::texture::sprite::SpriteAtlas;
use bevy_ecs::entity::Entity; use bevy_ecs::entity::Entity;
use bevy_ecs::event::EventWriter; use bevy_ecs::event::EventWriter;
@@ -13,15 +13,17 @@ use sdl2::video::Window;
/// This runs before the render system so it can update the sprite based on the current direction of travel, as well as whether the entity is moving. /// This runs before the render system so it can update the sprite based on the current direction of travel, as well as whether the entity is moving.
pub fn directional_render_system( pub fn directional_render_system(
dt: Res<DeltaTime>, dt: Res<DeltaTime>,
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable, &Position)>, mut renderables: Query<(&MovementState, &Movable, &mut DirectionalAnimated, &mut Renderable)>,
mut errors: EventWriter<GameError>, mut errors: EventWriter<GameError>,
) { ) {
for (velocity, mut texture, mut renderable, position) in renderables.iter_mut() { for (movement_state, movable, mut texture, mut renderable) in renderables.iter_mut() {
let stopped = matches!(position, Position::AtNode(_)); let stopped = matches!(movement_state, MovementState::Stopped);
let current_direction = movable.current_direction;
let texture = if stopped { let texture = if stopped {
texture.stopped_textures[velocity.direction.as_usize()].as_mut() texture.stopped_textures[current_direction.as_usize()].as_mut()
} else { } else {
texture.textures[velocity.direction.as_usize()].as_mut() texture.textures[current_direction.as_usize()].as_mut()
}; };
if let Some(texture) = texture { if let Some(texture) = texture {

View File

@@ -1,5 +1,5 @@
use pacman::entity::direction::Direction; use pacman::entity::direction::Direction;
use pacman::entity::graph::{EdgePermissions, Graph, Node}; use pacman::entity::graph::{Graph, Node, TraversalFlags};
fn create_test_graph() -> Graph { fn create_test_graph() -> Graph {
let mut graph = Graph::new(); let mut graph = Graph::new();
@@ -78,11 +78,11 @@ fn test_graph_edge_permissions() {
}); });
graph graph
.add_edge(node1, node2, false, None, Direction::Right, EdgePermissions::GhostsOnly) .add_edge(node1, node2, false, None, Direction::Right, TraversalFlags::GHOST)
.unwrap(); .unwrap();
let edge = graph.find_edge_in_direction(node1, Direction::Right).unwrap(); let edge = graph.find_edge_in_direction(node1, Direction::Right).unwrap();
assert_eq!(edge.permissions, EdgePermissions::GhostsOnly); assert_eq!(edge.traversal_flags, TraversalFlags::GHOST);
} }
#[test] #[test]
@@ -118,21 +118,21 @@ fn should_error_on_negative_edge_distance() {
position: glam::Vec2::new(16.0, 0.0), position: glam::Vec2::new(16.0, 0.0),
}); });
let result = graph.add_edge(node1, node2, false, Some(-1.0), Direction::Right, EdgePermissions::All); let result = graph.add_edge(node1, node2, false, Some(-1.0), Direction::Right, TraversalFlags::ALL);
assert!(result.is_err()); assert!(result.is_err());
} }
#[test] #[test]
fn should_error_on_duplicate_edge_without_replace() { fn should_error_on_duplicate_edge_without_replace() {
let mut graph = create_test_graph(); let mut graph = create_test_graph();
let result = graph.add_edge(0, 1, false, None, Direction::Right, EdgePermissions::All); let result = graph.add_edge(0, 1, false, None, Direction::Right, TraversalFlags::ALL);
assert!(result.is_err()); assert!(result.is_err());
} }
#[test] #[test]
fn should_allow_replacing_an_edge() { fn should_allow_replacing_an_edge() {
let mut graph = create_test_graph(); let mut graph = create_test_graph();
let result = graph.add_edge(0, 1, true, Some(42.0), Direction::Right, EdgePermissions::All); let result = graph.add_edge(0, 1, true, Some(42.0), Direction::Right, TraversalFlags::ALL);
assert!(result.is_ok()); assert!(result.is_ok());
let edge = graph.find_edge(0, 1).unwrap(); let edge = graph.find_edge(0, 1).unwrap();