Compare commits

...

5 Commits

7 changed files with 228 additions and 137 deletions

20
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "monthly"
groups:
dependencies:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
groups:
dependencies:
patterns:
- "*"

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

@@ -1,7 +1,7 @@
//! This module contains the main game logic and state. //! This module contains the main game logic and state.
use anyhow::Result; use anyhow::Result;
use glam::UVec2; use glam::{UVec2, Vec2};
use rand::{rngs::SmallRng, Rng, SeedableRng}; use rand::{rngs::SmallRng, Rng, SeedableRng};
use sdl2::{ use sdl2::{
image::LoadTexture, image::LoadTexture,
@@ -14,10 +14,11 @@ use sdl2::{
use crate::{ use crate::{
asset::{get_asset_bytes, Asset}, asset::{get_asset_bytes, Asset},
audio::Audio, audio::Audio,
constants::RAW_BOARD, constants::{CELL_SIZE, RAW_BOARD},
entity::{ entity::{
ghost::{Ghost, GhostType}, ghost::{Ghost, GhostType},
pacman::Pacman, pacman::Pacman,
r#trait::Entity,
}, },
map::Map, map::Map,
texture::{ texture::{
@@ -187,7 +188,7 @@ impl Game {
fn render_pathfinding_debug<T: RenderTarget>(&self, canvas: &mut Canvas<T>) -> Result<()> { fn render_pathfinding_debug<T: RenderTarget>(&self, canvas: &mut Canvas<T>) -> Result<()> {
let pacman_node = self.pacman.current_node_id(); let pacman_node = self.pacman.current_node_id();
for (i, ghost) in self.ghosts.iter().enumerate() { for ghost in self.ghosts.iter() {
if let Some(path) = ghost.calculate_path_to_target(&self.map.graph, pacman_node) { if let Some(path) = ghost.calculate_path_to_target(&self.map.graph, pacman_node) {
if path.len() < 2 { if path.len() < 2 {
continue; // Skip if path is too short continue; // Skip if path is too short
@@ -197,34 +198,41 @@ impl Game {
canvas.set_draw_color(ghost.debug_color()); canvas.set_draw_color(ghost.debug_color());
// Calculate offset based on ghost index to prevent overlapping lines // Calculate offset based on ghost index to prevent overlapping lines
let offset = (i as f32) * 2.0 - 3.0; // Offset range: -3.0 to 3.0 // let offset = (i as f32) * 2.0 - 3.0; // Offset range: -3.0 to 3.0
// Calculate a consistent offset direction for the entire path // Calculate a consistent offset direction for the entire path
let first_node = self.map.graph.get_node(path[0]).unwrap(); // let first_node = self.map.graph.get_node(path[0]).unwrap();
let last_node = self.map.graph.get_node(path[path.len() - 1]).unwrap(); // let last_node = self.map.graph.get_node(path[path.len() - 1]).unwrap();
let first_pos = first_node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
let last_pos = last_node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
// Use the overall direction from start to end to determine the perpendicular offset // Use the overall direction from start to end to determine the perpendicular offset
let overall_dir = (last_pos - first_pos).normalize(); let offset = match ghost.ghost_type {
let perp_dir = glam::Vec2::new(-overall_dir.y, overall_dir.x); GhostType::Blinky => Vec2::new(0.25, 0.5),
GhostType::Pinky => Vec2::new(-0.25, -0.25),
GhostType::Inky => Vec2::new(0.5, -0.5),
GhostType::Clyde => Vec2::new(-0.5, 0.25),
} * 5.0;
// Calculate offset positions for all nodes using the same perpendicular direction // Calculate offset positions for all nodes using the same perpendicular direction
let mut offset_positions = Vec::new(); let mut offset_positions = Vec::new();
for &node_id in &path { for &node_id in &path {
let node = self.map.graph.get_node(node_id).unwrap(); let node = self.map.graph.get_node(node_id).unwrap();
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2(); let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
offset_positions.push(pos + perp_dir * offset); offset_positions.push(pos + offset);
} }
// Draw lines between the offset positions // Draw lines between the offset positions
for window in offset_positions.windows(2) { for window in offset_positions.windows(2) {
canvas if let (Some(from), Some(to)) = (window.first(), window.get(1)) {
.draw_line( // Skip if the distance is too far (used for preventing lines between tunnel portals)
(window[0].x as i32, window[0].y as i32), if from.distance_squared(*to) > (CELL_SIZE * 16).pow(2) as f32 {
(window[1].x as i32, window[1].y as i32), continue;
) }
.map_err(anyhow::Error::msg)?;
// Draw the line
canvas
.draw_line((from.x as i32, from.y as i32), (to.x as i32, to.y as i32))
.map_err(anyhow::Error::msg)?;
}
} }
} }
} }