Compare commits

..

12 Commits

120 changed files with 3732 additions and 2314 deletions

771
Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,8 @@ serde_json = "1.0.142"
smallvec = "1.15.1" smallvec = "1.15.1"
strum = "0.27.2" strum = "0.27.2"
strum_macros = "0.27.2" strum_macros = "0.27.2"
phf = { version = "0.11", features = ["macros"] }
bevy_ecs = "0.16.1"
[profile.release] [profile.release]
lto = true lto = true
@@ -57,3 +59,8 @@ aarch64-apple-darwin = { triplet = "arm64-osx" }
[target.'cfg(target_os = "emscripten")'.dependencies] [target.'cfg(target_os = "emscripten")'.dependencies]
libc = "0.2.175" libc = "0.2.175"
[build-dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
phf = { version = "0.11", features = ["macros"] }

View File

@@ -1,6 +1,6 @@
# Pac-Man # Pac-Man
[![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![Code Coverage][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits] [![Tests Status][badge-test]][test] [![Build Status][badge-build]][build] [![If you're seeing this, Coveralls.io is broken again and it's not my fault.][badge-coverage]][coverage] [![Online Demo][badge-online-demo]][demo] [![Last Commit][badge-last-commit]][commits]
[badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg [badge-test]: https://github.com/Xevion/Pac-Man/actions/workflows/tests.yaml/badge.svg
[badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg [badge-build]: https://github.com/Xevion/Pac-Man/actions/workflows/build.yaml/badge.svg

View File

File diff suppressed because it is too large Load Diff

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

50
build.rs Normal file
View File

@@ -0,0 +1,50 @@
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct AtlasMapper {
frames: HashMap<String, MapperFrame>,
}
#[derive(Copy, Clone, Debug, Deserialize)]
struct MapperFrame {
x: u16,
y: u16,
width: u16,
height: u16,
}
fn main() {
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("atlas_data.rs");
let mut file = BufWriter::new(File::create(&path).unwrap());
let atlas_json = include_str!("./assets/game/atlas.json");
let atlas_mapper: AtlasMapper = serde_json::from_str(atlas_json).unwrap();
writeln!(&mut file, "use phf::phf_map;").unwrap();
writeln!(&mut file, "use crate::texture::sprite::MapperFrame;").unwrap();
writeln!(
&mut file,
"pub static ATLAS_FRAMES: phf::Map<&'static str, MapperFrame> = phf_map! {{"
)
.unwrap();
for (name, frame) in atlas_mapper.frames {
writeln!(
&mut file,
" \"{}\" => MapperFrame {{ x: {}, y: {}, width: {}, height: {} }},",
name, frame.x, frame.y, frame.width, frame.height
)
.unwrap();
}
writeln!(&mut file, "}};").unwrap();
println!("cargo:rerun-if-changed=assets/game/atlas.json");
}

View File

@@ -1,13 +1,11 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use glam::Vec2; use glam::Vec2;
use sdl2::event::{Event, WindowEvent};
use sdl2::keyboard::Keycode;
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator}; use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
use sdl2::ttf::Sdl2TtfContext; use sdl2::ttf::Sdl2TtfContext;
use sdl2::video::{Window, WindowContext}; use sdl2::video::{Window, WindowContext};
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem}; use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
use tracing::{error, event}; use tracing::{error, warn};
use crate::error::{GameError, GameResult}; use crate::error::{GameError, GameResult};
@@ -16,12 +14,9 @@ use crate::game::Game;
use crate::platform::get_platform; use crate::platform::get_platform;
pub struct App { pub struct App {
game: Game, pub game: Game,
canvas: Canvas<Window>,
event_pump: &'static mut EventPump,
backbuffer: Texture<'static>,
paused: bool,
last_tick: Instant, last_tick: Instant,
focused: bool,
cursor_pos: Vec2, cursor_pos: Vec2,
} }
@@ -51,33 +46,33 @@ impl App {
.build() .build()
.map_err(|e| GameError::Sdl(e.to_string()))?; .map_err(|e| GameError::Sdl(e.to_string()))?;
let mut canvas = window.into_canvas().build().map_err(|e| GameError::Sdl(e.to_string()))?; let mut canvas = Box::leak(Box::new(
window
.into_canvas()
.accelerated()
.present_vsync()
.build()
.map_err(|e| GameError::Sdl(e.to_string()))?,
));
canvas canvas
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y) .set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?; .map_err(|e| GameError::Sdl(e.to_string()))?;
let texture_creator: &'static TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator())); let texture_creator: &'static mut TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
let mut game = Game::new(texture_creator)?; let game = Game::new(canvas, texture_creator, event_pump)?;
// game.audio.set_mute(cfg!(debug_assertions)); // game.audio.set_mute(cfg!(debug_assertions));
let mut backbuffer = texture_creator
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
backbuffer.set_scale_mode(ScaleMode::Nearest);
// Initial draw // Initial draw
game.draw(&mut canvas, &mut backbuffer) // game.draw(&mut canvas, &mut backbuffer)
.map_err(|e| GameError::Sdl(e.to_string()))?; // .map_err(|e| GameError::Sdl(e.to_string()))?;
game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO) // game.present_backbuffer(&mut canvas, &backbuffer, glam::Vec2::ZERO)
.map_err(|e| GameError::Sdl(e.to_string()))?; // .map_err(|e| GameError::Sdl(e.to_string()))?;
Ok(Self { Ok(App {
game, game,
canvas, focused: true,
event_pump,
backbuffer,
paused: false,
last_tick: Instant::now(), last_tick: Instant::now(),
cursor_pos: Vec2::ZERO, cursor_pos: Vec2::ZERO,
}) })
@@ -87,78 +82,51 @@ impl App {
{ {
let start = Instant::now(); let start = Instant::now();
for event in self.event_pump.poll_iter() { // for event in self
match event { // .game
Event::Window { win_event, .. } => match win_event { // .world
WindowEvent::Hidden => { // .get_non_send_resource_mut::<&'static mut EventPump>()
event!(tracing::Level::DEBUG, "Window hidden"); // .unwrap()
} // .poll_iter()
WindowEvent::Shown => { // {
event!(tracing::Level::DEBUG, "Window shown"); // match event {
} // Event::Window { win_event, .. } => match win_event {
_ => {} // WindowEvent::FocusGained => {
}, // self.focused = true;
// It doesn't really make sense to have this available in the browser // }
#[cfg(not(target_os = "emscripten"))] // WindowEvent::FocusLost => {
Event::Quit { .. } // self.focused = false;
| Event::KeyDown { // }
keycode: Some(Keycode::Escape) | Some(Keycode::Q), // _ => {}
.. // },
} => { // Event::MouseMotion { x, y, .. } => {
event!(tracing::Level::INFO, "Exit requested. Exiting..."); // // Convert window coordinates to logical coordinates
return false; // self.cursor_pos = Vec2::new(x as f32, y as f32);
} // }
Event::KeyDown { // _ => {}
keycode: Some(Keycode::P), // }
.. // }
} => {
self.paused = !self.paused;
event!(tracing::Level::INFO, "{}", if self.paused { "Paused" } else { "Unpaused" });
}
Event::KeyDown {
keycode: Some(Keycode::Space),
..
} => {
self.game.toggle_debug_mode();
}
Event::KeyDown { keycode: Some(key), .. } => {
self.game.keyboard_event(key);
}
Event::MouseMotion { x, y, .. } => {
// Convert window coordinates to logical coordinates
self.cursor_pos = Vec2::new(x as f32, y as f32);
}
_ => {}
}
}
let dt = self.last_tick.elapsed().as_secs_f32(); let dt = self.last_tick.elapsed().as_secs_f32();
self.last_tick = Instant::now(); self.last_tick = Instant::now();
if !self.paused { let exit = self.game.tick(dt);
self.game.tick(dt);
if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) { if exit {
error!("Failed to draw game: {}", e); return false;
}
if let Err(e) = self
.game
.present_backbuffer(&mut self.canvas, &self.backbuffer, self.cursor_pos)
{
error!("Failed to present backbuffer: {}", e);
}
} }
// if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
// error!("Failed to draw game: {}", e);
// }
if start.elapsed() < LOOP_TIME { if start.elapsed() < LOOP_TIME {
let time = LOOP_TIME.saturating_sub(start.elapsed()); let time = LOOP_TIME.saturating_sub(start.elapsed());
if time != Duration::ZERO { if time != Duration::ZERO {
get_platform().sleep(time); get_platform().sleep(time, self.focused);
} }
} else { } else {
event!( warn!("Game loop behind schedule by: {:?}", start.elapsed() - LOOP_TIME);
tracing::Level::WARN,
"Game loop behind schedule by: {:?}",
start.elapsed() - LOOP_TIME
);
} }
true true

View File

@@ -12,8 +12,6 @@ pub enum Asset {
Wav3, Wav3,
Wav4, Wav4,
Atlas, Atlas,
AtlasJson,
// Add more as needed
} }
impl Asset { impl Asset {
@@ -26,7 +24,6 @@ impl Asset {
Wav3 => "sound/waka/3.ogg", Wav3 => "sound/waka/3.ogg",
Wav4 => "sound/waka/4.ogg", Wav4 => "sound/waka/4.ogg",
Atlas => "atlas.png", Atlas => "atlas.png",
AtlasJson => "atlas.json",
} }
} }
} }
@@ -36,6 +33,7 @@ mod imp {
use crate::error::AssetError; use crate::error::AssetError;
use crate::platform::get_platform; use crate::platform::get_platform;
/// Returns the raw bytes of the given asset.
pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> { pub fn get_asset_bytes(asset: Asset) -> Result<Cow<'static, [u8]>, AssetError> {
get_platform().get_asset_bytes(asset) get_platform().get_asset_bytes(asset)
} }

View File

@@ -18,8 +18,6 @@ pub const SCALE: f32 = 2.6;
pub const BOARD_CELL_OFFSET: UVec2 = UVec2::new(0, 3); pub const BOARD_CELL_OFFSET: UVec2 = UVec2::new(0, 3);
/// The offset of the game board from the top-left corner of the window, in pixels. /// The offset of the game board from the top-left corner of the window, in pixels.
pub const BOARD_PIXEL_OFFSET: UVec2 = UVec2::new(BOARD_CELL_OFFSET.x * CELL_SIZE, BOARD_CELL_OFFSET.y * CELL_SIZE); pub const BOARD_PIXEL_OFFSET: UVec2 = UVec2::new(BOARD_CELL_OFFSET.x * CELL_SIZE, BOARD_CELL_OFFSET.y * CELL_SIZE);
/// The size of the game board, in pixels.
pub const BOARD_PIXEL_SIZE: UVec2 = UVec2::new(BOARD_CELL_SIZE.x * CELL_SIZE, BOARD_CELL_SIZE.y * CELL_SIZE);
/// The size of the canvas, in pixels. /// The size of the canvas, in pixels.
pub const CANVAS_SIZE: UVec2 = UVec2::new( pub const CANVAS_SIZE: UVec2 = UVec2::new(
(BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE, (BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE,

172
src/ecs/interact.rs Normal file
View File

@@ -0,0 +1,172 @@
use bevy_ecs::{
event::{EventReader, EventWriter},
query::With,
system::{Query, Res, ResMut},
};
use crate::{
ecs::{DeltaTime, GlobalState, PlayerControlled, Position, Velocity},
error::{EntityError, GameError},
game::events::GameEvent,
input::commands::GameCommand,
map::builder::Map,
};
pub fn movement_system(
map: Res<Map>,
delta_time: Res<DeltaTime>,
mut entities: Query<(&PlayerControlled, &mut Velocity, &mut Position)>,
mut errors: EventWriter<GameError>,
) {
for (player, mut velocity, mut position) in entities.iter_mut() {
let distance = velocity.speed.unwrap_or(0.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 edge.permissions.can_traverse(edge) {
// // Start moving in that direction
*position = Position::BetweenNodes {
from: node_id,
to: edge.target,
traversed: distance,
};
velocity.direction = next_direction;
// } else {
// return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(
// format!(
// "Cannot traverse edge from {} to {} in direction {:?}",
// node_id, edge.target, next_direction
// ),
// )));
// }
} else {
errors.write(
EntityError::InvalidMovement(format!(
"No edge found in direction {:?} from node {}",
next_direction, node_id
))
.into(),
);
}
velocity.next_direction = None; // Consume the buffered direction regardless of whether we started moving with it
}
}
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 edge.permissions.can_traverse(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 edge.permissions.can_traverse(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;
}
}
}
}
}
}
}
// Handles
pub fn interact_system(
mut events: EventReader<GameEvent>,
mut state: ResMut<GlobalState>,
mut players: Query<(&PlayerControlled, &mut Velocity)>,
mut errors: EventWriter<GameError>,
) {
// Get the player's velocity (handling to ensure there is only one player)
let mut velocity = match players.single_mut() {
Ok((_, velocity)) => velocity,
Err(e) => {
errors.write(GameError::InvalidState(format!("Player not found: {}", e)).into());
return;
}
};
// Handle events
for event in events.read() {
match event {
GameEvent::Command(command) => match command {
GameCommand::MovePlayer(direction) => {
velocity.direction = *direction;
}
GameCommand::Exit => {
state.exit = true;
}
_ => {}
},
}
}
}

150
src/ecs/mod.rs Normal file
View File

@@ -0,0 +1,150 @@
//! The Entity-Component-System (ECS) module.
//!
//! 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<AnimatedTexture>; 4],
pub stopped_textures: [Option<AnimatedTexture>; 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<Vec2> {
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<NodeId> {
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: Option<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 interact;
pub mod render;

95
src/ecs/render.rs Normal file
View File

@@ -0,0 +1,95 @@
use crate::ecs::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity};
use crate::error::{EntityError, GameError, TextureError};
use crate::map::builder::Map;
use crate::texture::sprite::SpriteAtlas;
use bevy_ecs::entity::Entity;
use bevy_ecs::event::EventWriter;
use bevy_ecs::system::{NonSendMut, Query, Res};
use sdl2::render::{Canvas, Texture};
use sdl2::video::Window;
/// Updates the directional animated texture of an entity.
pub fn directional_render_system(
dt: Res<DeltaTime>,
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable)>,
mut errors: EventWriter<GameError>,
) {
for (velocity, mut texture, mut renderable) in renderables.iter_mut() {
let texture = if velocity.speed.is_none() {
texture.stopped_textures[velocity.direction.as_usize()].as_mut()
} else {
texture.textures[velocity.direction.as_usize()].as_mut()
};
if let Some(texture) = texture {
texture.tick(dt.0);
renderable.sprite = *texture.current_tile();
} else {
errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into());
continue;
}
}
}
pub struct MapTextureResource(pub Texture<'static>);
pub struct BackbufferResource(pub Texture<'static>);
pub fn render_system(
mut canvas: NonSendMut<&mut Canvas<Window>>,
map_texture: NonSendMut<MapTextureResource>,
mut backbuffer: NonSendMut<BackbufferResource>,
mut atlas: NonSendMut<SpriteAtlas>,
map: Res<Map>,
mut renderables: Query<(Entity, &mut Renderable, &Position)>,
mut errors: EventWriter<GameError>,
) {
// Clear the main canvas first
canvas.set_draw_color(sdl2::pixels::Color::BLACK);
canvas.clear();
// Render to backbuffer
canvas
.with_texture_canvas(&mut backbuffer.0, |backbuffer_canvas| {
// Clear the backbuffer
backbuffer_canvas.set_draw_color(sdl2::pixels::Color::BLACK);
backbuffer_canvas.clear();
// Copy the pre-rendered map texture to the backbuffer
backbuffer_canvas
.copy(&map_texture.0, None, None)
.err()
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
// Render all entities to the backbuffer
for (_, mut renderable, position) in renderables.iter_mut() {
let pos = position.get_pixel_pos(&map.graph);
match pos {
Ok(pos) => {
let dest = crate::helpers::centered_with_size(
glam::IVec2::new(pos.x as i32, pos.y as i32),
glam::UVec2::new(renderable.sprite.size.x as u32, renderable.sprite.size.y as u32),
);
renderable
.sprite
.render(backbuffer_canvas, &mut atlas, dest)
.err()
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
}
Err(e) => {
errors.write(e.into());
}
}
}
})
.err()
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
// Copy backbuffer to main canvas and present
canvas
.copy(&backbuffer.0, None, None)
.err()
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
canvas.present();
}

View File

@@ -1,128 +1,128 @@
use smallvec::SmallVec; // use smallvec::SmallVec;
use std::collections::HashMap; // use std::collections::HashMap;
use crate::entity::{graph::NodeId, traversal::Position}; // use crate::entity::{graph::NodeId, traversal::Position};
/// Trait for entities that can participate in collision detection. // /// Trait for entities that can participate in collision detection.
pub trait Collidable { // pub trait Collidable {
/// Returns the current position of this entity. // /// Returns the current position of this entity.
fn position(&self) -> Position; // fn position(&self) -> Position;
/// Checks if this entity is colliding with another entity. // /// Checks if this entity is colliding with another entity.
#[allow(dead_code)] // #[allow(dead_code)]
fn is_colliding_with(&self, other: &dyn Collidable) -> bool { // fn is_colliding_with(&self, other: &dyn Collidable) -> bool {
positions_overlap(&self.position(), &other.position()) // positions_overlap(&self.position(), &other.position())
} // }
} // }
/// System for tracking entities by their positions for efficient collision detection. // /// System for tracking entities by their positions for efficient collision detection.
#[derive(Default)] // #[derive(Default)]
pub struct CollisionSystem { // pub struct CollisionSystem {
/// Maps node IDs to lists of entity IDs that are at that node // /// Maps node IDs to lists of entity IDs that are at that node
node_entities: HashMap<NodeId, Vec<EntityId>>, // node_entities: HashMap<NodeId, Vec<EntityId>>,
/// Maps entity IDs to their current positions // /// Maps entity IDs to their current positions
entity_positions: HashMap<EntityId, Position>, // entity_positions: HashMap<EntityId, Position>,
/// Next available entity ID // /// Next available entity ID
next_id: EntityId, // next_id: EntityId,
} // }
/// Unique identifier for an entity in the collision system // /// Unique identifier for an entity in the collision system
pub type EntityId = u32; // pub type EntityId = u32;
impl CollisionSystem { // impl CollisionSystem {
/// Registers an entity with the collision system and returns its ID // /// Registers an entity with the collision system and returns its ID
pub fn register_entity(&mut self, position: Position) -> EntityId { // pub fn register_entity(&mut self, position: Position) -> EntityId {
let id = self.next_id; // let id = self.next_id;
self.next_id += 1; // self.next_id += 1;
self.entity_positions.insert(id, position); // self.entity_positions.insert(id, position);
self.update_node_entities(id, position); // self.update_node_entities(id, position);
id // id
} // }
/// Updates an entity's position // /// Updates an entity's position
pub fn update_position(&mut self, entity_id: EntityId, new_position: Position) { // pub fn update_position(&mut self, entity_id: EntityId, new_position: Position) {
if let Some(old_position) = self.entity_positions.get(&entity_id) { // if let Some(old_position) = self.entity_positions.get(&entity_id) {
// Remove from old nodes // // Remove from old nodes
self.remove_from_nodes(entity_id, *old_position); // self.remove_from_nodes(entity_id, *old_position);
} // }
// Update position and add to new nodes // // Update position and add to new nodes
self.entity_positions.insert(entity_id, new_position); // self.entity_positions.insert(entity_id, new_position);
self.update_node_entities(entity_id, new_position); // self.update_node_entities(entity_id, new_position);
} // }
/// Removes an entity from the collision system // /// Removes an entity from the collision system
#[allow(dead_code)] // #[allow(dead_code)]
pub fn remove_entity(&mut self, entity_id: EntityId) { // pub fn remove_entity(&mut self, entity_id: EntityId) {
if let Some(position) = self.entity_positions.remove(&entity_id) { // if let Some(position) = self.entity_positions.remove(&entity_id) {
self.remove_from_nodes(entity_id, position); // self.remove_from_nodes(entity_id, position);
} // }
} // }
/// Gets all entity IDs at a specific node // /// Gets all entity IDs at a specific node
pub fn entities_at_node(&self, node: NodeId) -> &[EntityId] { // pub fn entities_at_node(&self, node: NodeId) -> &[EntityId] {
self.node_entities.get(&node).map(|v| v.as_slice()).unwrap_or(&[]) // self.node_entities.get(&node).map(|v| v.as_slice()).unwrap_or(&[])
} // }
/// Gets all entity IDs that could collide with an entity at the given position // /// Gets all entity IDs that could collide with an entity at the given position
pub fn potential_collisions(&self, position: &Position) -> Vec<EntityId> { // pub fn potential_collisions(&self, position: &Position) -> Vec<EntityId> {
let mut collisions = Vec::new(); // let mut collisions = Vec::new();
let nodes = get_nodes(position); // let nodes = get_nodes(position);
for node in nodes { // for node in nodes {
collisions.extend(self.entities_at_node(node)); // collisions.extend(self.entities_at_node(node));
} // }
// Remove duplicates // // Remove duplicates
collisions.sort_unstable(); // collisions.sort_unstable();
collisions.dedup(); // collisions.dedup();
collisions // collisions
} // }
/// Updates the node_entities map when an entity's position changes // /// Updates the node_entities map when an entity's position changes
fn update_node_entities(&mut self, entity_id: EntityId, position: Position) { // fn update_node_entities(&mut self, entity_id: EntityId, position: Position) {
let nodes = get_nodes(&position); // let nodes = get_nodes(&position);
for node in nodes { // for node in nodes {
self.node_entities.entry(node).or_default().push(entity_id); // self.node_entities.entry(node).or_default().push(entity_id);
} // }
} // }
/// Removes an entity from all nodes it was previously at // /// Removes an entity from all nodes it was previously at
fn remove_from_nodes(&mut self, entity_id: EntityId, position: Position) { // fn remove_from_nodes(&mut self, entity_id: EntityId, position: Position) {
let nodes = get_nodes(&position); // let nodes = get_nodes(&position);
for node in nodes { // for node in nodes {
if let Some(entities) = self.node_entities.get_mut(&node) { // if let Some(entities) = self.node_entities.get_mut(&node) {
entities.retain(|&id| id != entity_id); // entities.retain(|&id| id != entity_id);
if entities.is_empty() { // if entities.is_empty() {
self.node_entities.remove(&node); // self.node_entities.remove(&node);
} // }
} // }
} // }
} // }
} // }
/// Checks if two positions overlap (entities are at the same location). // /// Checks if two positions overlap (entities are at the same location).
fn positions_overlap(a: &Position, b: &Position) -> bool { // fn positions_overlap(a: &Position, b: &Position) -> bool {
let a_nodes = get_nodes(a); // let a_nodes = get_nodes(a);
let b_nodes = get_nodes(b); // let b_nodes = get_nodes(b);
// Check if any nodes overlap // // Check if any nodes overlap
a_nodes.iter().any(|a_node| b_nodes.contains(a_node)) // a_nodes.iter().any(|a_node| b_nodes.contains(a_node))
// TODO: More complex overlap detection, the above is a simple check, but it could become an early filter for more precise calculations later // // TODO: More complex overlap detection, the above is a simple check, but it could become an early filter for more precise calculations later
} // }
/// Gets all nodes that an entity is currently at or between. // /// Gets all nodes that an entity is currently at or between.
fn get_nodes(pos: &Position) -> SmallVec<[NodeId; 2]> { // fn get_nodes(pos: &Position) -> SmallVec<[NodeId; 2]> {
let mut nodes = SmallVec::new(); // let mut nodes = SmallVec::new();
match pos { // match pos {
Position::AtNode(node) => nodes.push(*node), // Position::AtNode(node) => nodes.push(*node),
Position::BetweenNodes { from, to, .. } => { // Position::BetweenNodes { from, to, .. } => {
nodes.push(*from); // nodes.push(*from);
nodes.push(*to); // nodes.push(*to);
} // }
} // }
nodes // nodes
} // }

View File

@@ -1,11 +1,13 @@
use glam::IVec2; use glam::IVec2;
/// The four cardinal directions. /// The four cardinal directions.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(usize)]
pub enum Direction { pub enum Direction {
Up, Up,
Down, Down,
Left, Left,
#[default]
Right, Right,
} }

View File

@@ -1,254 +1,254 @@
//! Ghost entity implementation. // //! Ghost entity implementation.
//! // //!
//! This module contains the ghost character logic, including movement, // //! This module contains the ghost character logic, including movement,
//! 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 pathfinding::prelude::dijkstra; // use pathfinding::prelude::dijkstra;
use rand::prelude::*; // use rand::prelude::*;
use smallvec::SmallVec; // use smallvec::SmallVec;
use tracing::error; // use tracing::error;
use crate::entity::{ // use crate::entity::{
collision::Collidable, // collision::Collidable,
direction::Direction, // direction::Direction,
graph::{Edge, EdgePermissions, Graph, NodeId}, // graph::{Edge, EdgePermissions, Graph, NodeId},
r#trait::Entity, // r#trait::Entity,
traversal::Traverser, // 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 crate::error::{EntityError, GameError, GameResult, TextureError}; // use crate::error::{EntityError, GameError, GameResult, TextureError};
/// Determines if a ghost can traverse a given edge. // /// Determines if a ghost can traverse a given edge.
/// // ///
/// Ghosts can move through edges that allow all entities or ghost-only edges. // /// Ghosts can move through edges that allow all entities or ghost-only edges.
fn can_ghost_traverse(edge: Edge) -> bool { // fn can_ghost_traverse(edge: Edge) -> bool {
matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly) // matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly)
} // }
/// The four classic ghost types. // /// The four classic ghost types.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] // #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GhostType { // pub enum GhostType {
Blinky, // Blinky,
Pinky, // Pinky,
Inky, // Inky,
Clyde, // Clyde,
} // }
impl GhostType { // impl GhostType {
/// Returns the ghost type name for atlas lookups. // /// Returns the ghost type name for atlas lookups.
pub fn as_str(self) -> &'static str { // pub fn as_str(self) -> &'static str {
match self { // match self {
GhostType::Blinky => "blinky", // GhostType::Blinky => "blinky",
GhostType::Pinky => "pinky", // GhostType::Pinky => "pinky",
GhostType::Inky => "inky", // GhostType::Inky => "inky",
GhostType::Clyde => "clyde", // GhostType::Clyde => "clyde",
} // }
} // }
/// Returns the base movement speed for this ghost type. // /// Returns the base movement speed for this ghost type.
pub fn base_speed(self) -> f32 { // pub fn base_speed(self) -> f32 {
match self { // match self {
GhostType::Blinky => 1.0, // GhostType::Blinky => 1.0,
GhostType::Pinky => 0.95, // GhostType::Pinky => 0.95,
GhostType::Inky => 0.9, // GhostType::Inky => 0.9,
GhostType::Clyde => 0.85, // GhostType::Clyde => 0.85,
} // }
} // }
} // }
/// A ghost entity that roams the game world. // /// A ghost entity that roams the game world.
/// // ///
/// Ghosts move through the game world using a graph-based navigation system // /// Ghosts move through the game world using a graph-based navigation system
/// and display directional animated sprites. They randomly choose directions // /// and display directional animated sprites. They randomly choose directions
/// at each intersection. // /// at each intersection.
pub struct Ghost { // pub struct Ghost {
/// Handles movement through the game graph // /// Handles movement through the game graph
pub traverser: Traverser, // pub traverser: Traverser,
/// The type of ghost (affects appearance and speed) // /// The type of ghost (affects appearance and speed)
pub ghost_type: GhostType, // pub ghost_type: GhostType,
/// Manages directional animated textures for different movement states // /// Manages directional animated textures for different movement states
texture: DirectionalAnimatedTexture, // texture: DirectionalAnimatedTexture,
/// Current movement speed // /// Current movement speed
speed: f32, // speed: f32,
} // }
impl Entity for Ghost { // impl Entity for Ghost {
fn traverser(&self) -> &Traverser { // fn traverser(&self) -> &Traverser {
&self.traverser // &self.traverser
} // }
fn traverser_mut(&mut self) -> &mut Traverser { // fn traverser_mut(&mut self) -> &mut Traverser {
&mut self.traverser // &mut self.traverser
} // }
fn texture(&self) -> &DirectionalAnimatedTexture { // fn texture(&self) -> &DirectionalAnimatedTexture {
&self.texture // &self.texture
} // }
fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture { // fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture {
&mut self.texture // &mut self.texture
} // }
fn speed(&self) -> f32 { // fn speed(&self) -> f32 {
self.speed // self.speed
} // }
fn can_traverse(&self, edge: Edge) -> bool { // fn can_traverse(&self, edge: Edge) -> bool {
can_ghost_traverse(edge) // can_ghost_traverse(edge)
} // }
fn tick(&mut self, dt: f32, graph: &Graph) { // fn tick(&mut self, dt: f32, graph: &Graph) {
// Choose random direction when at a node // // Choose random direction when at a node
if self.traverser.position.is_at_node() { // if self.traverser.position.is_at_node() {
self.choose_random_direction(graph); // self.choose_random_direction(graph);
} // }
if let Err(e) = self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse) { // if let Err(e) = self.traverser.advance(graph, dt * 60.0 * self.speed, &can_ghost_traverse) {
error!("Ghost movement error: {}", e); // error!("Ghost movement error: {}", e);
} // }
self.texture.tick(dt); // 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.
/// // ///
/// Sets up animated textures for all four directions with moving and stopped states. // /// Sets up animated textures for all four directions with moving and stopped states.
/// The moving animation cycles through two sprite variants. // /// The moving animation cycles through two sprite variants.
pub fn new(graph: &Graph, start_node: NodeId, ghost_type: GhostType, atlas: &SpriteAtlas) -> GameResult<Self> { // pub fn new(graph: &Graph, start_node: NodeId, ghost_type: GhostType, atlas: &SpriteAtlas) -> GameResult<Self> {
let mut textures = [None, None, None, None]; // let mut textures = [None, None, None, None];
let mut stopped_textures = [None, None, None, None]; // let mut stopped_textures = [None, None, None, None];
for direction in Direction::DIRECTIONS { // for direction in Direction::DIRECTIONS {
let moving_prefix = match direction { // let moving_prefix = match direction {
Direction::Up => "up", // Direction::Up => "up",
Direction::Down => "down", // Direction::Down => "down",
Direction::Left => "left", // Direction::Left => "left",
Direction::Right => "right", // Direction::Right => "right",
}; // };
let moving_tiles = vec![ // let moving_tiles = vec![
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a")) // SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"))
.ok_or_else(|| { // .ok_or_else(|| {
GameError::Texture(TextureError::AtlasTileNotFound(format!( // GameError::Texture(TextureError::AtlasTileNotFound(format!(
"ghost/{}/{}_{}.png", // "ghost/{}/{}_{}.png",
ghost_type.as_str(), // ghost_type.as_str(),
moving_prefix, // moving_prefix,
"a" // "a"
))) // )))
})?, // })?,
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "b")) // SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "b"))
.ok_or_else(|| { // .ok_or_else(|| {
GameError::Texture(TextureError::AtlasTileNotFound(format!( // GameError::Texture(TextureError::AtlasTileNotFound(format!(
"ghost/{}/{}_{}.png", // "ghost/{}/{}_{}.png",
ghost_type.as_str(), // ghost_type.as_str(),
moving_prefix, // moving_prefix,
"b" // "b"
))) // )))
})?, // })?,
]; // ];
let stopped_tiles = // let stopped_tiles =
vec![ // vec![
SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a")) // SpriteAtlas::get_tile(atlas, &format!("ghost/{}/{}_{}.png", ghost_type.as_str(), moving_prefix, "a"))
.ok_or_else(|| { // .ok_or_else(|| {
GameError::Texture(TextureError::AtlasTileNotFound(format!( // GameError::Texture(TextureError::AtlasTileNotFound(format!(
"ghost/{}/{}_{}.png", // "ghost/{}/{}_{}.png",
ghost_type.as_str(), // ghost_type.as_str(),
moving_prefix, // moving_prefix,
"a" // "a"
))) // )))
})?, // })?,
]; // ];
textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2)?); // textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.2)?);
stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?); // stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?);
} // }
Ok(Self { // Ok(Self {
traverser: Traverser::new(graph, start_node, Direction::Left, &can_ghost_traverse), // traverser: Traverser::new(graph, start_node, Direction::Left, &can_ghost_traverse),
ghost_type, // ghost_type,
texture: DirectionalAnimatedTexture::new(textures, stopped_textures), // texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
speed: ghost_type.base_speed(), // speed: ghost_type.base_speed(),
}) // })
} // }
/// 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();
let intersection = &graph.adjacency_list[current_node]; // let intersection = &graph.adjacency_list[current_node];
// Collect all available directions // // Collect all available directions
let mut available_directions = SmallVec::<[_; 4]>::new(); // let mut available_directions = SmallVec::<[_; 4]>::new();
for direction in Direction::DIRECTIONS { // for direction in Direction::DIRECTIONS {
if let Some(edge) = intersection.get(direction) { // if let Some(edge) = intersection.get(direction) {
if can_ghost_traverse(edge) { // if can_ghost_traverse(edge) {
available_directions.push(direction); // available_directions.push(direction);
} // }
} // }
} // }
// Choose a random direction (avoid reversing unless necessary) // // Choose a random direction (avoid reversing unless necessary)
if !available_directions.is_empty() { // if !available_directions.is_empty() {
let mut rng = SmallRng::from_os_rng(); // let mut rng = SmallRng::from_os_rng();
// Filter out the opposite direction if possible, but allow it if we have limited options // // Filter out the opposite direction if possible, but allow it if we have limited options
let opposite = self.traverser.direction.opposite(); // let opposite = self.traverser.direction.opposite();
let filtered_directions: Vec<_> = available_directions // let filtered_directions: Vec<_> = available_directions
.iter() // .iter()
.filter(|&&dir| dir != opposite || available_directions.len() <= 2) // .filter(|&&dir| dir != opposite || available_directions.len() <= 2)
.collect(); // .collect();
if let Some(&random_direction) = filtered_directions.choose(&mut rng) { // if let Some(&random_direction) = filtered_directions.choose(&mut rng) {
self.traverser.set_next_direction(*random_direction); // self.traverser.set_next_direction(*random_direction);
} // }
} // }
} // }
/// 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 an error if pathfinding fails. // /// Returns a vector of NodeIds representing the path, or an error if pathfinding fails.
/// The path includes the current node and the target node. // /// The path includes the current node and the target node.
pub fn calculate_path_to_target(&self, graph: &Graph, target: NodeId) -> GameResult<Vec<NodeId>> { // pub fn calculate_path_to_target(&self, graph: &Graph, target: NodeId) -> GameResult<Vec<NodeId>> {
let start_node = self.traverser.position.from_node_id(); // let start_node = self.traverser.position.from_node_id();
// Use Dijkstra's algorithm to find the shortest path // // Use Dijkstra's algorithm to find the shortest path
let result = dijkstra( // let result = dijkstra(
&start_node, // &start_node,
|&node_id| { // |&node_id| {
// Get all edges from the current node // // Get all edges from the current node
graph.adjacency_list[node_id] // graph.adjacency_list[node_id]
.edges() // .edges()
.filter(|edge| can_ghost_traverse(*edge)) // .filter(|edge| can_ghost_traverse(*edge))
.map(|edge| (edge.target, (edge.distance * 100.0) as u32)) // .map(|edge| (edge.target, (edge.distance * 100.0) as u32))
.collect::<Vec<_>>() // .collect::<Vec<_>>()
}, // },
|&node_id| node_id == target, // |&node_id| node_id == target,
); // );
result.map(|(path, _cost)| path).ok_or_else(|| { // result.map(|(path, _cost)| path).ok_or_else(|| {
GameError::Entity(EntityError::PathfindingFailed(format!( // GameError::Entity(EntityError::PathfindingFailed(format!(
"No path found from node {} to target {}", // "No path found from node {} to target {}",
start_node, target // start_node, target
))) // )))
}) // })
} // }
/// Returns the ghost's color for debug rendering. // /// Returns the ghost's color for debug rendering.
pub fn debug_color(&self) -> sdl2::pixels::Color { // pub fn debug_color(&self) -> sdl2::pixels::Color {
match self.ghost_type { // match self.ghost_type {
GhostType::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red // GhostType::Blinky => sdl2::pixels::Color::RGB(255, 0, 0), // Red
GhostType::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink // GhostType::Pinky => sdl2::pixels::Color::RGB(255, 182, 255), // Pink
GhostType::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan // GhostType::Inky => sdl2::pixels::Color::RGB(0, 255, 255), // Cyan
GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange // GhostType::Clyde => sdl2::pixels::Color::RGB(255, 182, 85), // Orange
} // }
} // }
} // }
impl Collidable for Ghost { // impl Collidable for Ghost {
fn position(&self) -> crate::entity::traversal::Position { // fn position(&self) -> crate::entity::traversal::Position {
self.traverser.position // self.traverser.position
} // }
} // }

View File

@@ -1,9 +1,8 @@
use glam::Vec2; use glam::Vec2;
use super::direction::Direction; use crate::ecs::NodeId;
/// A unique identifier for a node, represented by its index in the graph's storage. use super::direction::Direction;
pub type NodeId = usize;
/// Defines who can traverse a given edge. /// Defines who can traverse a given edge.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]

View File

@@ -1,117 +1,117 @@
use crate::{ // use crate::{
constants, // constants,
entity::{collision::Collidable, graph::Graph}, // entity::{collision::Collidable, graph::Graph},
error::{EntityError, GameResult}, // error::{EntityError, GameResult},
texture::sprite::{Sprite, SpriteAtlas}, // texture::sprite::{Sprite, SpriteAtlas},
}; // };
use sdl2::render::{Canvas, RenderTarget}; // use sdl2::render::{Canvas, RenderTarget};
use strum_macros::{EnumCount, EnumIter}; // use strum_macros::{EnumCount, EnumIter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] // #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ItemType { // pub enum ItemType {
Pellet, // Pellet,
Energizer, // Energizer,
#[allow(dead_code)] // #[allow(dead_code)]
Fruit { // Fruit {
kind: FruitKind, // kind: FruitKind,
}, // },
} // }
impl ItemType { // impl ItemType {
pub fn get_score(self) -> u32 { // pub fn get_score(self) -> u32 {
match self { // match self {
ItemType::Pellet => 10, // ItemType::Pellet => 10,
ItemType::Energizer => 50, // ItemType::Energizer => 50,
ItemType::Fruit { kind } => kind.get_score(), // ItemType::Fruit { kind } => kind.get_score(),
} // }
} // }
} // }
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCount)] // #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCount)]
#[allow(dead_code)] // #[allow(dead_code)]
pub enum FruitKind { // pub enum FruitKind {
Apple, // Apple,
Strawberry, // Strawberry,
Orange, // Orange,
Melon, // Melon,
Bell, // Bell,
Key, // Key,
Galaxian, // Galaxian,
} // }
impl FruitKind { // impl FruitKind {
#[allow(dead_code)] // #[allow(dead_code)]
pub fn index(self) -> u8 { // pub fn index(self) -> u8 {
match self { // match self {
FruitKind::Apple => 0, // FruitKind::Apple => 0,
FruitKind::Strawberry => 1, // FruitKind::Strawberry => 1,
FruitKind::Orange => 2, // FruitKind::Orange => 2,
FruitKind::Melon => 3, // FruitKind::Melon => 3,
FruitKind::Bell => 4, // FruitKind::Bell => 4,
FruitKind::Key => 5, // FruitKind::Key => 5,
FruitKind::Galaxian => 6, // FruitKind::Galaxian => 6,
} // }
} // }
pub fn get_score(self) -> u32 { // pub fn get_score(self) -> u32 {
match self { // match self {
FruitKind::Apple => 100, // FruitKind::Apple => 100,
FruitKind::Strawberry => 300, // FruitKind::Strawberry => 300,
FruitKind::Orange => 500, // FruitKind::Orange => 500,
FruitKind::Melon => 700, // FruitKind::Melon => 700,
FruitKind::Bell => 1000, // FruitKind::Bell => 1000,
FruitKind::Key => 2000, // FruitKind::Key => 2000,
FruitKind::Galaxian => 3000, // FruitKind::Galaxian => 3000,
} // }
} // }
} // }
pub struct Item { // pub struct Item {
pub node_index: usize, // pub node_index: usize,
pub item_type: ItemType, // pub item_type: ItemType,
pub sprite: Sprite, // pub sprite: Sprite,
pub collected: bool, // pub collected: bool,
} // }
impl Item { // impl Item {
pub fn new(node_index: usize, item_type: ItemType, sprite: Sprite) -> Self { // pub fn new(node_index: usize, item_type: ItemType, sprite: Sprite) -> Self {
Self { // Self {
node_index, // node_index,
item_type, // item_type,
sprite, // sprite,
collected: false, // collected: false,
} // }
} // }
pub fn is_collected(&self) -> bool { // pub fn is_collected(&self) -> bool {
self.collected // self.collected
} // }
pub fn collect(&mut self) { // pub fn collect(&mut self) {
self.collected = true; // self.collected = true;
} // }
pub fn get_score(&self) -> u32 { // pub fn get_score(&self) -> u32 {
self.item_type.get_score() // self.item_type.get_score()
} // }
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> GameResult<()> { // pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> GameResult<()> {
if self.collected { // if self.collected {
return Ok(()); // return Ok(());
} // }
let node = graph // let node = graph
.get_node(self.node_index) // .get_node(self.node_index)
.ok_or(EntityError::NodeNotFound(self.node_index))?; // .ok_or(EntityError::NodeNotFound(self.node_index))?;
let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2(); // let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2();
self.sprite.render(canvas, atlas, position)?; // self.sprite.render(canvas, atlas, position)?;
Ok(()) // Ok(())
} // }
} // }
impl Collidable for Item { // impl Collidable for Item {
fn position(&self) -> crate::entity::traversal::Position { // fn position(&self) -> crate::entity::traversal::Position {
crate::entity::traversal::Position::AtNode(self.node_index) // crate::entity::traversal::Position::AtNode(self.node_index)
} // }
} // }

View File

@@ -1,134 +1,115 @@
//! Pac-Man entity implementation. // //! Pac-Man entity implementation.
//! // //!
//! This module contains the main player character logic, including movement, // //! This module contains the main player character logic, including movement,
//! 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 crate::entity::{ // use crate::entity::{
collision::Collidable, // collision::Collidable,
direction::Direction, // direction::Direction,
graph::{Edge, EdgePermissions, Graph, NodeId}, // graph::{Edge, EdgePermissions, Graph, NodeId},
r#trait::Entity, // r#trait::Entity,
traversal::Traverser, // 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 tracing::error;
use tracing::error;
use crate::error::{GameError, GameResult, TextureError}; // use crate::error::{GameError, GameResult, TextureError};
/// Determines if Pac-Man can traverse a given edge. // /// Determines if Pac-Man can traverse a given edge.
/// // ///
/// Pac-Man can only move through edges that allow all entities. // /// Pac-Man can only move through edges that allow all entities.
fn can_pacman_traverse(edge: Edge) -> bool { // fn can_pacman_traverse(edge: Edge) -> bool {
matches!(edge.permissions, EdgePermissions::All) // matches!(edge.permissions, EdgePermissions::All)
} // }
/// The main player character entity. // /// The main player character entity.
/// // ///
/// Pac-Man moves through the game world using a graph-based navigation system // /// Pac-Man moves through the game world using a graph-based navigation system
/// and displays directional animated sprites based on movement state. // /// and displays directional animated sprites based on movement state.
pub struct Pacman { // pub struct Pacman {
/// Handles movement through the game graph // /// Handles movement through the game graph
pub traverser: Traverser, // pub traverser: Traverser,
/// Manages directional animated textures for different movement states // /// Manages directional animated textures for different movement states
texture: DirectionalAnimatedTexture, // texture: DirectionalAnimatedTexture,
} // }
impl Entity for Pacman { // impl Entity for Pacman {
fn traverser(&self) -> &Traverser { // fn traverser(&self) -> &Traverser {
&self.traverser // &self.traverser
} // }
fn traverser_mut(&mut self) -> &mut Traverser { // fn traverser_mut(&mut self) -> &mut Traverser {
&mut self.traverser // &mut self.traverser
} // }
fn texture(&self) -> &DirectionalAnimatedTexture { // fn texture(&self) -> &DirectionalAnimatedTexture {
&self.texture // &self.texture
} // }
fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture { // fn texture_mut(&mut self) -> &mut DirectionalAnimatedTexture {
&mut self.texture // &mut self.texture
} // }
fn speed(&self) -> f32 { // fn speed(&self) -> f32 {
1.125 // 1.125
} // }
fn can_traverse(&self, edge: Edge) -> bool { // fn can_traverse(&self, edge: Edge) -> bool {
can_pacman_traverse(edge) // can_pacman_traverse(edge)
} // }
fn tick(&mut self, dt: f32, graph: &Graph) { // fn tick(&mut self, dt: f32, graph: &Graph) {
if let Err(e) = self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse) { // if let Err(e) = self.traverser.advance(graph, dt * 60.0 * 1.125, &can_pacman_traverse) {
error!("Pac-Man movement error: {}", e); // error!("Pac-Man movement error: {}", e);
} // }
self.texture.tick(dt); // 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.
/// // ///
/// Sets up animated textures for all four directions with moving and stopped states. // /// Sets up animated textures for all four directions with moving and stopped states.
/// The moving animation cycles through open mouth, closed mouth, and full sprites. // /// The moving animation cycles through open mouth, closed mouth, and full sprites.
pub fn new(graph: &Graph, start_node: NodeId, atlas: &SpriteAtlas) -> GameResult<Self> { // pub fn new(graph: &Graph, start_node: NodeId, atlas: &SpriteAtlas) -> GameResult<Self> {
let mut textures = [None, None, None, None]; // let mut textures = [None, None, None, None];
let mut stopped_textures = [None, None, None, None]; // let mut stopped_textures = [None, None, None, None];
for direction in Direction::DIRECTIONS { // for direction in Direction::DIRECTIONS {
let moving_prefix = match direction { // let moving_prefix = match direction {
Direction::Up => "pacman/up", // Direction::Up => "pacman/up",
Direction::Down => "pacman/down", // Direction::Down => "pacman/down",
Direction::Left => "pacman/left", // Direction::Left => "pacman/left",
Direction::Right => "pacman/right", // Direction::Right => "pacman/right",
}; // };
let moving_tiles = vec![ // let moving_tiles = vec![
SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_a.png")) // SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_a.png"))
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_a.png"))))?, // .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_a.png"))))?,
SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png")) // SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png"))
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?, // .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?,
SpriteAtlas::get_tile(atlas, "pacman/full.png") // 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())))?,
]; // ];
let stopped_tiles = vec![SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png")) // let stopped_tiles = vec![SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png"))
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?]; // .ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound(format!("{moving_prefix}_b.png"))))?];
textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.08)?); // textures[direction.as_usize()] = Some(AnimatedTexture::new(moving_tiles, 0.08)?);
stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?); // stopped_textures[direction.as_usize()] = Some(AnimatedTexture::new(stopped_tiles, 0.1)?);
} // }
Ok(Self { // Ok(Self {
traverser: Traverser::new(graph, start_node, Direction::Left, &can_pacman_traverse), // traverser: Traverser::new(graph, start_node, Direction::Left, &can_pacman_traverse),
texture: DirectionalAnimatedTexture::new(textures, stopped_textures), // texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
}) // })
} // }
// }
/// Handles keyboard input to change Pac-Man's direction. // impl Collidable for Pacman {
/// // fn position(&self) -> crate::entity::traversal::Position {
/// Maps arrow keys to directions and queues the direction change // self.traverser.position
/// for the next valid intersection. // }
pub fn handle_key(&mut self, keycode: Keycode) { // }
let direction = match keycode {
Keycode::Up => Some(Direction::Up),
Keycode::Down => Some(Direction::Down),
Keycode::Left => Some(Direction::Left),
Keycode::Right => Some(Direction::Right),
_ => None,
};
if let Some(direction) = direction {
self.traverser.set_next_direction(direction);
}
}
}
impl Collidable for Pacman {
fn position(&self) -> crate::entity::traversal::Position {
self.traverser.position
}
}

View File

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

View File

@@ -1,57 +1,10 @@
use tracing::error; use tracing::error;
use crate::ecs::{NodeId, Position};
use crate::error::GameResult; use crate::error::GameResult;
use super::direction::Direction; use super::direction::Direction;
use super::graph::{Edge, Graph, NodeId}; use super::graph::{Edge, Graph};
/// 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(Debug, PartialEq, Clone, Copy)]
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,
},
}
#[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<NodeId> {
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(_))
}
}
/// Manages an entity's movement through the graph. /// Manages an entity's movement through the graph.
/// ///
@@ -72,27 +25,6 @@ pub struct Traverser {
} }
impl Traverser { impl Traverser {
/// Creates a new traverser starting at the given node ID.
///
/// The traverser will immediately attempt to start moving in the initial direction.
pub fn new<F>(graph: &Graph, start_node: NodeId, initial_direction: Direction, can_traverse: &F) -> Self
where
F: Fn(Edge) -> bool,
{
let mut traverser = Traverser {
position: Position::AtNode(start_node),
direction: initial_direction,
next_direction: Some((initial_direction, 1)),
};
// This will kickstart the traverser into motion
if let Err(e) = traverser.advance(graph, 0.0, can_traverse) {
error!("Traverser initialization error: {}", e);
}
traverser
}
/// Sets the next direction for the traverser to take. /// Sets the next direction for the traverser to take.
/// ///
/// The direction is buffered and will be applied at the next opportunity, /// The direction is buffered and will be applied at the next opportunity,

Some files were not shown because too many files have changed in this diff Show More