Compare commits

...

2 Commits

6 changed files with 184 additions and 120 deletions

View File

@@ -56,6 +56,6 @@ on_change_strategy = "kill_then_restart"
[keybindings] [keybindings]
c = "job:clippy" c = "job:clippy"
shift-c = "job:check" alt-c = "job:check"
ctrl-shift-c = "job:check-all" ctrl-alt-c = "job:check-all"
ctrl-c = "job:clippy-all" shift-c = "job:clippy-all"

View File

@@ -4,20 +4,17 @@
//! animation, and rendering. Ghosts move through the game graph using //! animation, and rendering. Ghosts move through the game graph using
//! a traverser and display directional animated textures. //! a traverser and display directional animated textures.
use glam::Vec2;
use pathfinding::prelude::dijkstra; use pathfinding::prelude::dijkstra;
use rand::prelude::*; use rand::prelude::*;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::constants::BOARD_PIXEL_OFFSET;
use crate::entity::direction::Direction; use crate::entity::direction::Direction;
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId}; use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
use crate::entity::traversal::{Position, Traverser}; use crate::entity::r#trait::Entity;
use crate::helpers::centered_with_size; use crate::entity::traversal::Traverser;
use crate::texture::animated::AnimatedTexture; use crate::texture::animated::AnimatedTexture;
use crate::texture::directional::DirectionalAnimatedTexture; use crate::texture::directional::DirectionalAnimatedTexture;
use crate::texture::sprite::SpriteAtlas; use crate::texture::sprite::SpriteAtlas;
use sdl2::render::{Canvas, RenderTarget};
/// Determines if a ghost can traverse a given edge. /// Determines if a ghost can traverse a given edge.
/// ///
@@ -73,6 +70,42 @@ pub struct Ghost {
speed: f32, speed: f32,
} }
impl Entity for Ghost {
fn traverser(&self) -> &Traverser {
&self.traverser
}
fn traverser_mut(&mut self) -> &mut Traverser {
&mut self.traverser
}
fn texture(&self) -> &DirectionalAnimatedTexture {
&self.texture
}
fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture {
&mut self.texture
}
fn speed(&self) -> f32 {
self.speed
}
fn can_traverse(&self, edge: Edge) -> bool {
can_ghost_traverse(edge)
}
fn tick(&mut self, dt: f32, graph: &Graph) {
// Choose random direction when at a node
if self.traverser.position.is_at_node() {
self.choose_random_direction(graph);
}
self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse);
self.texture.tick(dt);
}
}
impl Ghost { impl Ghost {
/// Creates a new ghost instance at the specified starting node. /// Creates a new ghost instance at the specified starting node.
/// ///
@@ -113,20 +146,6 @@ impl Ghost {
} }
} }
/// Updates the ghost's position and animation state.
///
/// Advances movement through the graph, updates texture animation,
/// and chooses random directions at intersections.
pub fn tick(&mut self, dt: f32, graph: &Graph) {
// Choose random direction when at a node
if self.traverser.position.is_at_node() {
self.choose_random_direction(graph);
}
self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse);
self.texture.tick(dt);
}
/// Chooses a random available direction at the current intersection. /// Chooses a random available direction at the current intersection.
fn choose_random_direction(&mut self, graph: &Graph) { fn choose_random_direction(&mut self, graph: &Graph) {
let current_node = self.traverser.position.from_node_id(); let current_node = self.traverser.position.from_node_id();
@@ -158,24 +177,6 @@ impl Ghost {
} }
} }
/// 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.
fn get_pixel_pos(&self, graph: &Graph) -> Vec2 {
let pos = match self.traverser.position {
Position::AtNode(node_id) => graph.get_node(node_id).unwrap().position,
Position::BetweenNodes { from, to, traversed } => {
let from_pos = graph.get_node(from).unwrap().position;
let to_pos = graph.get_node(to).unwrap().position;
let edge = graph.find_edge(from, to).unwrap();
from_pos + (to_pos - from_pos) * (traversed / edge.distance)
}
};
Vec2::new(pos.x + BOARD_PIXEL_OFFSET.x as f32, pos.y + BOARD_PIXEL_OFFSET.y as f32)
}
/// Calculates the shortest path from the ghost's current position to a target node using Dijkstra's algorithm. /// Calculates the shortest path from the ghost's current position to a target node using Dijkstra's algorithm.
/// ///
/// Returns a vector of NodeIds representing the path, or None if no path exists. /// Returns a vector of NodeIds representing the path, or None if no path exists.
@@ -209,26 +210,4 @@ impl Ghost {
GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange
} }
} }
/// Renders the ghost at its current position.
///
/// Draws the appropriate directional sprite based on the ghost's
/// current movement state and direction.
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) {
let pixel_pos = self.get_pixel_pos(graph);
let dest = centered_with_size(
glam::IVec2::new(pixel_pos.x as i32, pixel_pos.y as i32),
glam::UVec2::new(16, 16),
);
if self.traverser.position.is_stopped() {
self.texture
.render_stopped(canvas, atlas, dest, self.traverser.direction)
.expect("Failed to render ghost");
} else {
self.texture
.render(canvas, atlas, dest, self.traverser.direction)
.expect("Failed to render ghost");
}
}
} }

View File

@@ -2,4 +2,5 @@ pub mod direction;
pub mod ghost; pub mod ghost;
pub mod graph; pub mod graph;
pub mod pacman; pub mod pacman;
pub mod r#trait;
pub mod traversal; pub mod traversal;

View File

@@ -4,18 +4,14 @@
//! animation, and rendering. Pac-Man moves through the game graph using //! animation, and rendering. Pac-Man moves through the game graph using
//! a traverser and displays directional animated textures. //! a traverser and displays directional animated textures.
use glam::{UVec2, Vec2};
use crate::constants::BOARD_PIXEL_OFFSET;
use crate::entity::direction::Direction; use crate::entity::direction::Direction;
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId}; use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
use crate::entity::traversal::{Position, Traverser}; use crate::entity::r#trait::Entity;
use crate::helpers::centered_with_size; use crate::entity::traversal::Traverser;
use crate::texture::animated::AnimatedTexture; use crate::texture::animated::AnimatedTexture;
use crate::texture::directional::DirectionalAnimatedTexture; use crate::texture::directional::DirectionalAnimatedTexture;
use crate::texture::sprite::SpriteAtlas; use crate::texture::sprite::SpriteAtlas;
use sdl2::keyboard::Keycode; use sdl2::keyboard::Keycode;
use sdl2::render::{Canvas, RenderTarget};
/// Determines if Pac-Man can traverse a given edge. /// Determines if Pac-Man can traverse a given edge.
/// ///
@@ -35,6 +31,37 @@ pub struct Pacman {
texture: DirectionalAnimatedTexture, texture: DirectionalAnimatedTexture,
} }
impl Entity for Pacman {
fn traverser(&self) -> &Traverser {
&self.traverser
}
fn traverser_mut(&mut self) -> &mut Traverser {
&mut self.traverser
}
fn texture(&self) -> &DirectionalAnimatedTexture {
&self.texture
}
fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture {
&mut self.texture
}
fn speed(&self) -> f32 {
1.125
}
fn can_traverse(&self, edge: Edge) -> bool {
can_pacman_traverse(edge)
}
fn tick(&mut self, dt: f32, graph: &Graph) {
self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse);
self.texture.tick(dt);
}
}
impl Pacman { impl Pacman {
/// Creates a new Pac-Man instance at the specified starting node. /// Creates a new Pac-Man instance at the specified starting node.
/// ///
@@ -70,15 +97,6 @@ impl Pacman {
} }
} }
/// Updates Pac-Man's position and animation state.
///
/// Advances movement through the graph and updates texture animation.
/// Movement speed is scaled by 60 FPS and a 1.125 multiplier.
pub fn tick(&mut self, dt: f32, graph: &Graph) {
self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse);
self.texture.tick(dt);
}
/// Handles keyboard input to change Pac-Man's direction. /// Handles keyboard input to change Pac-Man's direction.
/// ///
/// Maps arrow keys to directions and queues the direction change /// Maps arrow keys to directions and queues the direction change
@@ -96,47 +114,4 @@ impl Pacman {
self.traverser.set_next_direction(direction); self.traverser.set_next_direction(direction);
} }
} }
/// Calculates the current pixel position in the game world.
///
/// Interpolates between nodes when moving between them.
fn get_pixel_pos(&self, graph: &Graph) -> Vec2 {
match self.traverser.position {
Position::AtNode(node_id) => graph.get_node(node_id).unwrap().position,
Position::BetweenNodes { from, to, traversed } => {
let from_pos = graph.get_node(from).unwrap().position;
let to_pos = graph.get_node(to).unwrap().position;
from_pos.lerp(to_pos, traversed / from_pos.distance(to_pos))
}
}
}
/// Returns the current node ID that Pac-Man is at or moving towards.
///
/// If Pac-Man is at a node, returns that node ID.
/// If Pac-Man is between nodes, returns the node it's moving towards.
pub fn current_node_id(&self) -> NodeId {
match self.traverser.position {
Position::AtNode(node_id) => node_id,
Position::BetweenNodes { to, .. } => to,
}
}
/// Renders Pac-Man to the canvas.
///
/// Calculates screen position, determines if Pac-Man is stopped,
/// and renders the appropriate directional texture.
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) {
let pixel_pos = self.get_pixel_pos(graph).round().as_ivec2() + BOARD_PIXEL_OFFSET.as_ivec2();
let dest = centered_with_size(pixel_pos, UVec2::new(16, 16));
let is_stopped = self.traverser.position.is_stopped();
if is_stopped {
self.texture
.render_stopped(canvas, atlas, dest, self.traverser.direction)
.unwrap();
} else {
self.texture.render(canvas, atlas, dest, self.traverser.direction).unwrap();
}
}
} }

108
src/entity/trait.rs Normal file
View File

@@ -0,0 +1,108 @@
//! Entity trait for common movement and rendering functionality.
//!
//! This module defines a trait that captures the shared behavior between
//! different game entities like Ghosts and Pac-Man, including movement,
//! rendering, and position calculations.
use glam::Vec2;
use sdl2::render::{Canvas, RenderTarget};
use crate::entity::direction::Direction;
use crate::entity::graph::{Edge, Graph, NodeId};
use crate::entity::traversal::{Position, Traverser};
use crate::texture::directional::DirectionalAnimatedTexture;
use crate::texture::sprite::SpriteAtlas;
/// Trait defining common functionality for game entities that move through the graph.
///
/// This trait provides a unified interface for entities that:
/// - Move through the game graph using a traverser
/// - Render using directional animated textures
/// - Have position calculations and movement speed
#[allow(dead_code)]
pub trait Entity {
/// Returns a reference to the entity's traverser for movement control.
fn traverser(&self) -> &Traverser;
/// Returns a mutable reference to the entity's traverser for movement control.
fn traverser_mut(&mut self) -> &mut Traverser;
/// Returns a reference to the entity's directional animated texture.
fn texture(&self) -> &DirectionalAnimatedTexture;
/// Returns a mutable reference to the entity's directional animated texture.
fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture;
/// Returns the movement speed multiplier for this entity.
fn speed(&self) -> f32;
/// Determines if this entity can traverse a given edge.
fn can_traverse(&self, edge: Edge) -> bool;
/// Updates the entity's position and animation state.
///
/// This method advances movement through the graph and updates texture animation.
fn tick(&mut self, dt: f32, graph: &Graph);
/// 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.
fn get_pixel_pos(&self, graph: &Graph) -> Vec2 {
let pos = match self.traverser().position {
Position::AtNode(node_id) => graph.get_node(node_id).unwrap().position,
Position::BetweenNodes { from, to, traversed } => {
let from_pos = graph.get_node(from).unwrap().position;
let to_pos = graph.get_node(to).unwrap().position;
let edge = graph.find_edge(from, to).unwrap();
from_pos + (to_pos - from_pos) * (traversed / edge.distance)
}
};
Vec2::new(
pos.x + crate::constants::BOARD_PIXEL_OFFSET.x as f32,
pos.y + crate::constants::BOARD_PIXEL_OFFSET.y as f32,
)
}
/// Returns the current node ID that the entity is at or moving towards.
///
/// If the entity is at a node, returns that node ID.
/// If the entity is between nodes, returns the node it's moving towards.
fn current_node_id(&self) -> NodeId {
match self.traverser().position {
Position::AtNode(node_id) => node_id,
Position::BetweenNodes { to, .. } => to,
}
}
/// Sets the next direction for the entity to take.
///
/// The direction is buffered and will be applied at the next opportunity,
/// typically when the entity reaches a new node.
fn set_next_direction(&mut self, direction: Direction) {
self.traverser_mut().set_next_direction(direction);
}
/// Renders the entity at its current position.
///
/// Draws the appropriate directional sprite based on the entity's
/// current movement state and direction.
fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) {
let pixel_pos = self.get_pixel_pos(graph);
let dest = crate::helpers::centered_with_size(
glam::IVec2::new(pixel_pos.x as i32, pixel_pos.y as i32),
glam::UVec2::new(16, 16),
);
if self.traverser().position.is_stopped() {
self.texture()
.render_stopped(canvas, atlas, dest, self.traverser().direction)
.expect("Failed to render entity");
} else {
self.texture()
.render(canvas, atlas, dest, self.traverser().direction)
.expect("Failed to render entity");
}
}
}

View File

@@ -18,6 +18,7 @@ use crate::{
entity::{ entity::{
ghost::{Ghost, GhostType}, ghost::{Ghost, GhostType},
pacman::Pacman, pacman::Pacman,
r#trait::Entity,
}, },
map::Map, map::Map,
texture::{ texture::{