mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-07 07:15:45 -06:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90adaf9e84 | |||
| 2140fbec1b | |||
| 78300bdf9c | |||
| 514a447162 | |||
| 3d0bc66e40 | |||
| e0a15c1ca8 |
165
src/game/mod.rs
165
src/game/mod.rs
@@ -8,20 +8,22 @@ use crate::error::{GameError, GameResult, TextureError};
|
|||||||
use crate::events::GameEvent;
|
use crate::events::GameEvent;
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::blinking::Blinking;
|
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::{
|
use crate::systems::{
|
||||||
audio::{audio_system, AudioEvent, AudioResource},
|
audio::{audio_system, AudioEvent, AudioResource},
|
||||||
blinking::blinking_system,
|
blinking::blinking_system,
|
||||||
collision::collision_system,
|
collision::collision_system,
|
||||||
components::{
|
components::{
|
||||||
AudioState, Collider, DeltaTime, DirectionalAnimated, EntityType, GlobalState, ItemBundle, ItemCollider, PacmanCollider,
|
AudioState, Collider, DeltaTime, DirectionalAnimated, EntityType, Ghost, GhostBundle, GhostCollider, GlobalState,
|
||||||
PlayerBundle, PlayerControlled, RenderDirty, Renderable, ScoreResource,
|
ItemBundle, ItemCollider, PacmanCollider, PlayerBundle, PlayerControlled, RenderDirty, Renderable, ScoreResource,
|
||||||
},
|
},
|
||||||
control::player_system,
|
|
||||||
debug::{debug_render_system, DebugState, DebugTextureResource},
|
debug::{debug_render_system, DebugState, DebugTextureResource},
|
||||||
|
ghost::ghost_movement_system,
|
||||||
input::input_system,
|
input::input_system,
|
||||||
item::item_system,
|
item::item_system,
|
||||||
movement::movement_system,
|
player::player_control_system,
|
||||||
profiling::{profile, SystemTimings},
|
profiling::{profile, SystemTimings},
|
||||||
render::{directional_render_system, dirty_render_system, render_system, BackbufferResource, MapTextureResource},
|
render::{directional_render_system, dirty_render_system, render_system, BackbufferResource, MapTextureResource},
|
||||||
};
|
};
|
||||||
@@ -45,7 +47,7 @@ use crate::{
|
|||||||
constants,
|
constants,
|
||||||
events::GameCommand,
|
events::GameCommand,
|
||||||
map::render::MapRenderer,
|
map::render::MapRenderer,
|
||||||
systems::input::Bindings,
|
systems::{debug::CursorPosition, input::Bindings},
|
||||||
texture::sprite::{AtlasMapper, SpriteAtlas},
|
texture::sprite::{AtlasMapper, SpriteAtlas},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -156,16 +158,12 @@ impl Game {
|
|||||||
|
|
||||||
let player = PlayerBundle {
|
let player = PlayerBundle {
|
||||||
player: PlayerControlled,
|
player: PlayerControlled,
|
||||||
position: Position {
|
position: Position::Stopped { node: pacman_start_node },
|
||||||
node: pacman_start_node,
|
velocity: Velocity {
|
||||||
edge_progress: None,
|
|
||||||
},
|
|
||||||
movement_state: MovementState::Stopped,
|
|
||||||
movable: Movable {
|
|
||||||
speed: 1.15,
|
speed: 1.15,
|
||||||
current_direction: Direction::Left,
|
direction: Direction::Left,
|
||||||
requested_direction: Some(Direction::Left), // Start moving left immediately
|
|
||||||
},
|
},
|
||||||
|
buffered_direction: BufferedDirection::None,
|
||||||
sprite: Renderable {
|
sprite: Renderable {
|
||||||
sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png")
|
sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png")
|
||||||
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?,
|
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("pacman/full.png".to_string())))?,
|
||||||
@@ -200,6 +198,7 @@ impl Game {
|
|||||||
world.insert_resource(RenderDirty::default());
|
world.insert_resource(RenderDirty::default());
|
||||||
world.insert_resource(DebugState::default());
|
world.insert_resource(DebugState::default());
|
||||||
world.insert_resource(AudioState::default());
|
world.insert_resource(AudioState::default());
|
||||||
|
world.insert_resource(CursorPosition::default());
|
||||||
|
|
||||||
world.add_observer(
|
world.add_observer(
|
||||||
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {
|
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {
|
||||||
@@ -210,19 +209,20 @@ impl Game {
|
|||||||
);
|
);
|
||||||
schedule.add_systems(
|
schedule.add_systems(
|
||||||
(
|
(
|
||||||
profile("input", input_system),
|
profile(SystemId::Input, input_system),
|
||||||
profile("player", player_system),
|
profile(SystemId::PlayerControls, player_control_system),
|
||||||
profile("movement", movement_system),
|
profile(SystemId::PlayerMovement, player_movement_system),
|
||||||
profile("collision", collision_system),
|
profile(SystemId::Ghost, ghost_movement_system),
|
||||||
profile("item", item_system),
|
profile(SystemId::Collision, collision_system),
|
||||||
profile("audio", audio_system),
|
profile(SystemId::Item, item_system),
|
||||||
profile("blinking", blinking_system),
|
profile(SystemId::Audio, audio_system),
|
||||||
profile("directional_render", directional_render_system),
|
profile(SystemId::Blinking, blinking_system),
|
||||||
profile("dirty_render", dirty_render_system),
|
profile(SystemId::DirectionalRender, directional_render_system),
|
||||||
profile("render", render_system),
|
profile(SystemId::DirtyRender, dirty_render_system),
|
||||||
profile("debug_render", debug_render_system),
|
profile(SystemId::Render, render_system),
|
||||||
|
profile(SystemId::DebugRender, debug_render_system),
|
||||||
profile(
|
profile(
|
||||||
"present",
|
SystemId::Present,
|
||||||
|mut canvas: NonSendMut<&mut Canvas<Window>>,
|
|mut canvas: NonSendMut<&mut Canvas<Window>>,
|
||||||
backbuffer: NonSendMut<BackbufferResource>,
|
backbuffer: NonSendMut<BackbufferResource>,
|
||||||
debug_state: Res<DebugState>,
|
debug_state: Res<DebugState>,
|
||||||
@@ -245,6 +245,9 @@ impl Game {
|
|||||||
// Spawn player
|
// Spawn player
|
||||||
world.spawn(player);
|
world.spawn(player);
|
||||||
|
|
||||||
|
// Spawn ghosts
|
||||||
|
Self::spawn_ghosts(&mut world)?;
|
||||||
|
|
||||||
// Spawn items
|
// Spawn items
|
||||||
let pellet_sprite = SpriteAtlas::get_tile(world.non_send_resource::<SpriteAtlas>(), "maze/pellet.png")
|
let pellet_sprite = SpriteAtlas::get_tile(world.non_send_resource::<SpriteAtlas>(), "maze/pellet.png")
|
||||||
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/pellet.png".to_string())))?;
|
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/pellet.png".to_string())))?;
|
||||||
@@ -263,10 +266,7 @@ impl Game {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut item = world.spawn(ItemBundle {
|
let mut item = world.spawn(ItemBundle {
|
||||||
position: Position {
|
position: Position::Stopped { node: node_id },
|
||||||
node: node_id,
|
|
||||||
edge_progress: None,
|
|
||||||
},
|
|
||||||
sprite: Renderable {
|
sprite: Renderable {
|
||||||
sprite,
|
sprite,
|
||||||
layer: 1,
|
layer: 1,
|
||||||
@@ -288,6 +288,111 @@ impl Game {
|
|||||||
Ok(Game { world, schedule })
|
Ok(Game { world, schedule })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawns all four ghosts at their starting positions with appropriate textures.
|
||||||
|
fn spawn_ghosts(world: &mut World) -> GameResult<()> {
|
||||||
|
// Extract the data we need first to avoid borrow conflicts
|
||||||
|
let ghost_start_positions = {
|
||||||
|
let map = world.resource::<Map>();
|
||||||
|
[
|
||||||
|
(Ghost::Blinky, map.start_positions.blinky),
|
||||||
|
(Ghost::Pinky, map.start_positions.pinky),
|
||||||
|
(Ghost::Inky, map.start_positions.inky),
|
||||||
|
(Ghost::Clyde, map.start_positions.clyde),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
for (ghost_type, start_node) in ghost_start_positions {
|
||||||
|
// Create the ghost bundle in a separate scope to manage borrows
|
||||||
|
let ghost = {
|
||||||
|
let atlas = world.non_send_resource::<SpriteAtlas>();
|
||||||
|
|
||||||
|
// Create directional animated textures for the ghost
|
||||||
|
let mut textures = [None, None, None, None];
|
||||||
|
let mut stopped_textures = [None, None, None, None];
|
||||||
|
|
||||||
|
for direction in Direction::DIRECTIONS {
|
||||||
|
let moving_prefix = match direction {
|
||||||
|
Direction::Up => "up",
|
||||||
|
Direction::Down => "down",
|
||||||
|
Direction::Left => "left",
|
||||||
|
Direction::Right => "right",
|
||||||
|
};
|
||||||
|
|
||||||
|
let moving_tiles = vec![
|
||||||
|
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
GameError::Texture(TextureError::AtlasTileNotFound(format!(
|
||||||
|
"ghost/{}/{}_{}.png",
|
||||||
|
ghost_type.as_str(),
|
||||||
|
moving_prefix,
|
||||||
|
"a"
|
||||||
|
)))
|
||||||
|
})?,
|
||||||
|
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "b"))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
GameError::Texture(TextureError::AtlasTileNotFound(format!(
|
||||||
|
"ghost/{}/{}_{}.png",
|
||||||
|
ghost_type.as_str(),
|
||||||
|
moving_prefix,
|
||||||
|
"b"
|
||||||
|
)))
|
||||||
|
})?,
|
||||||
|
];
|
||||||
|
|
||||||
|
let stopped_tiles = vec![SpriteAtlas::get_tile(
|
||||||
|
atlas,
|
||||||
|
&format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"),
|
||||||
|
)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
GameError::Texture(TextureError::AtlasTileNotFound(format!(
|
||||||
|
"ghost/{}/{}_{}.png",
|
||||||
|
ghost_type.as_str(),
|
||||||
|
moving_prefix,
|
||||||
|
"a"
|
||||||
|
)))
|
||||||
|
})?];
|
||||||
|
|
||||||
|
textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2)?);
|
||||||
|
stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
GhostBundle {
|
||||||
|
ghost: ghost_type,
|
||||||
|
position: Position::Stopped { node: start_node },
|
||||||
|
velocity: Velocity {
|
||||||
|
speed: ghost_type.base_speed(),
|
||||||
|
direction: Direction::Left,
|
||||||
|
},
|
||||||
|
sprite: Renderable {
|
||||||
|
sprite: SpriteAtlas::get_tile(atlas, &format!("ghost/{}/left_a.png", ghost_type.as_str())).ok_or_else(
|
||||||
|
|| {
|
||||||
|
GameError::Texture(TextureError::AtlasTileNotFound(format!(
|
||||||
|
"ghost/{}/left_a.png",
|
||||||
|
ghost_type.as_str()
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
)?,
|
||||||
|
layer: 0,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
directional_animated: DirectionalAnimated {
|
||||||
|
textures,
|
||||||
|
stopped_textures,
|
||||||
|
},
|
||||||
|
entity_type: EntityType::Ghost,
|
||||||
|
collider: Collider {
|
||||||
|
size: crate::constants::CELL_SIZE as f32 * 1.375,
|
||||||
|
},
|
||||||
|
ghost_collider: GhostCollider,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
world.spawn(ghost);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Ticks the game state.
|
/// Ticks the game state.
|
||||||
///
|
///
|
||||||
/// Returns true if the game should exit.
|
/// Returns true if the game should exit.
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ pub fn collision_system(
|
|||||||
// Check PACMAN × ITEM collisions
|
// Check PACMAN × ITEM collisions
|
||||||
for (pacman_entity, pacman_pos, pacman_collider) in pacman_query.iter() {
|
for (pacman_entity, pacman_pos, pacman_collider) in pacman_query.iter() {
|
||||||
for (item_entity, item_pos, item_collider) in item_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)) => {
|
(Ok(pacman_pixel), Ok(item_pixel)) => {
|
||||||
// Calculate the distance between the two entities's precise pixel positions
|
// Calculate the distance between the two entities's precise pixel positions
|
||||||
let distance = pacman_pixel.distance(item_pixel);
|
let distance = pacman_pixel.distance(item_pixel);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use bitflags::bitflags;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
entity::graph::TraversalFlags,
|
entity::graph::TraversalFlags,
|
||||||
systems::movement::{Movable, MovementState, Position},
|
systems::movement::{BufferedDirection, Position, Velocity},
|
||||||
texture::{animated::AnimatedTexture, sprite::AtlasTile},
|
texture::{animated::AnimatedTexture, sprite::AtlasTile},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -11,6 +11,46 @@ use crate::{
|
|||||||
#[derive(Default, Component)]
|
#[derive(Default, Component)]
|
||||||
pub struct PlayerControlled;
|
pub struct PlayerControlled;
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Ghost {
|
||||||
|
Blinky,
|
||||||
|
Pinky,
|
||||||
|
Inky,
|
||||||
|
Clyde,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ghost {
|
||||||
|
/// Returns the ghost type name for atlas lookups.
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A tag component denoting the type of entity.
|
/// A tag component denoting the type of entity.
|
||||||
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum EntityType {
|
pub enum EntityType {
|
||||||
@@ -76,8 +116,8 @@ pub struct ItemCollider;
|
|||||||
pub struct PlayerBundle {
|
pub struct PlayerBundle {
|
||||||
pub player: PlayerControlled,
|
pub player: PlayerControlled,
|
||||||
pub position: Position,
|
pub position: Position,
|
||||||
pub movement_state: MovementState,
|
pub velocity: Velocity,
|
||||||
pub movable: Movable,
|
pub buffered_direction: BufferedDirection,
|
||||||
pub sprite: Renderable,
|
pub sprite: Renderable,
|
||||||
pub directional_animated: DirectionalAnimated,
|
pub directional_animated: DirectionalAnimated,
|
||||||
pub entity_type: EntityType,
|
pub entity_type: EntityType,
|
||||||
@@ -94,6 +134,18 @@ pub struct ItemBundle {
|
|||||||
pub item_collider: ItemCollider,
|
pub item_collider: ItemCollider,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Bundle)]
|
||||||
|
pub struct GhostBundle {
|
||||||
|
pub ghost: Ghost,
|
||||||
|
pub position: Position,
|
||||||
|
pub velocity: Velocity,
|
||||||
|
pub sprite: Renderable,
|
||||||
|
pub directional_animated: DirectionalAnimated,
|
||||||
|
pub entity_type: EntityType,
|
||||||
|
pub collider: Collider,
|
||||||
|
pub ghost_collider: GhostCollider,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct GlobalState {
|
pub struct GlobalState {
|
||||||
pub exit: bool,
|
pub exit: bool,
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
use bevy_ecs::{
|
|
||||||
event::{EventReader, EventWriter},
|
|
||||||
prelude::ResMut,
|
|
||||||
query::With,
|
|
||||||
system::Query,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
error::GameError,
|
|
||||||
events::{GameCommand, GameEvent},
|
|
||||||
systems::components::{GlobalState, PlayerControlled},
|
|
||||||
systems::debug::DebugState,
|
|
||||||
systems::movement::Movable,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handles player input and control
|
|
||||||
pub fn player_system(
|
|
||||||
mut events: EventReader<GameEvent>,
|
|
||||||
mut state: ResMut<GlobalState>,
|
|
||||||
mut debug_state: ResMut<DebugState>,
|
|
||||||
mut players: Query<&mut Movable, With<PlayerControlled>>,
|
|
||||||
mut errors: EventWriter<GameError>,
|
|
||||||
) {
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
//! Debug rendering system
|
//! Debug rendering system
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
use crate::constants::BOARD_PIXEL_OFFSET;
|
use crate::constants::BOARD_PIXEL_OFFSET;
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
use crate::systems::components::Collider;
|
use crate::systems::components::Collider;
|
||||||
@@ -6,11 +8,15 @@ use crate::systems::movement::Position;
|
|||||||
use crate::systems::profiling::SystemTimings;
|
use crate::systems::profiling::SystemTimings;
|
||||||
use crate::systems::render::BackbufferResource;
|
use crate::systems::render::BackbufferResource;
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
|
use glam::Vec2;
|
||||||
use sdl2::pixels::Color;
|
use sdl2::pixels::Color;
|
||||||
use sdl2::rect::Rect;
|
use sdl2::rect::Rect;
|
||||||
use sdl2::render::{Canvas, Texture, TextureCreator};
|
use sdl2::render::{Canvas, Texture, TextureCreator};
|
||||||
use sdl2::video::{Window, WindowContext};
|
use sdl2::video::{Window, WindowContext};
|
||||||
|
|
||||||
|
#[derive(Resource, Default, Debug, Copy, Clone)]
|
||||||
|
pub struct CursorPosition(pub Vec2);
|
||||||
|
|
||||||
#[derive(Resource, Default, Debug, Copy, Clone, PartialEq)]
|
#[derive(Resource, Default, Debug, Copy, Clone, PartialEq)]
|
||||||
pub enum DebugState {
|
pub enum DebugState {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -36,10 +42,13 @@ pub struct DebugTextureResource(pub Texture<'static>);
|
|||||||
fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
|
fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
|
||||||
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
||||||
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
||||||
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
|
let scale = scale_x.min(scale_y);
|
||||||
|
|
||||||
let x = (pos.0 * scale) as i32;
|
let offset_x = (output_size.0 as f32 - logical_size.0 as f32 * scale) / 2.0;
|
||||||
let y = (pos.1 * scale) as i32;
|
let offset_y = (output_size.1 as f32 - logical_size.1 as f32 * scale) / 2.0;
|
||||||
|
|
||||||
|
let x = (pos.0 * scale + offset_x) as i32;
|
||||||
|
let y = (pos.1 * scale + offset_y) as i32;
|
||||||
(x, y)
|
(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +56,13 @@ fn transform_position(pos: (f32, f32), output_size: (u32, u32), logical_size: (u
|
|||||||
fn transform_position_with_offset(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
|
fn transform_position_with_offset(pos: (f32, f32), output_size: (u32, u32), logical_size: (u32, u32)) -> (i32, i32) {
|
||||||
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
let scale_x = output_size.0 as f32 / logical_size.0 as f32;
|
||||||
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
let scale_y = output_size.1 as f32 / logical_size.1 as f32;
|
||||||
let scale = scale_x.min(scale_y); // Use the smaller scale to maintain aspect ratio
|
let scale = scale_x.min(scale_y);
|
||||||
|
|
||||||
let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale) as i32;
|
let offset_x = (output_size.0 as f32 - logical_size.0 as f32 * scale) / 2.0;
|
||||||
let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale) as i32;
|
let offset_y = (output_size.1 as f32 - logical_size.1 as f32 * scale) / 2.0;
|
||||||
|
|
||||||
|
let x = ((pos.0 + BOARD_PIXEL_OFFSET.x as f32) * scale + offset_x) as i32;
|
||||||
|
let y = ((pos.1 + BOARD_PIXEL_OFFSET.y as f32) * scale + offset_y) as i32;
|
||||||
(x, y)
|
(x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,20 +88,14 @@ fn render_timing_display(
|
|||||||
let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap();
|
let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap();
|
||||||
|
|
||||||
// Format timing information using the formatting module
|
// Format timing information using the formatting module
|
||||||
let timing_text = timings.format_timing_display();
|
let lines = timings.format_timing_display();
|
||||||
|
|
||||||
// Split text by newlines and render each line separately
|
|
||||||
let lines: Vec<&str> = timing_text.lines().collect();
|
|
||||||
if lines.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let line_height = 14; // Approximate line height for 12pt font
|
let line_height = 14; // Approximate line height for 12pt font
|
||||||
let padding = 10;
|
let padding = 10;
|
||||||
|
|
||||||
// Calculate background dimensions
|
// Calculate background dimensions
|
||||||
let max_width = lines
|
let max_width = lines
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|&&l| !l.is_empty()) // Don't consider empty lines for width
|
.filter(|l| !l.is_empty()) // Don't consider empty lines for width
|
||||||
.map(|line| font.size_of(line).unwrap().0)
|
.map(|line| font.size_of(line).unwrap().0)
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
@@ -135,6 +141,7 @@ pub fn debug_render_system(
|
|||||||
timings: Res<SystemTimings>,
|
timings: Res<SystemTimings>,
|
||||||
map: Res<Map>,
|
map: Res<Map>,
|
||||||
colliders: Query<(&Collider, &Position)>,
|
colliders: Query<(&Collider, &Position)>,
|
||||||
|
cursor: Res<CursorPosition>,
|
||||||
) {
|
) {
|
||||||
if *debug_state == DebugState::Off {
|
if *debug_state == DebugState::Off {
|
||||||
return;
|
return;
|
||||||
@@ -159,28 +166,49 @@ pub fn debug_render_system(
|
|||||||
// Get texture creator before entering the closure to avoid borrowing conflicts
|
// Get texture creator before entering the closure to avoid borrowing conflicts
|
||||||
let mut texture_creator = canvas.texture_creator();
|
let mut texture_creator = canvas.texture_creator();
|
||||||
|
|
||||||
|
let cursor_world_pos = cursor.0 - BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
|
||||||
// Draw debug info on the high-resolution debug texture
|
// Draw debug info on the high-resolution debug texture
|
||||||
canvas
|
canvas
|
||||||
.with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
|
.with_texture_canvas(&mut debug_texture.0, |debug_canvas| {
|
||||||
match *debug_state {
|
match *debug_state {
|
||||||
DebugState::Graph => {
|
DebugState::Graph => {
|
||||||
|
// Find the closest node to the cursor
|
||||||
|
|
||||||
|
let closest_node = map
|
||||||
|
.graph
|
||||||
|
.nodes()
|
||||||
|
.map(|node| node.position.distance(cursor_world_pos))
|
||||||
|
.enumerate()
|
||||||
|
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less))
|
||||||
|
.map(|(id, _)| id);
|
||||||
|
|
||||||
debug_canvas.set_draw_color(Color::RED);
|
debug_canvas.set_draw_color(Color::RED);
|
||||||
for (start_node, end_node) in map.graph.edges() {
|
for (start_node, end_node) in map.graph.edges() {
|
||||||
let start_node = map.graph.get_node(start_node).unwrap().position;
|
let start_node_model = map.graph.get_node(start_node).unwrap();
|
||||||
let end_node = map.graph.get_node(end_node.target).unwrap().position;
|
let end_node = map.graph.get_node(end_node.target).unwrap().position;
|
||||||
|
|
||||||
// Transform positions using common method
|
// Transform positions using common method
|
||||||
let (start_x, start_y) =
|
let (start_x, start_y) = transform_position_with_offset(
|
||||||
transform_position_with_offset((start_node.x, start_node.y), output_size, logical_size);
|
(start_node_model.position.x, start_node_model.position.y),
|
||||||
|
output_size,
|
||||||
|
logical_size,
|
||||||
|
);
|
||||||
let (end_x, end_y) = transform_position_with_offset((end_node.x, end_node.y), output_size, logical_size);
|
let (end_x, end_y) = transform_position_with_offset((end_node.x, end_node.y), output_size, logical_size);
|
||||||
|
|
||||||
debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap();
|
debug_canvas.draw_line((start_x, start_y), (end_x, end_y)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
debug_canvas.set_draw_color(Color::BLUE);
|
for (id, node) in map.graph.nodes().enumerate() {
|
||||||
for node in map.graph.nodes() {
|
|
||||||
let pos = node.position;
|
let pos = node.position;
|
||||||
|
|
||||||
|
// Set color based on whether the node is the closest to the cursor
|
||||||
|
if Some(id) == closest_node {
|
||||||
|
debug_canvas.set_draw_color(Color::YELLOW);
|
||||||
|
} else {
|
||||||
|
debug_canvas.set_draw_color(Color::BLUE);
|
||||||
|
}
|
||||||
|
|
||||||
// Transform position using common method
|
// Transform position using common method
|
||||||
let (x, y) = transform_position_with_offset((pos.x, pos.y), output_size, logical_size);
|
let (x, y) = transform_position_with_offset((pos.x, pos.y), output_size, logical_size);
|
||||||
let size = transform_size(4.0, output_size, logical_size);
|
let size = transform_size(4.0, output_size, logical_size);
|
||||||
@@ -193,7 +221,7 @@ pub fn debug_render_system(
|
|||||||
DebugState::Collision => {
|
DebugState::Collision => {
|
||||||
debug_canvas.set_draw_color(Color::GREEN);
|
debug_canvas.set_draw_color(Color::GREEN);
|
||||||
for (collider, position) in colliders.iter() {
|
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
|
// Transform position and size using common methods
|
||||||
let (x, y) = transform_position((pos.x, pos.y), output_size, logical_size);
|
let (x, y) = transform_position((pos.x, pos.y), output_size, logical_size);
|
||||||
@@ -207,6 +235,28 @@ pub fn debug_render_system(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render node ID if a node is highlighted
|
||||||
|
if let DebugState::Graph = *debug_state {
|
||||||
|
if let Some(closest_node_id) = map
|
||||||
|
.graph
|
||||||
|
.nodes()
|
||||||
|
.map(|node| node.position.distance(cursor_world_pos))
|
||||||
|
.enumerate()
|
||||||
|
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Less))
|
||||||
|
.map(|(id, _)| id)
|
||||||
|
{
|
||||||
|
let node = map.graph.get_node(closest_node_id).unwrap();
|
||||||
|
let (x, y) = transform_position_with_offset((node.position.x, node.position.y), output_size, logical_size);
|
||||||
|
|
||||||
|
let ttf_context = sdl2::ttf::init().unwrap();
|
||||||
|
let font = ttf_context.load_font("assets/site/TerminalVector.ttf", 12).unwrap();
|
||||||
|
let surface = font.render(&closest_node_id.to_string()).blended(Color::WHITE).unwrap();
|
||||||
|
let texture = texture_creator.create_texture_from_surface(&surface).unwrap();
|
||||||
|
let dest = Rect::new(x + 10, y - 5, texture.query().width, texture.query().height);
|
||||||
|
debug_canvas.copy(&texture, None, dest).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render timing information in the top-left corner
|
// Render timing information in the top-left corner
|
||||||
render_timing_display(debug_canvas, &mut texture_creator, &timings);
|
render_timing_display(debug_canvas, &mut texture_creator, &timings);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
use num_width::NumberWidth;
|
use num_width::NumberWidth;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use strum::EnumCount;
|
||||||
|
|
||||||
|
use crate::systems::profiling::SystemId;
|
||||||
|
|
||||||
// Helper to split a duration into a integer, decimal, and unit
|
// Helper to split a duration into a integer, decimal, and unit
|
||||||
fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
|
fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
|
||||||
@@ -34,7 +37,9 @@ fn get_value(duration: &Duration) -> (u64, u32, &'static str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Formats timing data into a vector of strings with proper alignment
|
/// Formats timing data into a vector of strings with proper alignment
|
||||||
pub fn format_timing_display(timing_data: impl IntoIterator<Item = (String, Duration, Duration)>) -> SmallVec<[String; 12]> {
|
pub fn format_timing_display(
|
||||||
|
timing_data: impl IntoIterator<Item = (String, Duration, Duration)>,
|
||||||
|
) -> SmallVec<[String; SystemId::COUNT]> {
|
||||||
let mut iter = timing_data.into_iter().peekable();
|
let mut iter = timing_data.into_iter().peekable();
|
||||||
if iter.peek().is_none() {
|
if iter.peek().is_none() {
|
||||||
return SmallVec::new();
|
return SmallVec::new();
|
||||||
@@ -98,5 +103,5 @@ pub fn format_timing_display(timing_data: impl IntoIterator<Item = (String, Dura
|
|||||||
max_std_int_width = max_std_int_width,
|
max_std_int_width = max_std_int_width,
|
||||||
max_std_decimal_width = max_std_decimal_width
|
max_std_decimal_width = max_std_decimal_width
|
||||||
)
|
)
|
||||||
}).collect::<SmallVec<[String; 12]>>()
|
}).collect::<SmallVec<[String; SystemId::COUNT]>>()
|
||||||
}
|
}
|
||||||
|
|||||||
69
src/systems/ghost.rs
Normal file
69
src/systems/ghost.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
use bevy_ecs::system::{Query, Res};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
entity::{direction::Direction, graph::Edge},
|
||||||
|
map::builder::Map,
|
||||||
|
systems::{
|
||||||
|
components::{DeltaTime, Ghost},
|
||||||
|
movement::{Position, Velocity},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Ghost AI system that handles randomized movement decisions.
|
||||||
|
///
|
||||||
|
/// This system runs on all ghosts and makes periodic decisions about
|
||||||
|
/// which direction to move in when they reach intersections.
|
||||||
|
pub fn ghost_movement_system(
|
||||||
|
map: Res<Map>,
|
||||||
|
delta_time: Res<DeltaTime>,
|
||||||
|
mut ghosts: Query<(&Ghost, &mut Velocity, &mut Position)>,
|
||||||
|
) {
|
||||||
|
for (_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();
|
||||||
|
|
||||||
|
let mut non_opposite_options: SmallVec<[Edge; 3]> = SmallVec::new();
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
&& edge.direction != opposite
|
||||||
|
{
|
||||||
|
non_opposite_options.push(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use bevy_ecs::{event::EventWriter, prelude::Res, resource::Resource, system::NonSendMut};
|
use bevy_ecs::{
|
||||||
|
event::EventWriter,
|
||||||
|
resource::Resource,
|
||||||
|
system::{NonSendMut, ResMut},
|
||||||
|
};
|
||||||
|
use glam::Vec2;
|
||||||
use sdl2::{event::Event, keyboard::Keycode, EventPump};
|
use sdl2::{event::Event, keyboard::Keycode, EventPump};
|
||||||
|
|
||||||
|
use crate::systems::debug::CursorPosition;
|
||||||
use crate::{
|
use crate::{
|
||||||
entity::direction::Direction,
|
entity::direction::Direction,
|
||||||
events::{GameCommand, GameEvent},
|
events::{GameCommand, GameEvent},
|
||||||
@@ -11,6 +17,8 @@ use crate::{
|
|||||||
#[derive(Debug, Clone, Resource)]
|
#[derive(Debug, Clone, Resource)]
|
||||||
pub struct Bindings {
|
pub struct Bindings {
|
||||||
key_bindings: HashMap<Keycode, GameCommand>,
|
key_bindings: HashMap<Keycode, GameCommand>,
|
||||||
|
movement_keys: HashSet<Keycode>,
|
||||||
|
last_movement_key: Option<Keycode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Bindings {
|
impl Default for Bindings {
|
||||||
@@ -35,23 +43,78 @@ impl Default for Bindings {
|
|||||||
key_bindings.insert(Keycode::Escape, GameCommand::Exit);
|
key_bindings.insert(Keycode::Escape, GameCommand::Exit);
|
||||||
key_bindings.insert(Keycode::Q, GameCommand::Exit);
|
key_bindings.insert(Keycode::Q, GameCommand::Exit);
|
||||||
|
|
||||||
Self { key_bindings }
|
let movement_keys = HashSet::from([
|
||||||
|
Keycode::W,
|
||||||
|
Keycode::A,
|
||||||
|
Keycode::S,
|
||||||
|
Keycode::D,
|
||||||
|
Keycode::Up,
|
||||||
|
Keycode::Down,
|
||||||
|
Keycode::Left,
|
||||||
|
Keycode::Right,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
key_bindings,
|
||||||
|
movement_keys,
|
||||||
|
last_movement_key: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn input_system(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>, mut pump: NonSendMut<&'static mut EventPump>) {
|
pub fn input_system(
|
||||||
|
mut bindings: ResMut<Bindings>,
|
||||||
|
mut writer: EventWriter<GameEvent>,
|
||||||
|
mut pump: NonSendMut<&'static mut EventPump>,
|
||||||
|
mut cursor: ResMut<CursorPosition>,
|
||||||
|
) {
|
||||||
|
let mut movement_key_pressed = false;
|
||||||
|
|
||||||
for event in pump.poll_iter() {
|
for event in pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
Event::Quit { .. } => {
|
Event::Quit { .. } => {
|
||||||
writer.write(GameEvent::Command(GameCommand::Exit));
|
writer.write(GameEvent::Command(GameCommand::Exit));
|
||||||
}
|
}
|
||||||
Event::KeyDown { keycode: Some(key), .. } => {
|
Event::MouseMotion { x, y, .. } => {
|
||||||
|
cursor.0 = Vec2::new(x as f32, y as f32);
|
||||||
|
}
|
||||||
|
Event::KeyUp {
|
||||||
|
repeat: false,
|
||||||
|
keycode: Some(key),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// If the last movement key was released, then forget it.
|
||||||
|
if let Some(last_movement_key) = bindings.last_movement_key {
|
||||||
|
if last_movement_key == key {
|
||||||
|
bindings.last_movement_key = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(key),
|
||||||
|
repeat: false,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
let command = bindings.key_bindings.get(&key).copied();
|
let command = bindings.key_bindings.get(&key).copied();
|
||||||
if let Some(command) = command {
|
if let Some(command) = command {
|
||||||
writer.write(GameEvent::Command(command));
|
writer.write(GameEvent::Command(command));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if bindings.movement_keys.contains(&key) {
|
||||||
|
movement_key_pressed = true;
|
||||||
|
bindings.last_movement_key = Some(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(last_movement_key) = bindings.last_movement_key {
|
||||||
|
if !movement_key_pressed {
|
||||||
|
let command = bindings.key_bindings.get(&last_movement_key).copied();
|
||||||
|
if let Some(command) = command {
|
||||||
|
writer.write(GameEvent::Command(command));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ pub mod audio;
|
|||||||
pub mod blinking;
|
pub mod blinking;
|
||||||
pub mod collision;
|
pub mod collision;
|
||||||
pub mod components;
|
pub mod components;
|
||||||
pub mod control;
|
|
||||||
pub mod debug;
|
pub mod debug;
|
||||||
pub mod formatting;
|
pub mod formatting;
|
||||||
|
pub mod ghost;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
|
pub mod player;
|
||||||
pub mod profiling;
|
pub mod profiling;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
|||||||
@@ -1,49 +1,40 @@
|
|||||||
|
use crate::entity::direction::Direction;
|
||||||
use crate::entity::graph::Graph;
|
use crate::entity::graph::Graph;
|
||||||
use crate::entity::{direction::Direction, graph::Edge};
|
use crate::error::{EntityError, GameResult};
|
||||||
use crate::error::{EntityError, GameError, GameResult};
|
|
||||||
use crate::map::builder::Map;
|
|
||||||
use crate::systems::components::{DeltaTime, EntityType};
|
|
||||||
use bevy_ecs::component::Component;
|
use bevy_ecs::component::Component;
|
||||||
use bevy_ecs::event::EventWriter;
|
|
||||||
use bevy_ecs::system::{Query, Res};
|
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
|
||||||
/// 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;
|
||||||
|
|
||||||
/// Progress along an edge between two nodes.
|
/// A component that represents the speed and cardinal direction of an entity.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
/// Speed is static, only applied when the entity has an edge to traverse.
|
||||||
pub struct EdgeProgress {
|
/// Direction is dynamic, but is controlled externally.
|
||||||
pub target_node: NodeId,
|
#[derive(Component, Debug, Copy, Clone, PartialEq)]
|
||||||
/// Progress from 0.0 (at source node) to 1.0 (at target node)
|
pub struct Velocity {
|
||||||
pub progress: f32,
|
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.
|
/// Pure spatial position component - works for both static and dynamic entities.
|
||||||
#[derive(Component, Debug, Copy, Clone, PartialEq, Default)]
|
#[derive(Component, Debug, Copy, Clone, PartialEq)]
|
||||||
pub struct Position {
|
pub enum Position {
|
||||||
/// The current/primary node this entity is at or traveling from
|
Stopped {
|
||||||
pub node: NodeId,
|
node: NodeId,
|
||||||
/// If Some, entity is traveling between nodes. If None, entity is stationary at node.
|
},
|
||||||
pub edge_progress: Option<EdgeProgress>,
|
Moving {
|
||||||
}
|
from: NodeId,
|
||||||
|
to: NodeId,
|
||||||
/// Explicit movement state - only for entities that can move.
|
remaining_distance: f32,
|
||||||
#[derive(Component, Debug, Clone, Copy, PartialEq, Default)]
|
|
||||||
pub enum MovementState {
|
|
||||||
#[default]
|
|
||||||
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 {
|
||||||
@@ -55,26 +46,32 @@ impl Position {
|
|||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// 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_position(&self, graph: &Graph) -> GameResult<Vec2> {
|
||||||
let pos = match &self.edge_progress {
|
let pos = match &self {
|
||||||
None => {
|
Position::Stopped { node } => {
|
||||||
// Entity is stationary at a 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
|
node.position
|
||||||
}
|
}
|
||||||
Some(edge_progress) => {
|
Position::Moving {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
remaining_distance,
|
||||||
|
} => {
|
||||||
// Entity is traveling between nodes
|
// Entity is traveling between nodes
|
||||||
let from_node = graph.get_node(self.node).ok_or(EntityError::NodeNotFound(self.node))?;
|
let from_node = graph.get_node(*from).ok_or(EntityError::NodeNotFound(*from))?;
|
||||||
let to_node = graph
|
let to_node = graph.get_node(*to).ok_or(EntityError::NodeNotFound(*to))?;
|
||||||
.get_node(edge_progress.target_node)
|
let edge = graph
|
||||||
.ok_or(EntityError::NodeNotFound(edge_progress.target_node))?;
|
.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
|
// 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
|
to_node.position
|
||||||
} else {
|
} else {
|
||||||
// Interpolate position based on progress
|
// 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 +81,205 @@ impl Position {
|
|||||||
pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32,
|
pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
/// Moves the position by a given distance towards it's current target node.
|
||||||
impl Position {
|
///
|
||||||
|
/// Returns the overflow distance, if any.
|
||||||
|
pub fn tick(&mut self, distance: f32) -> Option<f32> {
|
||||||
|
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<f32> = 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).
|
/// 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 {
|
||||||
self.edge_progress.is_none()
|
matches!(self, Position::Stopped { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `NodeId` of the current node (source of travel if moving).
|
/// Returns the `NodeId` of the current node (source of travel if moving).
|
||||||
pub fn current_node(&self) -> NodeId {
|
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<NodeId> {
|
|
||||||
self.edge_progress.as_ref().map(|ep| ep.target_node)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true` if the entity is traveling between nodes.
|
|
||||||
pub fn is_moving(&self) -> bool {
|
|
||||||
self.edge_progress.is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Map>,
|
|
||||||
delta_time: Res<DeltaTime>,
|
|
||||||
mut entities: Query<(&mut MovementState, &mut Movable, &mut Position, &EntityType)>,
|
|
||||||
mut errors: EventWriter<GameError>,
|
|
||||||
) {
|
|
||||||
for (mut movement_state, mut movable, mut position, entity_type) in entities.iter_mut() {
|
|
||||||
let distance = movable.speed * 60.0 * delta_time.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.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);
|
|
||||||
|
|
||||||
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 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;
|
|
||||||
|
|
||||||
// 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(
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pub fn movement_system(
|
||||||
|
// map: Res<Map>,
|
||||||
|
// delta_time: Res<DeltaTime>,
|
||||||
|
// mut entities: Query<(&mut Position, &mut Movable, &EntityType)>,
|
||||||
|
// mut errors: EventWriter<GameError>,
|
||||||
|
// ) {
|
||||||
|
// for (mut position, mut movable, 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
|
||||||
|
// };
|
||||||
|
|
||||||
|
// *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;
|
||||||
|
|
||||||
|
// // 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 *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 };
|
||||||
|
|
||||||
|
// 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 = 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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
143
src/systems/player.rs
Normal file
143
src/systems/player.rs
Normal file
@@ -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<GameEvent>,
|
||||||
|
mut state: ResMut<GlobalState>,
|
||||||
|
mut debug_state: ResMut<DebugState>,
|
||||||
|
mut audio_state: ResMut<AudioState>,
|
||||||
|
mut players: Query<&mut BufferedDirection, With<PlayerControlled>>,
|
||||||
|
mut errors: EventWriter<GameError>,
|
||||||
|
) {
|
||||||
|
// 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<Map>,
|
||||||
|
delta_time: Res<DeltaTime>,
|
||||||
|
mut entities: Query<(&mut Position, &mut Velocity, &mut BufferedDirection), With<PlayerControlled>>,
|
||||||
|
// mut errors: EventWriter<GameError>,
|
||||||
|
) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,44 @@ use bevy_ecs::system::{IntoSystem, System};
|
|||||||
use circular_buffer::CircularBuffer;
|
use circular_buffer::CircularBuffer;
|
||||||
use micromap::Map;
|
use micromap::Map;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::fmt::Display;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use strum::EnumCount;
|
||||||
|
use strum_macros::{EnumCount, IntoStaticStr};
|
||||||
use thousands::Separable;
|
use thousands::Separable;
|
||||||
|
|
||||||
|
use crate::systems::formatting;
|
||||||
|
|
||||||
/// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic.
|
/// The maximum number of systems that can be profiled. Must not be exceeded, or it will panic.
|
||||||
const MAX_SYSTEMS: usize = 12;
|
const MAX_SYSTEMS: usize = SystemId::COUNT;
|
||||||
/// The number of durations to keep in the circular buffer.
|
/// The number of durations to keep in the circular buffer.
|
||||||
const TIMING_WINDOW_SIZE: usize = 30;
|
const TIMING_WINDOW_SIZE: usize = 30;
|
||||||
|
|
||||||
|
#[derive(EnumCount, IntoStaticStr, Debug, PartialEq, Eq, Hash, Copy, Clone)]
|
||||||
|
pub enum SystemId {
|
||||||
|
Input,
|
||||||
|
PlayerControls,
|
||||||
|
Ghost,
|
||||||
|
Movement,
|
||||||
|
Audio,
|
||||||
|
Blinking,
|
||||||
|
DirectionalRender,
|
||||||
|
DirtyRender,
|
||||||
|
Render,
|
||||||
|
DebugRender,
|
||||||
|
Present,
|
||||||
|
Collision,
|
||||||
|
Item,
|
||||||
|
PlayerMovement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SystemId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", Into::<&'static str>::into(self).to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default, Debug)]
|
#[derive(Resource, Default, Debug)]
|
||||||
pub struct SystemTimings {
|
pub struct SystemTimings {
|
||||||
/// Map of system names to a queue of durations, using a circular buffer.
|
/// Map of system names to a queue of durations, using a circular buffer.
|
||||||
@@ -20,18 +50,18 @@ pub struct SystemTimings {
|
|||||||
///
|
///
|
||||||
/// Also, we use a micromap::Map as the number of systems is generally quite small.
|
/// Also, we use a micromap::Map as the number of systems is generally quite small.
|
||||||
/// Just make sure to set the capacity appropriately, or it will panic.
|
/// Just make sure to set the capacity appropriately, or it will panic.
|
||||||
pub timings: RwLock<Map<&'static str, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, MAX_SYSTEMS>>,
|
pub timings: RwLock<Map<SystemId, Mutex<CircularBuffer<TIMING_WINDOW_SIZE, Duration>>, MAX_SYSTEMS>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemTimings {
|
impl SystemTimings {
|
||||||
pub fn add_timing(&self, name: &'static str, duration: Duration) {
|
pub fn add_timing(&self, id: SystemId, duration: Duration) {
|
||||||
// acquire a upgradable read lock
|
// acquire a upgradable read lock
|
||||||
let mut timings = self.timings.upgradable_read();
|
let mut timings = self.timings.upgradable_read();
|
||||||
|
|
||||||
// happy path, the name is already in the map (no need to mutate the hashmap)
|
// happy path, the name is already in the map (no need to mutate the hashmap)
|
||||||
if timings.contains_key(name) {
|
if timings.contains_key(&id) {
|
||||||
let queue = timings
|
let queue = timings
|
||||||
.get(name)
|
.get(&id)
|
||||||
.expect("System name not found in map after contains_key check");
|
.expect("System name not found in map after contains_key check");
|
||||||
let mut queue = queue.lock();
|
let mut queue = queue.lock();
|
||||||
|
|
||||||
@@ -41,16 +71,16 @@ impl SystemTimings {
|
|||||||
|
|
||||||
// otherwise, acquire a write lock and insert a new queue
|
// otherwise, acquire a write lock and insert a new queue
|
||||||
timings.with_upgraded(|timings| {
|
timings.with_upgraded(|timings| {
|
||||||
let queue = timings.entry(name).or_insert_with(|| Mutex::new(CircularBuffer::new()));
|
let queue = timings.entry(id).or_insert_with(|| Mutex::new(CircularBuffer::new()));
|
||||||
queue.lock().push_back(duration);
|
queue.lock().push_back(duration);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_stats(&self) -> Map<&'static str, (Duration, Duration), MAX_SYSTEMS> {
|
pub fn get_stats(&self) -> Map<SystemId, (Duration, Duration), MAX_SYSTEMS> {
|
||||||
let timings = self.timings.read();
|
let timings = self.timings.read();
|
||||||
let mut stats = Map::new();
|
let mut stats = Map::new();
|
||||||
|
|
||||||
for (name, queue) in timings.iter() {
|
for (id, queue) in timings.iter() {
|
||||||
if queue.lock().is_empty() {
|
if queue.lock().is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -65,7 +95,7 @@ impl SystemTimings {
|
|||||||
let std_dev = variance.sqrt();
|
let std_dev = variance.sqrt();
|
||||||
|
|
||||||
stats.insert(
|
stats.insert(
|
||||||
*name,
|
*id,
|
||||||
(
|
(
|
||||||
Duration::from_secs_f64(mean / 1000.0),
|
Duration::from_secs_f64(mean / 1000.0),
|
||||||
Duration::from_secs_f64(std_dev / 1000.0),
|
Duration::from_secs_f64(std_dev / 1000.0),
|
||||||
@@ -101,7 +131,7 @@ impl SystemTimings {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_timing_display(&self) -> String {
|
pub fn format_timing_display(&self) -> SmallVec<[String; SystemId::COUNT]> {
|
||||||
let stats = self.get_stats();
|
let stats = self.get_stats();
|
||||||
let (total_avg, total_std) = self.get_total_stats();
|
let (total_avg, total_std) = self.get_total_stats();
|
||||||
|
|
||||||
@@ -126,11 +156,11 @@ impl SystemTimings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the formatting module to format the data
|
// Use the formatting module to format the data
|
||||||
crate::systems::formatting::format_timing_display(timing_data).join("\n")
|
formatting::format_timing_display(timing_data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn profile<S, M>(name: &'static str, system: S) -> impl FnMut(&mut bevy_ecs::world::World)
|
pub fn profile<S, M>(id: SystemId, system: S) -> impl FnMut(&mut bevy_ecs::world::World)
|
||||||
where
|
where
|
||||||
S: IntoSystem<(), (), M> + 'static,
|
S: IntoSystem<(), (), M> + 'static,
|
||||||
{
|
{
|
||||||
@@ -147,7 +177,7 @@ where
|
|||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
|
|
||||||
if let Some(timings) = world.get_resource::<SystemTimings>() {
|
if let Some(timings) = world.get_resource::<SystemTimings>() {
|
||||||
timings.add_timing(name, duration);
|
timings.add_timing(id, duration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, RenderDirty, Renderable};
|
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 crate::texture::sprite::SpriteAtlas;
|
||||||
use bevy_ecs::entity::Entity;
|
use bevy_ecs::entity::Entity;
|
||||||
use bevy_ecs::event::EventWriter;
|
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.
|
/// 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<(&MovementState, &Movable, &mut DirectionalAnimated, &mut Renderable)>,
|
mut renderables: Query<(&Position, &Velocity, &mut DirectionalAnimated, &mut Renderable)>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
for (movement_state, movable, mut texture, mut renderable) in renderables.iter_mut() {
|
for (position, velocity, mut texture, mut renderable) in renderables.iter_mut() {
|
||||||
let stopped = matches!(movement_state, MovementState::Stopped);
|
let stopped = matches!(position, Position::Stopped { .. });
|
||||||
let current_direction = movable.current_direction;
|
let current_direction = velocity.direction;
|
||||||
|
|
||||||
let texture = if stopped {
|
let texture = if stopped {
|
||||||
texture.stopped_textures[current_direction.as_usize()].as_mut()
|
texture.stopped_textures[current_direction.as_usize()].as_mut()
|
||||||
@@ -96,7 +96,7 @@ pub fn render_system(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = position.get_pixel_pos(&map.graph);
|
let pos = position.get_pixel_position(&map.graph);
|
||||||
match pos {
|
match pos {
|
||||||
Ok(pos) => {
|
Ok(pos) => {
|
||||||
let dest = crate::helpers::centered_with_size(
|
let dest = crate::helpers::centered_with_size(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use pacman::systems::profiling::SystemTimings;
|
use pacman::systems::profiling::{SystemId, SystemTimings};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -6,12 +6,12 @@ fn test_timing_statistics() {
|
|||||||
let timings = SystemTimings::default();
|
let timings = SystemTimings::default();
|
||||||
|
|
||||||
// Add some test data
|
// Add some test data
|
||||||
timings.add_timing("test_system", Duration::from_millis(10));
|
timings.add_timing(SystemId::PlayerControls, Duration::from_millis(10));
|
||||||
timings.add_timing("test_system", Duration::from_millis(12));
|
timings.add_timing(SystemId::PlayerControls, Duration::from_millis(12));
|
||||||
timings.add_timing("test_system", Duration::from_millis(8));
|
timings.add_timing(SystemId::PlayerControls, Duration::from_millis(8));
|
||||||
|
|
||||||
let stats = timings.get_stats();
|
let stats = timings.get_stats();
|
||||||
let (avg, std_dev) = stats.get("test_system").unwrap();
|
let (avg, std_dev) = stats.get(&SystemId::PlayerControls).unwrap();
|
||||||
|
|
||||||
// Average should be 10ms, standard deviation should be small
|
// Average should be 10ms, standard deviation should be small
|
||||||
assert!((avg.as_millis() as f64 - 10.0).abs() < 1.0);
|
assert!((avg.as_millis() as f64 - 10.0).abs() < 1.0);
|
||||||
|
|||||||
Reference in New Issue
Block a user