From 78300bdf9cd4254fd19f1aa18a02be7c264e7710 Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 16 Aug 2025 11:44:10 -0500 Subject: [PATCH] feat: rewrite movement systems separately for player/ghosts --- src/game/mod.rs | 58 ++---- src/systems/collision.rs | 5 +- src/systems/components.rs | 59 ++---- src/systems/control.rs | 58 ------ src/systems/debug.rs | 2 +- src/systems/ghost.rs | 70 +++++-- src/systems/mod.rs | 2 +- src/systems/movement.rs | 421 +++++++++++++++++++++----------------- src/systems/player.rs | 143 +++++++++++++ src/systems/profiling.rs | 3 +- src/systems/render.rs | 12 +- 11 files changed, 476 insertions(+), 357 deletions(-) delete mode 100644 src/systems/control.rs create mode 100644 src/systems/player.rs diff --git a/src/game/mod.rs b/src/game/mod.rs index 895d951..2d95212 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -8,23 +8,22 @@ use crate::error::{GameError, GameResult, TextureError}; use crate::events::GameEvent; use crate::map::builder::Map; use crate::systems::blinking::Blinking; -use crate::systems::movement::{Movable, MovementState, Position}; +use crate::systems::movement::{BufferedDirection, Position, Velocity}; +use crate::systems::player::player_movement_system; use crate::systems::profiling::SystemId; use crate::systems::{ audio::{audio_system, AudioEvent, AudioResource}, blinking::blinking_system, collision::collision_system, components::{ - AudioState, Collider, DeltaTime, DirectionalAnimated, EntityType, GhostBehavior, GhostBundle, GhostCollider, GhostType, - GlobalState, ItemBundle, ItemCollider, PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, - ScoreResource, + AudioState, Collider, DeltaTime, DirectionalAnimated, EntityType, Ghost, GhostBundle, GhostCollider, GlobalState, + ItemBundle, ItemCollider, PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, ScoreResource, }, - control::player_system, debug::{debug_render_system, DebugState, DebugTextureResource}, - ghost::ghost_system, + ghost::ghost_movement_system, input::input_system, item::item_system, - movement::movement_system, + player::player_control_system, profiling::{profile, SystemTimings}, render::{directional_render_system, dirty_render_system, render_system, BackbufferResource, MapTextureResource}, }; @@ -159,16 +158,12 @@ impl Game { let player = PlayerBundle { player: PlayerControlled, - position: Position { - node: pacman_start_node, - edge_progress: None, - }, - movement_state: MovementState::Stopped, - movable: Movable { + position: Position::Stopped { node: pacman_start_node }, + velocity: Velocity { speed: 1.15, - current_direction: Direction::Left, - requested_direction: Some(Direction::Left), // Start moving left immediately + direction: Direction::Left, }, + buffered_direction: BufferedDirection::None, sprite: Renderable { sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png") .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?, @@ -214,9 +209,9 @@ impl Game { schedule.add_systems( ( profile(SystemId::Input, input_system), - profile(SystemId::Player, player_system), - profile(SystemId::Ghost, ghost_system), - profile(SystemId::Movement, movement_system), + profile(SystemId::PlayerControls, player_control_system), + profile(SystemId::PlayerMovement, player_movement_system), + profile(SystemId::Ghost, ghost_movement_system), profile(SystemId::Collision, collision_system), profile(SystemId::Item, item_system), profile(SystemId::Audio, audio_system), @@ -270,10 +265,7 @@ impl Game { }; let mut item = world.spawn(ItemBundle { - position: Position { - node: node_id, - edge_progress: None, - }, + position: Position::Stopped { node: node_id }, sprite: Renderable { sprite, layer: 1, @@ -301,10 +293,10 @@ impl Game { let ghost_start_positions = { let map = world.resource::(); [ - (GhostType::Blinky, map.start_positions.blinky), - (GhostType::Pinky, map.start_positions.pinky), - (GhostType::Inky, map.start_positions.inky), - (GhostType::Clyde, map.start_positions.clyde), + (Ghost::Blinky, map.start_positions.blinky), + (Ghost::Pinky, map.start_positions.pinky), + (Ghost::Inky, map.start_positions.inky), + (Ghost::Clyde, map.start_positions.clyde), ] }; @@ -364,17 +356,11 @@ impl Game { } GhostBundle { - ghost_type, - ghost_behavior: GhostBehavior::default(), - position: Position { - node: start_node, - edge_progress: None, - }, - movement_state: MovementState::Stopped, - movable: Movable { + ghost: ghost_type, + position: Position::Stopped { node: start_node }, + velocity: Velocity { speed: ghost_type.base_speed(), - current_direction: Direction::Left, - requested_direction: Some(Direction::Left), // Start with some movement + direction: Direction::Left, }, sprite: Renderable { sprite: SpriteAtlas::get_tile(atlas, &format!("ghost/{}/left_a.png", ghost_type.as_str())).ok_or_else( diff --git a/src/systems/collision.rs b/src/systems/collision.rs index cd291a7..dc3c180 100644 --- a/src/systems/collision.rs +++ b/src/systems/collision.rs @@ -19,7 +19,10 @@ pub fn collision_system( // Check PACMAN × ITEM collisions for (pacman_entity, pacman_pos, pacman_collider) in pacman_query.iter() { for (item_entity, item_pos, item_collider) in item_query.iter() { - match (pacman_pos.get_pixel_pos(&map.graph), item_pos.get_pixel_pos(&map.graph)) { + match ( + pacman_pos.get_pixel_position(&map.graph), + item_pos.get_pixel_position(&map.graph), + ) { (Ok(pacman_pixel), Ok(item_pixel)) => { // Calculate the distance between the two entities's precise pixel positions let distance = pacman_pixel.distance(item_pixel); diff --git a/src/systems/components.rs b/src/systems/components.rs index 91cec2b..5a9d4d7 100644 --- a/src/systems/components.rs +++ b/src/systems/components.rs @@ -3,7 +3,7 @@ use bitflags::bitflags; use crate::{ entity::graph::TraversalFlags, - systems::movement::{Movable, MovementState, Position}, + systems::movement::{BufferedDirection, Position, Velocity}, texture::{animated::AnimatedTexture, sprite::AtlasTile}, }; @@ -11,61 +11,42 @@ use crate::{ #[derive(Default, Component)] pub struct PlayerControlled; -/// The four classic ghost types. #[derive(Component, Debug, Clone, Copy, PartialEq, Eq)] -pub enum GhostType { +pub enum Ghost { Blinky, Pinky, Inky, Clyde, } -impl GhostType { +impl Ghost { /// Returns the ghost type name for atlas lookups. pub fn as_str(self) -> &'static str { match self { - GhostType::Blinky => "blinky", - GhostType::Pinky => "pinky", - GhostType::Inky => "inky", - GhostType::Clyde => "clyde", + Ghost::Blinky => "blinky", + Ghost::Pinky => "pinky", + Ghost::Inky => "inky", + Ghost::Clyde => "clyde", } } /// Returns the base movement speed for this ghost type. pub fn base_speed(self) -> f32 { match self { - GhostType::Blinky => 1.0, - GhostType::Pinky => 0.95, - GhostType::Inky => 0.9, - GhostType::Clyde => 0.85, + Ghost::Blinky => 1.0, + Ghost::Pinky => 0.95, + Ghost::Inky => 0.9, + Ghost::Clyde => 0.85, } } /// Returns the ghost's color for debug rendering. pub fn debug_color(&self) -> sdl2::pixels::Color { match self { - GhostType::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red - GhostType::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink - GhostType::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan - GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange - } - } -} - -/// Ghost AI behavior component - controls randomized movement decisions. -#[derive(Component)] -pub struct GhostBehavior { - /// Timer for making new direction decisions - pub decision_timer: f32, - /// Interval between direction decisions (in seconds) - pub decision_interval: f32, -} - -impl Default for GhostBehavior { - fn default() -> Self { - Self { - decision_timer: 0.0, - decision_interval: 0.5, // Make decisions every half second + Ghost::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red + Ghost::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink + Ghost::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan + Ghost::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange } } } @@ -135,8 +116,8 @@ pub struct ItemCollider; pub struct PlayerBundle { pub player: PlayerControlled, pub position: Position, - pub movement_state: MovementState, - pub movable: Movable, + pub velocity: Velocity, + pub buffered_direction: BufferedDirection, pub sprite: Renderable, pub directional_animated: DirectionalAnimated, pub entity_type: EntityType, @@ -155,11 +136,9 @@ pub struct ItemBundle { #[derive(Bundle)] pub struct GhostBundle { - pub ghost_type: GhostType, - pub ghost_behavior: GhostBehavior, + pub ghost: Ghost, pub position: Position, - pub movement_state: MovementState, - pub movable: Movable, + pub velocity: Velocity, pub sprite: Renderable, pub directional_animated: DirectionalAnimated, pub entity_type: EntityType, diff --git a/src/systems/control.rs b/src/systems/control.rs deleted file mode 100644 index 84efa83..0000000 --- a/src/systems/control.rs +++ /dev/null @@ -1,58 +0,0 @@ -use bevy_ecs::{ - event::{EventReader, EventWriter}, - prelude::ResMut, - query::With, - system::Query, -}; - -use crate::{ - error::GameError, - events::{GameCommand, GameEvent}, - systems::components::{AudioState, GlobalState, PlayerControlled}, - systems::debug::DebugState, - systems::movement::Movable, -}; - -// Handles player input and control -pub fn player_system( - mut events: EventReader, - mut state: ResMut, - mut debug_state: ResMut, - mut audio_state: ResMut, - mut players: Query<&mut Movable, With>, - mut errors: EventWriter, -) { - // Get the player's movable component (ensuring there is only one player) - let mut movable = match players.single_mut() { - Ok(movable) => movable, - Err(e) => { - errors.write(GameError::InvalidState(format!( - "No/multiple entities queried for player system: {}", - e - ))); - return; - } - }; - - // Handle events - for event in events.read() { - if let GameEvent::Command(command) = event { - match command { - GameCommand::MovePlayer(direction) => { - movable.requested_direction = Some(*direction); - } - GameCommand::Exit => { - state.exit = true; - } - GameCommand::ToggleDebug => { - *debug_state = debug_state.next(); - } - GameCommand::MuteAudio => { - audio_state.muted = !audio_state.muted; - tracing::info!("Audio {}", if audio_state.muted { "muted" } else { "unmuted" }); - } - _ => {} - } - } - } -} diff --git a/src/systems/debug.rs b/src/systems/debug.rs index 428add0..f106a23 100644 --- a/src/systems/debug.rs +++ b/src/systems/debug.rs @@ -187,7 +187,7 @@ pub fn debug_render_system( DebugState::Collision => { debug_canvas.set_draw_color(Color::GREEN); for (collider, position) in colliders.iter() { - let pos = position.get_pixel_pos(&map.graph).unwrap(); + let pos = position.get_pixel_position(&map.graph).unwrap(); // Transform position and size using common methods let (x, y) = transform_position((pos.x, pos.y), output_size, logical_size); diff --git a/src/systems/ghost.rs b/src/systems/ghost.rs index 65bd29f..e477d27 100644 --- a/src/systems/ghost.rs +++ b/src/systems/ghost.rs @@ -3,11 +3,11 @@ use rand::prelude::*; use smallvec::SmallVec; use crate::{ - entity::direction::Direction, + entity::{direction::Direction, graph::Edge}, map::builder::Map, systems::{ - components::{DeltaTime, EntityType, GhostBehavior, GhostType}, - movement::{Movable, Position}, + components::{DeltaTime, Ghost}, + movement::{Position, Velocity}, }, }; @@ -15,27 +15,55 @@ use crate::{ /// /// This system runs on all ghosts and makes periodic decisions about /// which direction to move in when they reach intersections. -pub fn ghost_system( +pub fn ghost_movement_system( map: Res, delta_time: Res, - mut ghosts: Query<(&mut GhostBehavior, &mut Movable, &Position, &EntityType, &GhostType)>, + mut ghosts: Query<(&mut Ghost, &mut Velocity, &mut Position)>, ) { - for (mut ghost_behavior, mut movable, position, entity_type, _ghost_type) in ghosts.iter_mut() { - // Only process ghosts - if *entity_type != EntityType::Ghost { - continue; - } + for (mut ghost, mut velocity, mut position) in ghosts.iter_mut() { + let mut distance = velocity.speed * 60.0 * delta_time.0; + loop { + match *position { + Position::Stopped { node: current_node } => { + let intersection = &map.graph.adjacency_list[current_node]; + let opposite = velocity.direction.opposite(); - // Update decision timer - ghost_behavior.decision_timer += delta_time.0; + let mut non_opposite_options: SmallVec<[Edge; 3]> = SmallVec::new(); - // Check if we should make a new direction decision - let should_decide = ghost_behavior.decision_timer >= ghost_behavior.decision_interval; - let at_intersection = position.is_at_node(); + // 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 should_decide && at_intersection { - choose_random_direction(&map, &mut movable, position); - ghost_behavior.decision_timer = 0.0; + let new_edge: Edge = if non_opposite_options.is_empty() { + if let Some(edge) = intersection.get(opposite) { + edge + } else { + break; + } + } else { + *non_opposite_options.choose(&mut SmallRng::from_os_rng()).unwrap() + }; + + velocity.direction = new_edge.direction; + *position = Position::Moving { + from: current_node, + to: new_edge.target, + remaining_distance: new_edge.distance, + }; + } + Position::Moving { .. } => { + if let Some(overflow) = position.tick(distance) { + distance = overflow; + } else { + break; + } + } + } } } } @@ -44,7 +72,7 @@ pub fn ghost_system( /// /// 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, movable: &mut Movable, position: &Position) { +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]; @@ -64,14 +92,14 @@ fn choose_random_direction(map: &Map, movable: &mut Movable, position: &Position let mut rng = SmallRng::from_os_rng(); // Filter out the opposite direction if possible, but allow it if we have limited options - let opposite = movable.current_direction.opposite(); + 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) { - movable.requested_direction = Some(*random_direction); + velocity.direction = *random_direction; } } } diff --git a/src/systems/mod.rs b/src/systems/mod.rs index a85e74b..fab80f4 100644 --- a/src/systems/mod.rs +++ b/src/systems/mod.rs @@ -7,12 +7,12 @@ pub mod audio; pub mod blinking; pub mod collision; pub mod components; -pub mod control; pub mod debug; pub mod formatting; pub mod ghost; pub mod input; pub mod item; pub mod movement; +pub mod player; pub mod profiling; pub mod render; diff --git a/src/systems/movement.rs b/src/systems/movement.rs index 3367ea0..ae34c28 100644 --- a/src/systems/movement.rs +++ b/src/systems/movement.rs @@ -2,48 +2,44 @@ 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}; +use crate::systems::components::{DeltaTime, EntityType, PlayerControlled}; 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. pub type NodeId = usize; -/// Progress along an edge between two nodes. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct EdgeProgress { - pub target_node: NodeId, - /// Progress from 0.0 (at source node) to 1.0 (at target node) - pub progress: f32, +/// A component that represents the speed and cardinal direction of an entity. +/// Speed is static, only applied when the entity has an edge to traverse. +/// Direction is dynamic, but is controlled externally. +#[derive(Component, Debug, Copy, Clone, PartialEq)] +pub struct Velocity { + pub speed: f32, + pub direction: Direction, +} + +/// A component that represents a direction change that is only remembered for a period of time. +/// This is used to allow entities to change direction before they reach their current target node (which consumes their buffered direction). +#[derive(Component, Debug, Copy, Clone, PartialEq)] +pub enum BufferedDirection { + None, + Some { direction: Direction, remaining_time: f32 }, } /// Pure spatial position component - works for both static and dynamic entities. -#[derive(Component, Debug, Copy, Clone, PartialEq, Default)] -pub struct Position { - /// The current/primary node this entity is at or traveling from - pub node: NodeId, - /// If Some, entity is traveling between nodes. If None, entity is stationary at node. - pub edge_progress: Option, -} - -/// Explicit movement state - only for entities that can move. -#[derive(Component, Debug, Clone, Copy, PartialEq, Default)] -pub enum MovementState { - #[default] - Stopped, - Moving { - direction: Direction, +#[derive(Component, Debug, Copy, Clone, PartialEq)] +pub enum Position { + Stopped { + node: NodeId, + }, + Moving { + from: NodeId, + to: NodeId, + remaining_distance: f32, }, -} - -/// 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, } impl Position { @@ -55,26 +51,32 @@ impl Position { /// # Errors /// /// Returns an `EntityError` if the node or edge is not found. - pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult { - let pos = match &self.edge_progress { - None => { + pub fn get_pixel_position(&self, graph: &Graph) -> GameResult { + let pos = match &self { + Position::Stopped { node } => { // Entity is stationary at a node - let node = graph.get_node(self.node).ok_or(EntityError::NodeNotFound(self.node))?; + let node = graph.get_node(*node).ok_or(EntityError::NodeNotFound(*node))?; node.position } - Some(edge_progress) => { + Position::Moving { + from, + to, + remaining_distance, + } => { // Entity is traveling between nodes - let from_node = graph.get_node(self.node).ok_or(EntityError::NodeNotFound(self.node))?; - let to_node = graph - .get_node(edge_progress.target_node) - .ok_or(EntityError::NodeNotFound(edge_progress.target_node))?; + let from_node = graph.get_node(*from).ok_or(EntityError::NodeNotFound(*from))?; + let to_node = graph.get_node(*to).ok_or(EntityError::NodeNotFound(*to))?; + let edge = graph + .find_edge(*from, *to) + .ok_or(EntityError::EdgeNotFound { from: *from, to: *to })?; // For zero-distance edges (tunnels), progress >= 1.0 means we're at the target - if edge_progress.progress >= 1.0 { + if edge.distance == 0.0 { to_node.position } else { // Interpolate position based on progress - from_node.position + (to_node.position - from_node.position) * edge_progress.progress + let progress = 1.0 - (*remaining_distance / edge.distance); + from_node.position.lerp(to_node.position, progress) } } }; @@ -84,183 +86,218 @@ impl Position { pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32, )) } -} -#[allow(dead_code)] -impl Position { + /// Moves the position by a given distance towards it's current target node. + /// + /// Returns the overflow distance, if any. + pub fn tick(&mut self, distance: f32) -> Option { + if distance <= 0.0 || self.is_at_node() { + return None; + } + + match self { + Position::Moving { + to, remaining_distance, .. + } => { + // If the remaining distance is less than or equal the distance, we'll reach the target + if *remaining_distance <= distance { + let overflow: Option = if *remaining_distance != distance { + Some(distance - *remaining_distance) + } else { + None + }; + *self = Position::Stopped { node: *to }; + + return overflow; + } + + *remaining_distance -= distance; + + None + } + _ => unreachable!(), + } + } + /// Returns `true` if the position is exactly at a node (not traveling). pub fn is_at_node(&self) -> bool { - self.edge_progress.is_none() + matches!(self, Position::Stopped { .. }) } /// Returns the `NodeId` of the current node (source of travel if moving). pub fn current_node(&self) -> NodeId { - self.node + match self { + Position::Stopped { node } => *node, + Position::Moving { from, .. } => *from, + } } /// Returns the `NodeId` of the destination node, if currently traveling. pub fn target_node(&self) -> Option { - self.edge_progress.as_ref().map(|ep| ep.target_node) + 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 { - self.edge_progress.is_some() + matches!(self, Position::Moving { .. }) } } -fn can_traverse(entity_type: EntityType, edge: Edge) -> bool { - let entity_flags = entity_type.traversal_flags(); - edge.traversal_flags.contains(entity_flags) -} +// pub fn movement_system( +// map: Res, +// delta_time: Res, +// mut entities: Query<(&mut Position, &mut Movable, &EntityType)>, +// mut errors: EventWriter, +// ) { +// for (mut position, mut movable, entity_type) in entities.iter_mut() { +// let distance = movable.speed * 60.0 * delta_time.0; -pub fn movement_system( - map: Res, - delta_time: Res, - mut entities: Query<(&mut MovementState, &mut Movable, &mut Position, &EntityType)>, - mut errors: EventWriter, -) { - for (mut movement_state, mut movable, mut position, entity_type) in entities.iter_mut() { - let distance = movable.speed * 60.0 * delta_time.0; +// match *position { +// Position::Stopped { .. } => { +// // Check if we have a requested direction to start moving +// if let Some(requested_direction) = movable.requested_direction { +// if let Some(edge) = map.graph.find_edge_in_direction(position.current_node(), requested_direction) { +// if can_traverse(*entity_type, edge) { +// // Start moving in the requested direction +// let progress = if edge.distance > 0.0 { +// distance / edge.distance +// } else { +// // Zero-distance edge (tunnels) - immediately teleport +// tracing::debug!( +// "Entity entering tunnel from node {} to node {}", +// position.current_node(), +// edge.target +// ); +// 1.0 +// }; - match *movement_state { - MovementState::Stopped => { - // Check if we have a requested direction to start moving - if let Some(requested_direction) = movable.requested_direction { - if let Some(edge) = map.graph.find_edge_in_direction(position.node, requested_direction) { - if can_traverse(*entity_type, edge) { - // Start moving in the requested direction - let progress = if edge.distance > 0.0 { - distance / edge.distance - } else { - // Zero-distance edge (tunnels) - immediately teleport - tracing::debug!("Entity entering tunnel from node {} to node {}", position.node, edge.target); - 1.0 - }; +// *position = Position::Moving { +// from: position.current_node(), +// to: edge.target, +// remaining_distance: progress, +// }; +// movable.current_direction = requested_direction; +// movable.requested_direction = None; +// } +// } else { +// errors.write( +// EntityError::InvalidMovement(format!( +// "No edge found in direction {:?} from node {}", +// requested_direction, +// position.current_node() +// )) +// .into(), +// ); +// } +// } +// } +// Position::Moving { +// from, +// to, +// remaining_distance, +// } => { +// // Continue moving or handle node transitions +// let current_node = *from; +// if let Some(edge) = map.graph.find_edge(current_node, *to) { +// // Extract target node before mutable operations +// let target_node = *to; - 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, - }; - } - } else { - errors.write( - EntityError::InvalidMovement(format!( - "No edge found in direction {:?} from node {}", - requested_direction, position.node - )) - .into(), - ); - } - } - } - MovementState::Moving { direction } => { - // Continue moving or handle node transitions - let current_node = position.node; - 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); - // 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 { +// *remaining_distance += distance / edge.distance; +// } else { +// // Zero-distance edge (tunnels) - immediately complete +// *remaining_distance = 1.0; +// } - 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; - } +// if *remaining_distance >= 1.0 { +// // Reached the target node +// let overflow = if edge.distance > 0.0 { +// (*remaining_distance - 1.0) * edge.distance +// } else { +// // Zero-distance edge - use remaining distance for overflow +// distance +// }; +// *position = Position::Stopped { node: target_node }; - if edge_progress.progress >= 1.0 { - // Reached the target node - let overflow = if edge.distance > 0.0 { - (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; - 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 +// }; - // 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 = Position::Moving { +// from: position.current_node(), +// to: next_edge.target, +// remaining_distance: next_progress, +// }; +// movable.current_direction = requested_direction; +// movable.requested_direction = None; +// continued_moving = true; +// } +// } +// } - 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 +// }; - // 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 = Position::Moving { +// from: position.current_node(), +// to: next_edge.target, +// remaining_distance: next_progress, +// }; +// // Keep current direction and movement state +// continued_moving = true; +// } +// } +// } - 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( - EntityError::InvalidMovement(format!( - "Inconsistent state: Moving on non-existent edge from {} to {}", - current_node, target_node - )) - .into(), - ); - *movement_state = MovementState::Stopped; - position.edge_progress = None; - } - } else { - // Movement state says moving but no edge progress - this shouldn't happen - errors.write(EntityError::InvalidMovement("Entity in Moving state but no edge progress".to_string()).into()); - *movement_state = MovementState::Stopped; - } - } - } - } -} +// // 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( +// EntityError::InvalidMovement(format!( +// "Inconsistent state: Moving on non-existent edge from {} to {}", +// current_node, target_node +// )) +// .into(), +// ); +// *movement_state = MovementState::Stopped; +// position.edge_progress = None; +// } +// } else { +// // Movement state says moving but no edge progress - this shouldn't happen +// errors.write(EntityError::InvalidMovement("Entity in Moving state but no edge progress".to_string()).into()); +// *movement_state = MovementState::Stopped; +// } +// } +// } +// } +// } diff --git a/src/systems/player.rs b/src/systems/player.rs new file mode 100644 index 0000000..2f6b758 --- /dev/null +++ b/src/systems/player.rs @@ -0,0 +1,143 @@ +use bevy_ecs::{ + event::{EventReader, EventWriter}, + prelude::ResMut, + query::With, + system::{Query, Res}, +}; + +use crate::{ + entity::graph::Edge, + error::GameError, + events::{GameCommand, GameEvent}, + map::builder::Map, + systems::{ + components::{AudioState, DeltaTime, EntityType, GlobalState, PlayerControlled}, + debug::DebugState, + movement::{BufferedDirection, Position, Velocity}, + }, +}; + +// Handles player input and control +pub fn player_control_system( + mut events: EventReader, + mut state: ResMut, + mut debug_state: ResMut, + mut audio_state: ResMut, + mut players: Query<(&mut BufferedDirection), With>, + mut errors: EventWriter, +) { + // Get the player's movable component (ensuring there is only one player) + let mut buffered_direction = match players.single_mut() { + Ok(buffered_direction) => buffered_direction, + Err(e) => { + errors.write(GameError::InvalidState(format!( + "No/multiple entities queried for player system: {}", + e + ))); + return; + } + }; + + // Handle events + for event in events.read() { + if let GameEvent::Command(command) = event { + match command { + GameCommand::MovePlayer(direction) => { + *buffered_direction = BufferedDirection::Some { + direction: *direction, + remaining_time: 0.25, + }; + } + GameCommand::Exit => { + state.exit = true; + } + GameCommand::ToggleDebug => { + *debug_state = debug_state.next(); + } + GameCommand::MuteAudio => { + audio_state.muted = !audio_state.muted; + tracing::info!("Audio {}", if audio_state.muted { "muted" } else { "unmuted" }); + } + _ => {} + } + } + } +} + +fn can_traverse(entity_type: EntityType, edge: Edge) -> bool { + let entity_flags = entity_type.traversal_flags(); + edge.traversal_flags.contains(entity_flags) +} + +pub fn player_movement_system( + map: Res, + delta_time: Res, + mut entities: Query<(&mut Position, &mut Velocity, &mut BufferedDirection), With>, + mut errors: EventWriter, +) { + for (mut position, mut velocity, mut buffered_direction) in entities.iter_mut() { + // Decrement the buffered direction remaining time + if let BufferedDirection::Some { + direction, + remaining_time, + } = *buffered_direction + { + if remaining_time <= 0.0 { + *buffered_direction = BufferedDirection::None; + } else { + *buffered_direction = BufferedDirection::Some { + direction, + remaining_time: remaining_time - delta_time.0, + }; + } + } + + let mut distance = velocity.speed * 60.0 * delta_time.0; + + loop { + match *position { + Position::Stopped { .. } => { + // If there is a buffered direction, travel it's edge first if available. + if let BufferedDirection::Some { direction, .. } = *buffered_direction { + // If there's no edge in that direction, ignore the buffered direction. + if let Some(edge) = map.graph.find_edge_in_direction(position.current_node(), direction) { + // If there is an edge in that direction (and it's traversable), start moving towards it and consume the buffered direction. + if can_traverse(EntityType::Player, edge) { + velocity.direction = edge.direction; + *position = Position::Moving { + from: position.current_node(), + to: edge.target, + remaining_distance: edge.distance, + }; + *buffered_direction = BufferedDirection::None; + } + } + } + + // If there is no buffered direction (or it's not yet valid), continue in the current direction. + if let Some(edge) = map.graph.find_edge_in_direction(position.current_node(), velocity.direction) { + if can_traverse(EntityType::Player, edge) { + velocity.direction = edge.direction; + *position = Position::Moving { + from: position.current_node(), + to: edge.target, + remaining_distance: edge.distance, + }; + } + } else { + // No edge in our current direction either, erase the buffered direction and stop. + *buffered_direction = BufferedDirection::None; + break; + } + } + Position::Moving { .. } => { + if let Some(overflow) = position.tick(distance) { + distance = overflow; + } else { + break; + } + } + } + } + } +} diff --git a/src/systems/profiling.rs b/src/systems/profiling.rs index 94227a4..ce5c188 100644 --- a/src/systems/profiling.rs +++ b/src/systems/profiling.rs @@ -20,7 +20,7 @@ const TIMING_WINDOW_SIZE: usize = 30; #[derive(EnumCount, IntoStaticStr, Debug, PartialEq, Eq, Hash, Copy, Clone)] pub enum SystemId { Input, - Player, + PlayerControls, Ghost, Movement, Audio, @@ -32,6 +32,7 @@ pub enum SystemId { Present, Collision, Item, + PlayerMovement, } impl Display for SystemId { diff --git a/src/systems/render.rs b/src/systems/render.rs index 50f5827..84b54c8 100644 --- a/src/systems/render.rs +++ b/src/systems/render.rs @@ -1,7 +1,7 @@ use crate::error::{GameError, TextureError}; use crate::map::builder::Map; use crate::systems::components::{DeltaTime, DirectionalAnimated, RenderDirty, Renderable}; -use crate::systems::movement::{Movable, MovementState, Position}; +use crate::systems::movement::{Position, Velocity}; use crate::texture::sprite::SpriteAtlas; use bevy_ecs::entity::Entity; use bevy_ecs::event::EventWriter; @@ -26,12 +26,12 @@ pub fn dirty_render_system( /// 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( dt: Res, - mut renderables: Query<(&MovementState, &Movable, &mut DirectionalAnimated, &mut Renderable)>, + mut renderables: Query<(&Position, &Velocity, &mut DirectionalAnimated, &mut Renderable)>, mut errors: EventWriter, ) { - for (movement_state, movable, mut texture, mut renderable) in renderables.iter_mut() { - let stopped = matches!(movement_state, MovementState::Stopped); - let current_direction = movable.current_direction; + for (position, velocity, mut texture, mut renderable) in renderables.iter_mut() { + let stopped = matches!(position, Position::Stopped { .. }); + let current_direction = velocity.direction; let texture = if stopped { texture.stopped_textures[current_direction.as_usize()].as_mut() @@ -96,7 +96,7 @@ pub fn render_system( continue; } - let pos = position.get_pixel_pos(&map.graph); + let pos = position.get_pixel_position(&map.graph); match pos { Ok(pos) => { let dest = crate::helpers::centered_with_size(