diff --git a/src/app.rs b/src/app.rs index 826e1bf..f796513 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,11 @@ use std::time::{Duration, Instant}; use glam::Vec2; -use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator}; +use sdl2::render::TextureCreator; use sdl2::ttf::Sdl2TtfContext; -use sdl2::video::{Window, WindowContext}; +use sdl2::video::WindowContext; use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem}; -use tracing::{error, warn}; +use tracing::warn; use crate::error::{GameError, GameResult}; diff --git a/src/ecs/components.rs b/src/ecs/components.rs new file mode 100644 index 0000000..3ab2e89 --- /dev/null +++ b/src/ecs/components.rs @@ -0,0 +1,138 @@ +use bevy_ecs::{bundle::Bundle, component::Component, resource::Resource}; +use glam::Vec2; + +use crate::{ + entity::{direction::Direction, graph::Graph}, + error::{EntityError, GameResult}, + texture::{animated::AnimatedTexture, sprite::AtlasTile}, +}; + +/// A tag component for entities that are controlled by the player. +#[derive(Default, Component)] +pub struct PlayerControlled; + +/// A component for entities that have a sprite, with a layer for ordering. +/// +/// This is intended to be modified by other entities allowing animation. +#[derive(Component)] +pub struct Renderable { + pub sprite: AtlasTile, + pub layer: u8, +} + +/// A component for entities that have a directional animated texture. +#[derive(Component)] +pub struct DirectionalAnimated { + pub textures: [Option; 4], + pub stopped_textures: [Option; 4], +} + +/// A unique identifier for a node, represented by its index in the graph's storage. +pub type NodeId = usize; + +/// Represents the current position of an entity traversing the graph. +/// +/// This enum allows for precise tracking of whether an entity is exactly at a node +/// or moving along an edge between two nodes. +#[derive(Component, Debug, Copy, Clone, PartialEq)] +pub enum Position { + /// The traverser is located exactly at a node. + AtNode(NodeId), + /// The traverser is on an edge between two nodes. + BetweenNodes { + from: NodeId, + to: NodeId, + /// The floating-point distance traversed along the edge from the `from` node. + traversed: f32, + }, +} + +impl Position { + /// Calculates the current pixel position in the game world. + /// + /// Converts the graph position to screen coordinates, accounting for + /// the board offset and centering the sprite. + pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult { + let pos = match self { + Position::AtNode(node_id) => { + let node = graph.get_node(*node_id).ok_or(EntityError::NodeNotFound(*node_id))?; + node.position + } + Position::BetweenNodes { from, to, traversed } => { + 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 })?; + from_node.position + (to_node.position - from_node.position) * (traversed / edge.distance) + } + }; + + Ok(Vec2::new( + pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32, + pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32, + )) + } +} + +impl Default for Position { + fn default() -> Self { + Position::AtNode(0) + } +} + +#[allow(dead_code)] +impl Position { + /// Returns `true` if the position is exactly at a node. + pub fn is_at_node(&self) -> bool { + matches!(self, Position::AtNode(_)) + } + + /// Returns the `NodeId` of the current or most recently departed node. + #[allow(clippy::wrong_self_convention)] + pub fn from_node_id(&self) -> NodeId { + match self { + Position::AtNode(id) => *id, + Position::BetweenNodes { from, .. } => *from, + } + } + + /// Returns the `NodeId` of the destination node, if currently on an edge. + #[allow(clippy::wrong_self_convention)] + pub fn to_node_id(&self) -> Option { + match self { + Position::AtNode(_) => None, + Position::BetweenNodes { to, .. } => Some(*to), + } + } + + /// Returns `true` if the traverser is stopped at a node. + pub fn is_stopped(&self) -> bool { + matches!(self, Position::AtNode(_)) + } +} + +/// A component for entities that have a velocity, with a direction and speed. +#[derive(Default, Component)] +pub struct Velocity { + pub direction: Direction, + pub next_direction: Option<(Direction, u8)>, + pub speed: f32, +} + +#[derive(Bundle)] +pub struct PlayerBundle { + pub player: PlayerControlled, + pub position: Position, + pub velocity: Velocity, + pub sprite: Renderable, + pub directional_animated: DirectionalAnimated, +} + +#[derive(Resource)] +pub struct GlobalState { + pub exit: bool, +} + +#[derive(Resource)] +pub struct DeltaTime(pub f32); diff --git a/src/ecs/interact.rs b/src/ecs/interact.rs index 2273052..8ac4588 100644 --- a/src/ecs/interact.rs +++ b/src/ecs/interact.rs @@ -1,141 +1,15 @@ use bevy_ecs::{ event::{EventReader, EventWriter}, - system::{Query, Res, ResMut}, + system::{Query, ResMut}, }; -use tracing::debug; -use crate::entity::graph::EdgePermissions; use crate::{ - ecs::{DeltaTime, GlobalState, PlayerControlled, Position, Velocity}, - error::{EntityError, GameError}, + ecs::components::{GlobalState, PlayerControlled, Velocity}, + error::GameError, game::events::GameEvent, input::commands::GameCommand, - map::builder::Map, }; -pub fn movement_system( - map: Res, - delta_time: Res, - mut entities: Query<(&mut PlayerControlled, &mut Velocity, &mut Position)>, - mut errors: EventWriter, -) { - for (mut player, mut velocity, mut position) in entities.iter_mut() { - let distance = velocity.speed * 60.0 * delta_time.0; - - // Decrement the remaining frames for the next direction - if let Some((direction, remaining)) = velocity.next_direction { - if remaining > 0 { - velocity.next_direction = Some((direction, remaining - 1)); - } else { - velocity.next_direction = None; - } - } - - match *position { - Position::AtNode(node_id) => { - // We're not moving, but a buffered direction is available. - if let Some((next_direction, _)) = velocity.next_direction { - if let Some(edge) = map.graph.find_edge_in_direction(node_id, next_direction) { - if can_traverse(&mut player, edge) { - // Start moving in that direction - *position = Position::BetweenNodes { - from: node_id, - to: edge.target, - traversed: distance, - }; - velocity.direction = next_direction; - velocity.next_direction = None; - } - } else { - errors.write( - EntityError::InvalidMovement(format!( - "No edge found in direction {:?} from node {}", - next_direction, node_id - )) - .into(), - ); - } - } - } - Position::BetweenNodes { from, to, traversed } => { - // There is no point in any of the next logic if we don't travel at all - if distance <= 0.0 { - return; - } - - let edge = map - .graph - .find_edge(from, to) - .ok_or_else(|| { - errors.write( - EntityError::InvalidMovement(format!( - "Inconsistent state: Traverser is on a non-existent edge from {} to {}.", - from, to - )) - .into(), - ); - return; - }) - .unwrap(); - - let new_traversed = traversed + distance; - - if new_traversed < edge.distance { - // Still on the same edge, just update the distance. - *position = Position::BetweenNodes { - from, - to, - traversed: new_traversed, - }; - } else { - let overflow = new_traversed - edge.distance; - let mut moved = false; - - // If we buffered a direction, try to find an edge in that direction - if let Some((next_dir, _)) = velocity.next_direction { - if let Some(edge) = map.graph.find_edge_in_direction(to, next_dir) { - if can_traverse(&mut player, edge) { - *position = Position::BetweenNodes { - from: to, - to: edge.target, - traversed: overflow, - }; - - velocity.direction = next_dir; // Remember our new direction - velocity.next_direction = None; // Consume the buffered direction - moved = true; - } - } - } - - // If we didn't move, try to continue in the current direction - if !moved { - if let Some(edge) = map.graph.find_edge_in_direction(to, velocity.direction) { - if can_traverse(&mut player, edge) { - *position = Position::BetweenNodes { - from: to, - to: edge.target, - traversed: overflow, - }; - } else { - *position = Position::AtNode(to); - velocity.next_direction = None; - } - } else { - *position = Position::AtNode(to); - velocity.next_direction = None; - } - } - } - } - } - } -} - -fn can_traverse(_player: &mut PlayerControlled, edge: crate::entity::graph::Edge) -> bool { - matches!(edge.permissions, EdgePermissions::All) -} - // Handles pub fn interact_system( mut events: EventReader, diff --git a/src/ecs/mod.rs b/src/ecs/mod.rs index 375cdcf..11f7459 100644 --- a/src/ecs/mod.rs +++ b/src/ecs/mod.rs @@ -3,148 +3,7 @@ //! This module contains all the ECS-related logic, including components, systems, //! and resources. -use bevy_ecs::{bundle::Bundle, component::Component, resource::Resource}; -use glam::Vec2; - -use crate::{ - entity::{direction::Direction, graph::Graph, traversal}, - error::{EntityError, GameResult}, - texture::{ - animated::AnimatedTexture, - directional::DirectionalAnimatedTexture, - sprite::{AtlasTile, Sprite}, - }, -}; - -/// A tag component for entities that are controlled by the player. -#[derive(Default, Component)] -pub struct PlayerControlled; - -/// A component for entities that have a sprite, with a layer for ordering. -/// -/// This is intended to be modified by other entities allowing animation. -#[derive(Component)] -pub struct Renderable { - pub sprite: AtlasTile, - pub layer: u8, -} - -/// A component for entities that have a directional animated texture. -#[derive(Component)] -pub struct DirectionalAnimated { - pub textures: [Option; 4], - pub stopped_textures: [Option; 4], -} - -/// A unique identifier for a node, represented by its index in the graph's storage. -pub type NodeId = usize; - -/// Represents the current position of an entity traversing the graph. -/// -/// This enum allows for precise tracking of whether an entity is exactly at a node -/// or moving along an edge between two nodes. -#[derive(Component, Debug, Copy, Clone, PartialEq)] -pub enum Position { - /// The traverser is located exactly at a node. - AtNode(NodeId), - /// The traverser is on an edge between two nodes. - BetweenNodes { - from: NodeId, - to: NodeId, - /// The floating-point distance traversed along the edge from the `from` node. - traversed: f32, - }, -} - -impl Position { - /// Calculates the current pixel position in the game world. - /// - /// Converts the graph position to screen coordinates, accounting for - /// the board offset and centering the sprite. - pub fn get_pixel_pos(&self, graph: &Graph) -> GameResult { - let pos = match self { - Position::AtNode(node_id) => { - let node = graph.get_node(*node_id).ok_or(EntityError::NodeNotFound(*node_id))?; - node.position - } - Position::BetweenNodes { from, to, traversed } => { - 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 })?; - from_node.position + (to_node.position - from_node.position) * (traversed / edge.distance) - } - }; - - Ok(Vec2::new( - pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32, - pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32, - )) - } -} - -impl Default for Position { - fn default() -> Self { - Position::AtNode(0) - } -} - -#[allow(dead_code)] -impl Position { - /// Returns `true` if the position is exactly at a node. - pub fn is_at_node(&self) -> bool { - matches!(self, Position::AtNode(_)) - } - - /// Returns the `NodeId` of the current or most recently departed node. - #[allow(clippy::wrong_self_convention)] - pub fn from_node_id(&self) -> NodeId { - match self { - Position::AtNode(id) => *id, - Position::BetweenNodes { from, .. } => *from, - } - } - - /// Returns the `NodeId` of the destination node, if currently on an edge. - #[allow(clippy::wrong_self_convention)] - pub fn to_node_id(&self) -> Option { - match self { - Position::AtNode(_) => None, - Position::BetweenNodes { to, .. } => Some(*to), - } - } - - /// Returns `true` if the traverser is stopped at a node. - pub fn is_stopped(&self) -> bool { - matches!(self, Position::AtNode(_)) - } -} - -/// A component for entities that have a velocity, with a direction and speed. -#[derive(Default, Component)] -pub struct Velocity { - pub direction: Direction, - pub next_direction: Option<(Direction, u8)>, - pub speed: f32, -} - -#[derive(Bundle)] -pub struct PlayerBundle { - pub player: PlayerControlled, - pub position: Position, - pub velocity: Velocity, - pub sprite: Renderable, - pub directional_animated: DirectionalAnimated, -} - -#[derive(Resource)] -pub struct GlobalState { - pub exit: bool, -} - -#[derive(Resource)] -pub struct DeltaTime(pub f32); - +pub mod components; pub mod interact; +pub mod movement; pub mod render; diff --git a/src/ecs/movement.rs b/src/ecs/movement.rs new file mode 100644 index 0000000..10f5a91 --- /dev/null +++ b/src/ecs/movement.rs @@ -0,0 +1,129 @@ +use crate::ecs::components::{DeltaTime, PlayerControlled, Position, Velocity}; +use crate::entity::graph::EdgePermissions; +use crate::error::{EntityError, GameError}; +use crate::map::builder::Map; +use bevy_ecs::event::EventWriter; +use bevy_ecs::system::{Query, Res}; + +fn can_traverse(_player: &mut PlayerControlled, edge: crate::entity::graph::Edge) -> bool { + matches!(edge.permissions, EdgePermissions::All) +} + +pub fn movement_system( + map: Res, + delta_time: Res, + mut entities: Query<(&mut PlayerControlled, &mut Velocity, &mut Position)>, + mut errors: EventWriter, +) { + for (mut player, mut velocity, mut position) in entities.iter_mut() { + let distance = velocity.speed * 60.0 * delta_time.0; + + // Decrement the remaining frames for the next direction + if let Some((direction, remaining)) = velocity.next_direction { + if remaining > 0 { + velocity.next_direction = Some((direction, remaining - 1)); + } else { + velocity.next_direction = None; + } + } + + match *position { + Position::AtNode(node_id) => { + // We're not moving, but a buffered direction is available. + if let Some((next_direction, _)) = velocity.next_direction { + if let Some(edge) = map.graph.find_edge_in_direction(node_id, next_direction) { + if can_traverse(&mut player, edge) { + // Start moving in that direction + *position = Position::BetweenNodes { + from: node_id, + to: edge.target, + traversed: distance, + }; + velocity.direction = next_direction; + velocity.next_direction = None; + } + } else { + errors.write( + EntityError::InvalidMovement(format!( + "No edge found in direction {:?} from node {}", + next_direction, node_id + )) + .into(), + ); + } + } + } + Position::BetweenNodes { from, to, traversed } => { + // There is no point in any of the next logic if we don't travel at all + if distance <= 0.0 { + return; + } + + let edge = map + .graph + .find_edge(from, to) + .ok_or_else(|| { + errors.write( + EntityError::InvalidMovement(format!( + "Inconsistent state: Traverser is on a non-existent edge from {} to {}.", + from, to + )) + .into(), + ); + return; + }) + .unwrap(); + + let new_traversed = traversed + distance; + + if new_traversed < edge.distance { + // Still on the same edge, just update the distance. + *position = Position::BetweenNodes { + from, + to, + traversed: new_traversed, + }; + } else { + let overflow = new_traversed - edge.distance; + let mut moved = false; + + // If we buffered a direction, try to find an edge in that direction + if let Some((next_dir, _)) = velocity.next_direction { + if let Some(edge) = map.graph.find_edge_in_direction(to, next_dir) { + if can_traverse(&mut player, edge) { + *position = Position::BetweenNodes { + from: to, + to: edge.target, + traversed: overflow, + }; + + velocity.direction = next_dir; // Remember our new direction + velocity.next_direction = None; // Consume the buffered direction + moved = true; + } + } + } + + // If we didn't move, try to continue in the current direction + if !moved { + if let Some(edge) = map.graph.find_edge_in_direction(to, velocity.direction) { + if can_traverse(&mut player, edge) { + *position = Position::BetweenNodes { + from: to, + to: edge.target, + traversed: overflow, + }; + } else { + *position = Position::AtNode(to); + velocity.next_direction = None; + } + } else { + *position = Position::AtNode(to); + velocity.next_direction = None; + } + } + } + } + } + } +} diff --git a/src/ecs/render.rs b/src/ecs/render.rs index 84050b7..56d7db7 100644 --- a/src/ecs/render.rs +++ b/src/ecs/render.rs @@ -1,5 +1,5 @@ -use crate::ecs::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity}; -use crate::error::{EntityError, GameError, TextureError}; +use crate::ecs::components::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity}; +use crate::error::{GameError, TextureError}; use crate::map::builder::Map; use crate::texture::sprite::SpriteAtlas; use bevy_ecs::entity::Entity; diff --git a/src/entity/graph.rs b/src/entity/graph.rs index e19f60f..b6dffb5 100644 --- a/src/entity/graph.rs +++ b/src/entity/graph.rs @@ -1,6 +1,6 @@ use glam::Vec2; -use crate::ecs::NodeId; +use crate::ecs::components::NodeId; use super::direction::Direction; diff --git a/src/entity/traversal.rs b/src/entity/traversal.rs index 5b7b46d..1bec8a4 100644 --- a/src/entity/traversal.rs +++ b/src/entity/traversal.rs @@ -1,6 +1,4 @@ -use tracing::error; - -use crate::ecs::{NodeId, Position}; +use crate::ecs::components::Position; use crate::error::GameResult; use super::direction::Direction; diff --git a/src/game/mod.rs b/src/game/mod.rs index 1633fe3..7210fc0 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -3,24 +3,24 @@ include!(concat!(env!("OUT_DIR"), "/atlas_data.rs")); use crate::constants::CANVAS_SIZE; -use crate::ecs::interact::{interact_system, movement_system}; +use crate::ecs::components::{ + DeltaTime, DirectionalAnimated, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity, +}; +use crate::ecs::interact::interact_system; +use crate::ecs::movement::movement_system; use crate::ecs::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource}; -use crate::ecs::{DeltaTime, DirectionalAnimated, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity}; use crate::entity::direction::Direction; -use crate::entity::{graph, traversal}; use crate::error::{GameError, GameResult, TextureError}; use crate::input::commands::GameCommand; use crate::map::builder::Map; use crate::texture::animated::AnimatedTexture; -use crate::texture::directional::DirectionalAnimatedTexture; -use crate::texture::sprite::Sprite; use bevy_ecs::event::EventRegistry; use bevy_ecs::observer::Trigger; use bevy_ecs::schedule::IntoScheduleConfigs; -use bevy_ecs::system::{Commands, ResMut}; +use bevy_ecs::system::ResMut; use bevy_ecs::{schedule::Schedule, world::World}; use sdl2::image::LoadTexture; -use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator}; +use sdl2::render::{Canvas, ScaleMode, TextureCreator}; use sdl2::video::{Window, WindowContext}; use sdl2::EventPump; @@ -29,7 +29,7 @@ use crate::input::{handle_input, Bindings}; use crate::map::render::MapRenderer; use crate::{ constants, - texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas}, + texture::sprite::{AtlasMapper, SpriteAtlas}, }; use self::events::GameEvent; diff --git a/src/input/mod.rs b/src/input/mod.rs index 7d1d480..97ae3cf 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use bevy_ecs::{ event::EventWriter, resource::Resource, - system::{Commands, NonSendMut, Res}, + system::{NonSendMut, Res}, }; use sdl2::{event::Event, keyboard::Keycode, EventPump}; diff --git a/src/map/builder.rs b/src/map/builder.rs index 2a23e3d..49b32e3 100644 --- a/src/map/builder.rs +++ b/src/map/builder.rs @@ -1,12 +1,12 @@ //! Map construction and building functionality. -use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE, RAW_BOARD}; -use crate::ecs::NodeId; +use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE}; +use crate::ecs::components::NodeId; use crate::entity::direction::Direction; use crate::entity::graph::{EdgePermissions, Graph, Node}; use crate::map::parser::MapTileParser; use crate::map::render::MapRenderer; -use crate::texture::sprite::{Sprite, SpriteAtlas}; +use crate::texture::sprite::SpriteAtlas; use bevy_ecs::resource::Resource; use glam::{IVec2, Vec2}; use sdl2::render::{Canvas, RenderTarget}; diff --git a/src/texture/sprite.rs b/src/texture/sprite.rs index 41ebb69..b271c9e 100644 --- a/src/texture/sprite.rs +++ b/src/texture/sprite.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use bevy_ecs::resource::Resource; use glam::U16Vec2; use sdl2::pixels::Color; use sdl2::rect::Rect;