mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-11 06:08:02 -06:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed8bd07518 | |||
| 27705f1ba2 | |||
| e964adc818 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -192,6 +192,7 @@ dependencies = [
|
|||||||
"sdl2",
|
"sdl2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"smallvec",
|
||||||
"spin_sleep",
|
"spin_sleep",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ anyhow = "1.0"
|
|||||||
glam = { version = "0.30.4", features = [] }
|
glam = { version = "0.30.4", features = [] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.141"
|
serde_json = "1.0.141"
|
||||||
|
smallvec = "1.15.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
width="80"
|
width="80"
|
||||||
height="80"
|
height="80"
|
||||||
viewBox="0 0 250 250"
|
viewBox="0 0 250 250"
|
||||||
class="fill-yellow-400 text-white"
|
class="fill-yellow-400 [&>.octo-arm,.octo-body]:fill-black"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
|
||||||
@@ -46,16 +46,12 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="min-h-screen flex flex-col">
|
<div class="min-h-screen flex flex-col">
|
||||||
<header class="pt-10">
|
|
||||||
<h1 class="text-4xl arcade-title scaled-text">Pac-Man in Rust</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="flex-1 flex items-center justify-center px-4">
|
<main class="flex-1 flex items-center justify-center px-4">
|
||||||
<div class="w-full max-w-5xl">
|
<div class="w-full max-w-5xl">
|
||||||
<canvas
|
<canvas
|
||||||
id="canvas"
|
id="canvas"
|
||||||
oncontextmenu="event.preventDefault()"
|
oncontextmenu="event.preventDefault()"
|
||||||
class="block bg-black w-full max-w-[90vw] h-auto rounded-xl shadow-[inset_0_0_0_2px_rgba(255,255,255,0.12),0_10px_30px_rgba(0,0,0,0.8)]"
|
class="block w-full h-full max-h-[90vh] aspect-square"
|
||||||
></canvas>
|
></canvas>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
14
src/app.rs
14
src/app.rs
@@ -1,6 +1,7 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
|
use glam::Vec2;
|
||||||
use sdl2::event::{Event, WindowEvent};
|
use sdl2::event::{Event, WindowEvent};
|
||||||
use sdl2::keyboard::Keycode;
|
use sdl2::keyboard::Keycode;
|
||||||
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
||||||
@@ -19,6 +20,7 @@ pub struct App<'a> {
|
|||||||
backbuffer: Texture<'a>,
|
backbuffer: Texture<'a>,
|
||||||
paused: bool,
|
paused: bool,
|
||||||
last_tick: Instant,
|
last_tick: Instant,
|
||||||
|
cursor_pos: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App<'_> {
|
impl App<'_> {
|
||||||
@@ -56,7 +58,7 @@ impl App<'_> {
|
|||||||
|
|
||||||
// Initial draw
|
// Initial draw
|
||||||
game.draw(&mut canvas, &mut backbuffer)?;
|
game.draw(&mut canvas, &mut backbuffer)?;
|
||||||
game.present_backbuffer(&mut canvas, &backbuffer)?;
|
game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
game,
|
game,
|
||||||
@@ -65,6 +67,7 @@ impl App<'_> {
|
|||||||
backbuffer,
|
backbuffer,
|
||||||
paused: false,
|
paused: false,
|
||||||
last_tick: Instant::now(),
|
last_tick: Instant::now(),
|
||||||
|
cursor_pos: Vec2::ZERO,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +112,10 @@ impl App<'_> {
|
|||||||
Event::KeyDown { keycode, .. } => {
|
Event::KeyDown { keycode, .. } => {
|
||||||
self.game.keyboard_event(keycode.unwrap());
|
self.game.keyboard_event(keycode.unwrap());
|
||||||
}
|
}
|
||||||
|
Event::MouseMotion { x, y, .. } => {
|
||||||
|
// Convert window coordinates to logical coordinates
|
||||||
|
self.cursor_pos = Vec2::new(x as f32, y as f32);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +128,10 @@ impl App<'_> {
|
|||||||
if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
|
if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
|
||||||
error!("Failed to draw game: {e}");
|
error!("Failed to draw game: {e}");
|
||||||
}
|
}
|
||||||
if let Err(e) = self.game.present_backbuffer(&mut self.canvas, &self.backbuffer) {
|
if let Err(e) = self
|
||||||
|
.game
|
||||||
|
.present_backbuffer(&mut self.canvas, &self.backbuffer, self.cursor_pos)
|
||||||
|
{
|
||||||
error!("Failed to present backbuffer: {e}");
|
error!("Failed to present backbuffer: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
216
src/entity/ghost.rs
Normal file
216
src/entity/ghost.rs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
//! Ghost entity implementation.
|
||||||
|
//!
|
||||||
|
//! This module contains the ghost character logic, including movement,
|
||||||
|
//! animation, and rendering. Ghosts move through the game graph using
|
||||||
|
//! a traverser and display directional animated textures.
|
||||||
|
|
||||||
|
use glam::Vec2;
|
||||||
|
use rand::prelude::*;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::constants::BOARD_PIXEL_OFFSET;
|
||||||
|
use crate::entity::direction::Direction;
|
||||||
|
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId, Position, Traverser};
|
||||||
|
use crate::helpers::centered_with_size;
|
||||||
|
use crate::texture::animated::AnimatedTexture;
|
||||||
|
use crate::texture::directional::DirectionalAnimatedTexture;
|
||||||
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
|
||||||
|
/// Determines if a ghost can traverse a given edge.
|
||||||
|
///
|
||||||
|
/// Ghosts can move through edges that allow all entities or ghost-only edges.
|
||||||
|
fn can_ghost_traverse(edge: Edge) -> bool {
|
||||||
|
matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The four classic ghost types.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum GhostType {
|
||||||
|
Blinky,
|
||||||
|
Pinky,
|
||||||
|
Inky,
|
||||||
|
Clyde,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GhostType {
|
||||||
|
/// Returns the ghost type name for atlas lookups.
|
||||||
|
pub fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
GhostType::Blinky => "blinky",
|
||||||
|
GhostType::Pinky => "pinky",
|
||||||
|
GhostType::Inky => "inky",
|
||||||
|
GhostType::Clyde => "clyde",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the base movement speed for this ghost type.
|
||||||
|
pub fn base_speed(self) -> f32 {
|
||||||
|
match self {
|
||||||
|
GhostType::Blinky => 1.0,
|
||||||
|
GhostType::Pinky => 0.95,
|
||||||
|
GhostType::Inky => 0.9,
|
||||||
|
GhostType::Clyde => 0.85,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A ghost entity that roams the game world.
|
||||||
|
///
|
||||||
|
/// Ghosts move through the game world using a graph-based navigation system
|
||||||
|
/// and display directional animated sprites. They randomly choose directions
|
||||||
|
/// at each intersection.
|
||||||
|
pub struct Ghost {
|
||||||
|
/// Handles movement through the game graph
|
||||||
|
pub traverser: Traverser,
|
||||||
|
/// The type of ghost (affects appearance and speed)
|
||||||
|
pub ghost_type: GhostType,
|
||||||
|
/// Manages directional animated textures for different movement states
|
||||||
|
texture: DirectionalAnimatedTexture,
|
||||||
|
/// Current movement speed
|
||||||
|
speed: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ghost {
|
||||||
|
/// Creates a new ghost instance at the specified starting node.
|
||||||
|
///
|
||||||
|
/// Sets up animated textures for all four directions with moving and stopped states.
|
||||||
|
/// The moving animation cycles through two sprite variants.
|
||||||
|
pub fn new(graph: &Graph, start_node: NodeId, ghost_type: GhostType, atlas: &SpriteAtlas) -> Self {
|
||||||
|
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")).unwrap(),
|
||||||
|
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "b")).unwrap(),
|
||||||
|
];
|
||||||
|
|
||||||
|
let stopped_tiles =
|
||||||
|
vec![
|
||||||
|
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"))
|
||||||
|
.unwrap(),
|
||||||
|
];
|
||||||
|
|
||||||
|
textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2).expect("Invalid frame duration"));
|
||||||
|
stopped_textures[direction.as_usize()] =
|
||||||
|
Some(AnimatedTexture::new(stopped_tiles, 0.1).expect("Invalid frame duration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
traverser: Traverser::new(graph, start_node, Direction::Left, &can_ghost_traverse),
|
||||||
|
ghost_type,
|
||||||
|
texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
|
||||||
|
speed: ghost_type.base_speed(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
fn choose_random_direction(&mut self, graph: &Graph) {
|
||||||
|
let current_node = self.traverser.position.from_node_id();
|
||||||
|
let intersection = &graph.adjacency_list[current_node];
|
||||||
|
|
||||||
|
// Collect all available directions
|
||||||
|
let mut available_directions = SmallVec::<[_; 4]>::new();
|
||||||
|
for direction in Direction::DIRECTIONS {
|
||||||
|
if let Some(edge) = intersection.get(direction) {
|
||||||
|
if can_ghost_traverse(edge) {
|
||||||
|
available_directions.push(direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Ghost {} at node {}: available directions: {:?}, current direction: {:?}",
|
||||||
|
self.ghost_type.as_str(),
|
||||||
|
current_node,
|
||||||
|
available_directions,
|
||||||
|
self.traverser.direction
|
||||||
|
);
|
||||||
|
|
||||||
|
// Choose a random direction (avoid reversing unless necessary)
|
||||||
|
if !available_directions.is_empty() {
|
||||||
|
let mut rng = SmallRng::from_os_rng();
|
||||||
|
|
||||||
|
// Filter out the opposite direction if possible, but allow it if we have limited options
|
||||||
|
let opposite = self.traverser.direction.opposite();
|
||||||
|
let filtered_directions: Vec<_> = available_directions
|
||||||
|
.iter()
|
||||||
|
.filter(|&&dir| dir != opposite || available_directions.len() <= 2)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Ghost {}: filtered directions: {:?}, opposite: {:?}",
|
||||||
|
self.ghost_type.as_str(),
|
||||||
|
filtered_directions,
|
||||||
|
opposite
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(&random_direction) = filtered_directions.choose(&mut rng) {
|
||||||
|
self.traverser.set_next_direction(*random_direction);
|
||||||
|
debug!("Ghost {} chose direction: {:?}", self.ghost_type.as_str(), random_direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod direction;
|
pub mod direction;
|
||||||
|
pub mod ghost;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
pub mod pacman;
|
pub mod pacman;
|
||||||
|
|||||||
67
src/game.rs
67
src/game.rs
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use glam::UVec2;
|
use glam::UVec2;
|
||||||
|
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
||||||
use sdl2::{
|
use sdl2::{
|
||||||
image::LoadTexture,
|
image::LoadTexture,
|
||||||
keyboard::Keycode,
|
keyboard::Keycode,
|
||||||
@@ -14,7 +15,10 @@ use crate::{
|
|||||||
asset::{get_asset_bytes, Asset},
|
asset::{get_asset_bytes, Asset},
|
||||||
audio::Audio,
|
audio::Audio,
|
||||||
constants::RAW_BOARD,
|
constants::RAW_BOARD,
|
||||||
entity::pacman::Pacman,
|
entity::{
|
||||||
|
ghost::{Ghost, GhostType},
|
||||||
|
pacman::Pacman,
|
||||||
|
},
|
||||||
map::Map,
|
map::Map,
|
||||||
texture::{
|
texture::{
|
||||||
sprite::{self, AtlasMapper, AtlasTile, SpriteAtlas},
|
sprite::{self, AtlasMapper, AtlasTile, SpriteAtlas},
|
||||||
@@ -30,6 +34,7 @@ pub struct Game {
|
|||||||
pub score: u32,
|
pub score: u32,
|
||||||
pub map: Map,
|
pub map: Map,
|
||||||
pub pacman: Pacman,
|
pub pacman: Pacman,
|
||||||
|
pub ghosts: Vec<Ghost>,
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
|
|
||||||
// Rendering resources
|
// Rendering resources
|
||||||
@@ -73,10 +78,23 @@ impl Game {
|
|||||||
let audio = Audio::new();
|
let audio = Audio::new();
|
||||||
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas);
|
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas);
|
||||||
|
|
||||||
|
// Create ghosts at random positions
|
||||||
|
let mut ghosts = Vec::new();
|
||||||
|
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
||||||
|
let mut rng = SmallRng::from_os_rng();
|
||||||
|
|
||||||
|
for &ghost_type in &ghost_types {
|
||||||
|
// Find a random node for the ghost to start at
|
||||||
|
let random_node = rng.random_range(0..map.graph.node_count());
|
||||||
|
let ghost = Ghost::new(&map.graph, random_node, ghost_type, &atlas);
|
||||||
|
ghosts.push(ghost);
|
||||||
|
}
|
||||||
|
|
||||||
Game {
|
Game {
|
||||||
score: 0,
|
score: 0,
|
||||||
map,
|
map,
|
||||||
pacman,
|
pacman,
|
||||||
|
ghosts,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
map_texture,
|
map_texture,
|
||||||
text_texture,
|
text_texture,
|
||||||
@@ -91,10 +109,41 @@ impl Game {
|
|||||||
if keycode == Keycode::M {
|
if keycode == Keycode::M {
|
||||||
self.audio.set_mute(!self.audio.is_muted());
|
self.audio.set_mute(!self.audio.is_muted());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if keycode == Keycode::R {
|
||||||
|
self.reset_game_state();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the game state, randomizing ghost positions and resetting Pac-Man
|
||||||
|
fn reset_game_state(&mut self) {
|
||||||
|
// Reset Pac-Man to starting position
|
||||||
|
let pacman_start_pos = self.map.find_starting_position(0).unwrap();
|
||||||
|
let pacman_start_node = *self
|
||||||
|
.map
|
||||||
|
.grid_to_node
|
||||||
|
.get(&glam::IVec2::new(pacman_start_pos.x as i32, pacman_start_pos.y as i32))
|
||||||
|
.expect("Pac-Man starting position not found in graph");
|
||||||
|
|
||||||
|
self.pacman = Pacman::new(&self.map.graph, pacman_start_node, &self.atlas);
|
||||||
|
|
||||||
|
// Randomize ghost positions
|
||||||
|
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
||||||
|
let mut rng = SmallRng::from_os_rng();
|
||||||
|
|
||||||
|
for (i, ghost) in self.ghosts.iter_mut().enumerate() {
|
||||||
|
let random_node = rng.random_range(0..self.map.graph.node_count());
|
||||||
|
*ghost = Ghost::new(&self.map.graph, random_node, ghost_types[i], &self.atlas);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick(&mut self, dt: f32) {
|
pub fn tick(&mut self, dt: f32) {
|
||||||
self.pacman.tick(dt, &self.map.graph);
|
self.pacman.tick(dt, &self.map.graph);
|
||||||
|
|
||||||
|
// Update all ghosts
|
||||||
|
for ghost in &mut self.ghosts {
|
||||||
|
ghost.tick(dt, &self.map.graph);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> Result<()> {
|
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> Result<()> {
|
||||||
@@ -102,16 +151,28 @@ impl Game {
|
|||||||
canvas.set_draw_color(Color::BLACK);
|
canvas.set_draw_color(Color::BLACK);
|
||||||
canvas.clear();
|
canvas.clear();
|
||||||
self.map.render(canvas, &mut self.atlas, &mut self.map_texture);
|
self.map.render(canvas, &mut self.atlas, &mut self.map_texture);
|
||||||
|
|
||||||
|
// Render all ghosts
|
||||||
|
for ghost in &self.ghosts {
|
||||||
|
ghost.render(canvas, &mut self.atlas, &self.map.graph);
|
||||||
|
}
|
||||||
|
|
||||||
self.pacman.render(canvas, &mut self.atlas, &self.map.graph);
|
self.pacman.render(canvas, &mut self.atlas, &self.map.graph);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn present_backbuffer<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &Texture) -> Result<()> {
|
pub fn present_backbuffer<T: RenderTarget>(
|
||||||
|
&mut self,
|
||||||
|
canvas: &mut Canvas<T>,
|
||||||
|
backbuffer: &Texture,
|
||||||
|
cursor_pos: glam::Vec2,
|
||||||
|
) -> Result<()> {
|
||||||
canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?;
|
canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?;
|
||||||
if self.debug_mode {
|
if self.debug_mode {
|
||||||
self.map.debug_render_nodes(canvas);
|
self.map
|
||||||
|
.debug_render_with_cursor(canvas, &mut self.text_texture, &mut self.atlas, cursor_pos);
|
||||||
}
|
}
|
||||||
self.draw_hud(canvas)?;
|
self.draw_hud(canvas)?;
|
||||||
canvas.present();
|
canvas.present();
|
||||||
|
|||||||
@@ -184,13 +184,18 @@ impl Map {
|
|||||||
MapRenderer::render_map(canvas, atlas, map_texture);
|
MapRenderer::render_map(canvas, atlas, map_texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a debug visualization of the navigation graph.
|
/// Renders a debug visualization with cursor-based highlighting.
|
||||||
///
|
///
|
||||||
/// This function is intended for development and debugging purposes. It draws the
|
/// This function provides interactive debugging by highlighting the nearest node
|
||||||
/// nodes and edges of the graph on top of the map, allowing for visual
|
/// to the cursor, showing its ID, and highlighting its connections.
|
||||||
/// inspection of the navigation paths.
|
pub fn debug_render_with_cursor<T: RenderTarget>(
|
||||||
pub fn debug_render_nodes<T: RenderTarget>(&self, canvas: &mut Canvas<T>) {
|
&self,
|
||||||
MapRenderer::debug_render_nodes(&self.graph, canvas);
|
canvas: &mut Canvas<T>,
|
||||||
|
text_renderer: &mut crate::texture::text::TextTexture,
|
||||||
|
atlas: &mut SpriteAtlas,
|
||||||
|
cursor_pos: glam::Vec2,
|
||||||
|
) {
|
||||||
|
MapRenderer::debug_render_with_cursor(&self.graph, canvas, text_renderer, atlas, cursor_pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds the house structure in the graph.
|
/// Builds the house structure in the graph.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! Map rendering functionality.
|
//! Map rendering functionality.
|
||||||
|
|
||||||
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
||||||
|
use crate::texture::text::TextTexture;
|
||||||
|
use glam::Vec2;
|
||||||
use sdl2::pixels::Color;
|
use sdl2::pixels::Color;
|
||||||
use sdl2::rect::{Point, Rect};
|
use sdl2::rect::{Point, Rect};
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
@@ -23,45 +25,93 @@ impl MapRenderer {
|
|||||||
let _ = map_texture.render(canvas, atlas, dest);
|
let _ = map_texture.render(canvas, atlas, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a debug visualization of the navigation graph.
|
/// Renders a debug visualization with cursor-based highlighting.
|
||||||
///
|
///
|
||||||
/// This function is intended for development and debugging purposes. It draws the
|
/// This function provides interactive debugging by highlighting the nearest node
|
||||||
/// nodes and edges of the graph on top of the map, allowing for visual
|
/// to the cursor, showing its ID, and highlighting its connections.
|
||||||
/// inspection of the navigation paths.
|
pub fn debug_render_with_cursor<T: RenderTarget>(
|
||||||
pub fn debug_render_nodes<T: RenderTarget>(graph: &crate::entity::graph::Graph, canvas: &mut Canvas<T>) {
|
graph: &crate::entity::graph::Graph,
|
||||||
|
canvas: &mut Canvas<T>,
|
||||||
|
text_renderer: &mut TextTexture,
|
||||||
|
atlas: &mut SpriteAtlas,
|
||||||
|
cursor_pos: Vec2,
|
||||||
|
) {
|
||||||
|
// Find the nearest node to the cursor
|
||||||
|
let nearest_node = Self::find_nearest_node(graph, cursor_pos);
|
||||||
|
|
||||||
|
// Draw all connections in blue
|
||||||
|
canvas.set_draw_color(Color::RGB(0, 0, 128)); // Dark blue for regular connections
|
||||||
for i in 0..graph.node_count() {
|
for i in 0..graph.node_count() {
|
||||||
let node = graph.get_node(i).unwrap();
|
let node = graph.get_node(i).unwrap();
|
||||||
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
|
||||||
// Draw connections
|
|
||||||
canvas.set_draw_color(Color::BLUE);
|
|
||||||
|
|
||||||
for edge in graph.adjacency_list[i].edges() {
|
for edge in graph.adjacency_list[i].edges() {
|
||||||
let end_pos = graph.get_node(edge.target).unwrap().position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
let end_pos = graph.get_node(edge.target).unwrap().position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
canvas
|
canvas
|
||||||
.draw_line((pos.x as i32, pos.y as i32), (end_pos.x as i32, end_pos.y as i32))
|
.draw_line((pos.x as i32, pos.y as i32), (end_pos.x as i32, end_pos.y as i32))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw all nodes in green
|
||||||
|
canvas.set_draw_color(Color::RGB(0, 128, 0)); // Dark green for regular nodes
|
||||||
|
for i in 0..graph.node_count() {
|
||||||
|
let node = graph.get_node(i).unwrap();
|
||||||
|
let pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
|
||||||
// Draw node
|
|
||||||
// let color = if pacman.position.from_node_idx() == i.into() {
|
|
||||||
// Color::GREEN
|
|
||||||
// } else if let Some(to_idx) = pacman.position.to_node_idx() {
|
|
||||||
// if to_idx == i.into() {
|
|
||||||
// Color::CYAN
|
|
||||||
// } else {
|
|
||||||
// Color::RED
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// Color::RED
|
|
||||||
// };
|
|
||||||
canvas.set_draw_color(Color::GREEN);
|
|
||||||
canvas
|
canvas
|
||||||
.fill_rect(Rect::new(0, 0, 3, 3).centered_on(Point::new(pos.x as i32, pos.y as i32)))
|
.fill_rect(Rect::new(0, 0, 3, 3).centered_on(Point::new(pos.x as i32, pos.y as i32)))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Draw node index
|
|
||||||
// text.render(canvas, atlas, &i.to_string(), pos.as_uvec2()).unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highlight connections from the nearest node in bright blue
|
||||||
|
if let Some(nearest_id) = nearest_node {
|
||||||
|
let nearest_pos = graph.get_node(nearest_id).unwrap().position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
|
||||||
|
canvas.set_draw_color(Color::RGB(0, 255, 255)); // Bright cyan for highlighted connections
|
||||||
|
for edge in graph.adjacency_list[nearest_id].edges() {
|
||||||
|
let end_pos = graph.get_node(edge.target).unwrap().position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
canvas
|
||||||
|
.draw_line(
|
||||||
|
(nearest_pos.x as i32, nearest_pos.y as i32),
|
||||||
|
(end_pos.x as i32, end_pos.y as i32),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight the nearest node in bright green
|
||||||
|
canvas.set_draw_color(Color::RGB(0, 255, 0)); // Bright green for highlighted node
|
||||||
|
canvas
|
||||||
|
.fill_rect(Rect::new(0, 0, 5, 5).centered_on(Point::new(nearest_pos.x as i32, nearest_pos.y as i32)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Draw node ID text (small, offset to top right)
|
||||||
|
text_renderer.set_scale(0.5); // Small text
|
||||||
|
let id_text = format!("#{}", nearest_id);
|
||||||
|
let text_pos = glam::UVec2::new(
|
||||||
|
(nearest_pos.x + 4.0) as u32, // Offset to the right
|
||||||
|
(nearest_pos.y - 6.0) as u32, // Offset to the top
|
||||||
|
);
|
||||||
|
let _ = text_renderer.render(canvas, atlas, &id_text, text_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the nearest node to the given cursor position.
|
||||||
|
pub fn find_nearest_node(graph: &crate::entity::graph::Graph, cursor_pos: Vec2) -> Option<usize> {
|
||||||
|
let mut nearest_id = None;
|
||||||
|
let mut nearest_distance = f32::INFINITY;
|
||||||
|
|
||||||
|
for i in 0..graph.node_count() {
|
||||||
|
let node = graph.get_node(i).unwrap();
|
||||||
|
let node_pos = node.position + crate::constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
let distance = cursor_pos.distance(node_pos);
|
||||||
|
|
||||||
|
if distance < nearest_distance {
|
||||||
|
nearest_distance = distance;
|
||||||
|
nearest_id = Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nearest_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
tests/debug_rendering.rs
Normal file
34
tests/debug_rendering.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use glam::Vec2;
|
||||||
|
use pacman::entity::graph::{Graph, Node};
|
||||||
|
use pacman::map::render::MapRenderer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_find_nearest_node() {
|
||||||
|
let mut graph = Graph::new();
|
||||||
|
|
||||||
|
// Add some test nodes
|
||||||
|
let node1 = graph.add_node(Node {
|
||||||
|
position: Vec2::new(10.0, 10.0),
|
||||||
|
});
|
||||||
|
let node2 = graph.add_node(Node {
|
||||||
|
position: Vec2::new(50.0, 50.0),
|
||||||
|
});
|
||||||
|
let node3 = graph.add_node(Node {
|
||||||
|
position: Vec2::new(100.0, 100.0),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test cursor near node1
|
||||||
|
let cursor_pos = Vec2::new(12.0, 8.0);
|
||||||
|
let nearest = MapRenderer::find_nearest_node(&graph, cursor_pos);
|
||||||
|
assert_eq!(nearest, Some(node1));
|
||||||
|
|
||||||
|
// Test cursor near node2
|
||||||
|
let cursor_pos = Vec2::new(45.0, 55.0);
|
||||||
|
let nearest = MapRenderer::find_nearest_node(&graph, cursor_pos);
|
||||||
|
assert_eq!(nearest, Some(node2));
|
||||||
|
|
||||||
|
// Test cursor near node3
|
||||||
|
let cursor_pos = Vec2::new(98.0, 102.0);
|
||||||
|
let nearest = MapRenderer::find_nearest_node(&graph, cursor_pos);
|
||||||
|
assert_eq!(nearest, Some(node3));
|
||||||
|
}
|
||||||
48
tests/ghost.rs
Normal file
48
tests/ghost.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
use pacman::entity::ghost::{Ghost, GhostType};
|
||||||
|
use pacman::entity::graph::Graph;
|
||||||
|
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn create_test_atlas() -> SpriteAtlas {
|
||||||
|
let mut frames = HashMap::new();
|
||||||
|
let directions = ["up", "down", "left", "right"];
|
||||||
|
let ghost_types = ["blinky", "pinky", "inky", "clyde"];
|
||||||
|
|
||||||
|
for ghost_type in &ghost_types {
|
||||||
|
for (i, dir) in directions.iter().enumerate() {
|
||||||
|
frames.insert(
|
||||||
|
format!("ghost/{}/{}_{}.png", ghost_type, dir, "a"),
|
||||||
|
MapperFrame {
|
||||||
|
x: i as u16 * 16,
|
||||||
|
y: 0,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
frames.insert(
|
||||||
|
format!("ghost/{}/{}_{}.png", ghost_type, dir, "b"),
|
||||||
|
MapperFrame {
|
||||||
|
x: i as u16 * 16,
|
||||||
|
y: 16,
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mapper = AtlasMapper { frames };
|
||||||
|
let dummy_texture = unsafe { std::mem::zeroed() };
|
||||||
|
SpriteAtlas::new(dummy_texture, mapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ghost_creation() {
|
||||||
|
let graph = Graph::new();
|
||||||
|
let atlas = create_test_atlas();
|
||||||
|
|
||||||
|
let ghost = Ghost::new(&graph, 0, GhostType::Blinky, &atlas);
|
||||||
|
|
||||||
|
assert_eq!(ghost.ghost_type, GhostType::Blinky);
|
||||||
|
assert_eq!(ghost.traverser.position.from_node_id(), 0);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user