Files
Pac-Man/src/systems/player.rs

169 lines
7.1 KiB
Rust

use bevy_ecs::{
event::{EventReader, EventWriter},
query::{With, Without},
system::{Query, Res, ResMut},
};
use crate::{
error::GameError,
events::{GameCommand, GameEvent},
map::{builder::Map, graph::Edge},
systems::{
components::{DeltaTime, EntityType, Frozen, GlobalState, MovementModifiers, PlayerControlled},
debug::DebugState,
movement::{BufferedDirection, Position, Velocity},
AudioState,
},
};
pub fn can_traverse(entity_type: EntityType, edge: Edge) -> bool {
let entity_flags = entity_type.traversal_flags();
edge.traversal_flags.contains(entity_flags)
}
/// Processes player input commands and updates game state accordingly.
///
/// Handles keyboard-driven commands like movement direction changes, debug mode
/// toggling, audio muting, and game exit requests. Movement commands are buffered
/// to allow direction changes before reaching intersections, improving gameplay
/// responsiveness. Non-movement commands immediately modify global game state.
pub fn player_control_system(
mut events: EventReader<GameEvent>,
mut state: ResMut<GlobalState>,
mut debug_state: ResMut<DebugState>,
mut audio_state: ResMut<AudioState>,
mut players: Query<&mut BufferedDirection, (With<PlayerControlled>, Without<Frozen>)>,
mut errors: EventWriter<GameError>,
) {
// Handle events
for event in events.read() {
if let GameEvent::Command(command) = event {
match command {
GameCommand::MovePlayer(direction) => {
// Get the player's movable component (ensuring there is only one player)
let mut buffered_direction = match players.single_mut() {
Ok(tuple) => tuple,
Err(e) => {
errors.write(GameError::InvalidState(format!(
"No/multiple entities queried for player system: {}",
e
)));
return;
}
};
*buffered_direction = BufferedDirection::Some {
direction: *direction,
remaining_time: 0.25,
};
}
GameCommand::Exit => {
state.exit = true;
}
GameCommand::ToggleDebug => {
debug_state.enabled = !debug_state.enabled;
}
GameCommand::MuteAudio => {
audio_state.muted = !audio_state.muted;
tracing::info!("Audio {}", if audio_state.muted { "muted" } else { "unmuted" });
}
_ => {}
}
}
}
}
/// Executes frame-by-frame movement for Pac-Man.
///
/// Handles movement logic including buffered direction changes, edge traversal validation, and continuous movement between nodes.
/// When stopped, prioritizes buffered directions for responsive controls, falling back to current direction.
/// Supports movement chaining within a single frame when traveling at high speeds.
#[allow(clippy::type_complexity)]
pub fn player_movement_system(
map: Res<Map>,
delta_time: Res<DeltaTime>,
mut entities: Query<
(&MovementModifiers, &mut Position, &mut Velocity, &mut BufferedDirection),
(With<PlayerControlled>, Without<Frozen>),
>,
) {
for (modifiers, 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 * modifiers.speed_multiplier * 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;
}
}
}
}
}
}
/// Applies tunnel slowdown based on the current node tile
pub fn player_tunnel_slowdown_system(map: Res<Map>, mut q: Query<(&Position, &mut MovementModifiers), With<PlayerControlled>>) {
if let Ok((position, mut modifiers)) = q.single_mut() {
let node = position.current_node();
let in_tunnel = map
.tile_at_node(node)
.map(|t| t == crate::constants::MapTile::Tunnel)
.unwrap_or(false);
modifiers.tunnel_slowdown_active = in_tunnel;
modifiers.speed_multiplier = if in_tunnel { 0.6 } else { 1.0 };
}
}