mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-10 06:07:55 -06:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 730daed20a | |||
| b9bae99a4c | |||
| 2c65048fb0 | |||
| 3388d77ec5 | |||
| 242da2e263 | |||
| 70fb2b9503 | |||
| 0aa056a0ae |
12
src/app.rs
12
src/app.rs
@@ -1,11 +1,11 @@
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
use sdl2::render::TextureCreator;
|
||||||
use sdl2::ttf::Sdl2TtfContext;
|
use sdl2::ttf::Sdl2TtfContext;
|
||||||
use sdl2::video::{Window, WindowContext};
|
use sdl2::video::WindowContext;
|
||||||
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
use sdl2::{AudioSubsystem, EventPump, Sdl, VideoSubsystem};
|
||||||
use tracing::{error, warn};
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::error::{GameError, GameResult};
|
use crate::error::{GameError, GameResult};
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ pub struct App {
|
|||||||
pub game: Game,
|
pub game: Game,
|
||||||
last_tick: Instant,
|
last_tick: Instant,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
cursor_pos: Vec2,
|
_cursor_pos: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -46,7 +46,7 @@ impl App {
|
|||||||
.build()
|
.build()
|
||||||
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
.map_err(|e| GameError::Sdl(e.to_string()))?;
|
||||||
|
|
||||||
let mut canvas = Box::leak(Box::new(
|
let canvas = Box::leak(Box::new(
|
||||||
window
|
window
|
||||||
.into_canvas()
|
.into_canvas()
|
||||||
.accelerated()
|
.accelerated()
|
||||||
@@ -74,7 +74,7 @@ impl App {
|
|||||||
game,
|
game,
|
||||||
focused: true,
|
focused: true,
|
||||||
last_tick: Instant::now(),
|
last_tick: Instant::now(),
|
||||||
cursor_pos: Vec2::ZERO,
|
_cursor_pos: Vec2::ZERO,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
|
||||||
use crate::ecs::NodeId;
|
use crate::systems::components::NodeId;
|
||||||
|
|
||||||
use super::direction::Direction;
|
use super::direction::Direction;
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ pub mod graph;
|
|||||||
pub mod item;
|
pub mod item;
|
||||||
pub mod pacman;
|
pub mod pacman;
|
||||||
pub mod r#trait;
|
pub mod r#trait;
|
||||||
pub mod traversal;
|
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
// use tracing::error;
|
|
||||||
|
|
||||||
// use crate::error::GameResult;
|
|
||||||
|
|
||||||
// use super::direction::Direction;
|
|
||||||
// use super::graph::{Edge, Graph, NodeId};
|
|
||||||
|
|
||||||
// /// Manages an entity's movement through the graph.
|
|
||||||
// ///
|
|
||||||
// /// A `Traverser` encapsulates the state of an entity's position and direction,
|
|
||||||
// /// providing a way to advance along the graph's paths based on a given distance.
|
|
||||||
// /// It also handles direction changes, buffering the next intended direction.
|
|
||||||
// pub struct Traverser {
|
|
||||||
// /// The current position of the traverser in the graph.
|
|
||||||
// pub position: Position,
|
|
||||||
// /// The current direction of movement.
|
|
||||||
// pub direction: Direction,
|
|
||||||
// /// Buffered direction change with remaining frame count for timing.
|
|
||||||
// ///
|
|
||||||
// /// The `u8` value represents the number of frames remaining before
|
|
||||||
// /// the buffered direction expires. This allows for responsive controls
|
|
||||||
// /// by storing direction changes for a limited time.
|
|
||||||
// pub next_direction: Option<(Direction, u8)>,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
// ///
|
|
||||||
// /// The direction is buffered and will be applied at the next opportunity,
|
|
||||||
// /// typically when the traverser reaches a new node. This allows for responsive
|
|
||||||
// /// controls, as the new direction is stored for a limited time.
|
|
||||||
// pub fn set_next_direction(&mut self, new_direction: Direction) {
|
|
||||||
// if self.direction != new_direction {
|
|
||||||
// self.next_direction = Some((new_direction, 30));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /// Advances the traverser along the graph by a specified distance.
|
|
||||||
// ///
|
|
||||||
// /// This method updates the traverser's position based on its current state
|
|
||||||
// /// and the distance to travel.
|
|
||||||
// ///
|
|
||||||
// /// - If at a node, it checks for a buffered direction to start moving.
|
|
||||||
// /// - If between nodes, it moves along the current edge.
|
|
||||||
// /// - If it reaches a node, it attempts to transition to a new edge based on
|
|
||||||
// /// the buffered direction or by continuing straight.
|
|
||||||
// /// - If no valid move is possible, it stops at the node.
|
|
||||||
// ///
|
|
||||||
// /// Returns an error if the movement is invalid (e.g., trying to move in an impossible direction).
|
|
||||||
// pub fn advance<F>(&mut self, graph: &Graph, distance: f32, can_traverse: &F) -> GameResult<()>
|
|
||||||
// where
|
|
||||||
// F: Fn(Edge) -> bool,
|
|
||||||
// {
|
|
||||||
// // Decrement the remaining frames for the next direction
|
|
||||||
// if let Some((direction, remaining)) = self.next_direction {
|
|
||||||
// if remaining > 0 {
|
|
||||||
// self.next_direction = Some((direction, remaining - 1));
|
|
||||||
// } else {
|
|
||||||
// self.next_direction = None;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// match self.position {
|
|
||||||
// Position::AtNode(node_id) => {
|
|
||||||
// // We're not moving, but a buffered direction is available.
|
|
||||||
// if let Some((next_direction, _)) = self.next_direction {
|
|
||||||
// if let Some(edge) = graph.find_edge_in_direction(node_id, next_direction) {
|
|
||||||
// if can_traverse(edge) {
|
|
||||||
// // Start moving in that direction
|
|
||||||
// self.position = Position::BetweenNodes {
|
|
||||||
// from: node_id,
|
|
||||||
// to: edge.target,
|
|
||||||
// traversed: distance.max(0.0),
|
|
||||||
// };
|
|
||||||
// self.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 {
|
|
||||||
// return Err(crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(
|
|
||||||
// format!("No edge found in direction {:?} from node {}", next_direction, node_id),
|
|
||||||
// )));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// self.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 Ok(());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let edge = graph.find_edge(from, to).ok_or_else(|| {
|
|
||||||
// crate::error::GameError::Entity(crate::error::EntityError::InvalidMovement(format!(
|
|
||||||
// "Inconsistent state: Traverser is on a non-existent edge from {} to {}.",
|
|
||||||
// from, to
|
|
||||||
// )))
|
|
||||||
// })?;
|
|
||||||
|
|
||||||
// let new_traversed = traversed + distance;
|
|
||||||
|
|
||||||
// if new_traversed < edge.distance {
|
|
||||||
// // Still on the same edge, just update the distance.
|
|
||||||
// self.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, _)) = self.next_direction {
|
|
||||||
// if let Some(edge) = graph.find_edge_in_direction(to, next_dir) {
|
|
||||||
// if can_traverse(edge) {
|
|
||||||
// self.position = Position::BetweenNodes {
|
|
||||||
// from: to,
|
|
||||||
// to: edge.target,
|
|
||||||
// traversed: overflow,
|
|
||||||
// };
|
|
||||||
|
|
||||||
// self.direction = next_dir; // Remember our new direction
|
|
||||||
// self.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) = graph.find_edge_in_direction(to, self.direction) {
|
|
||||||
// if can_traverse(edge) {
|
|
||||||
// self.position = Position::BetweenNodes {
|
|
||||||
// from: to,
|
|
||||||
// to: edge.target,
|
|
||||||
// traversed: overflow,
|
|
||||||
// };
|
|
||||||
// } else {
|
|
||||||
// self.position = Position::AtNode(to);
|
|
||||||
// self.next_direction = None;
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// self.position = Position::AtNode(to);
|
|
||||||
// self.next_direction = None;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use bevy_ecs::event::Event;
|
use bevy_ecs::event::Event;
|
||||||
|
|
||||||
use crate::input::commands::GameCommand;
|
use crate::systems::input::GameCommand;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Event)]
|
#[derive(Debug, Clone, Copy, Event)]
|
||||||
pub enum GameEvent {
|
pub enum GameEvent {
|
||||||
@@ -3,38 +3,35 @@
|
|||||||
include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
|
include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
|
||||||
|
|
||||||
use crate::constants::CANVAS_SIZE;
|
use crate::constants::CANVAS_SIZE;
|
||||||
use crate::ecs::interact::interact_system;
|
|
||||||
use crate::ecs::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource};
|
|
||||||
use crate::ecs::{DeltaTime, DirectionalAnimated, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity};
|
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::direction::Direction;
|
||||||
use crate::entity::{graph, traversal};
|
|
||||||
use crate::error::{GameError, GameResult, TextureError};
|
use crate::error::{GameError, GameResult, TextureError};
|
||||||
use crate::input::commands::GameCommand;
|
use crate::events::GameEvent;
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
|
use crate::systems::components::{
|
||||||
|
DeltaTime, DirectionalAnimated, EntityType, GlobalState, PlayerBundle, PlayerControlled, Position, Renderable, Velocity,
|
||||||
|
};
|
||||||
|
use crate::systems::control::player_system;
|
||||||
|
use crate::systems::movement::movement_system;
|
||||||
|
use crate::systems::render::{directional_render_system, render_system, BackbufferResource, MapTextureResource};
|
||||||
use crate::texture::animated::AnimatedTexture;
|
use crate::texture::animated::AnimatedTexture;
|
||||||
use crate::texture::directional::DirectionalAnimatedTexture;
|
|
||||||
use crate::texture::sprite::Sprite;
|
|
||||||
use bevy_ecs::event::EventRegistry;
|
use bevy_ecs::event::EventRegistry;
|
||||||
use bevy_ecs::observer::Trigger;
|
use bevy_ecs::observer::Trigger;
|
||||||
use bevy_ecs::schedule::IntoScheduleConfigs;
|
use bevy_ecs::schedule::IntoScheduleConfigs;
|
||||||
use bevy_ecs::system::{Commands, ResMut};
|
use bevy_ecs::system::ResMut;
|
||||||
use bevy_ecs::{schedule::Schedule, world::World};
|
use bevy_ecs::{schedule::Schedule, world::World};
|
||||||
use sdl2::image::LoadTexture;
|
use sdl2::image::LoadTexture;
|
||||||
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
use sdl2::render::{Canvas, ScaleMode, TextureCreator};
|
||||||
use sdl2::video::{Window, WindowContext};
|
use sdl2::video::{Window, WindowContext};
|
||||||
use sdl2::EventPump;
|
use sdl2::EventPump;
|
||||||
|
|
||||||
use crate::asset::{get_asset_bytes, Asset};
|
use crate::asset::{get_asset_bytes, Asset};
|
||||||
use crate::input::{handle_input, Bindings};
|
|
||||||
use crate::map::render::MapRenderer;
|
use crate::map::render::MapRenderer;
|
||||||
|
use crate::systems::input::{input_system, Bindings, GameCommand};
|
||||||
use crate::{
|
use crate::{
|
||||||
constants,
|
constants,
|
||||||
texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas},
|
texture::sprite::{AtlasMapper, SpriteAtlas},
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::events::GameEvent;
|
|
||||||
|
|
||||||
pub mod events;
|
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
/// The `Game` struct is the main entry point for the game.
|
/// The `Game` struct is the main entry point for the game.
|
||||||
@@ -132,7 +129,11 @@ impl Game {
|
|||||||
let player = PlayerBundle {
|
let player = PlayerBundle {
|
||||||
player: PlayerControlled,
|
player: PlayerControlled,
|
||||||
position: Position::AtNode(pacman_start_node),
|
position: Position::AtNode(pacman_start_node),
|
||||||
velocity: Velocity::default(),
|
velocity: Velocity {
|
||||||
|
direction: Direction::Up,
|
||||||
|
next_direction: None,
|
||||||
|
speed: 1.125,
|
||||||
|
},
|
||||||
sprite: Renderable {
|
sprite: Renderable {
|
||||||
sprite: SpriteAtlas::get_tile(&atlas, "pacman/full.png")
|
sprite: 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())))?,
|
||||||
@@ -142,6 +143,7 @@ impl Game {
|
|||||||
textures,
|
textures,
|
||||||
stopped_textures,
|
stopped_textures,
|
||||||
},
|
},
|
||||||
|
entity_type: EntityType::Player,
|
||||||
};
|
};
|
||||||
|
|
||||||
world.insert_non_send_resource(atlas);
|
world.insert_non_send_resource(atlas);
|
||||||
@@ -164,7 +166,16 @@ impl Game {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
schedule.add_systems((handle_input, interact_system, directional_render_system, render_system).chain());
|
schedule.add_systems(
|
||||||
|
(
|
||||||
|
input_system,
|
||||||
|
player_system,
|
||||||
|
movement_system,
|
||||||
|
directional_render_system,
|
||||||
|
render_system,
|
||||||
|
)
|
||||||
|
.chain(),
|
||||||
|
);
|
||||||
|
|
||||||
// Spawn player
|
// Spawn player
|
||||||
world.spawn(player);
|
world.spawn(player);
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
use crate::entity::direction::Direction;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum GameCommand {
|
|
||||||
MovePlayer(Direction),
|
|
||||||
Exit,
|
|
||||||
TogglePause,
|
|
||||||
ToggleDebug,
|
|
||||||
MuteAudio,
|
|
||||||
ResetLevel,
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,12 @@ pub mod app;
|
|||||||
pub mod asset;
|
pub mod asset;
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod ecs;
|
|
||||||
pub mod entity;
|
pub mod entity;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod events;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub mod input;
|
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
|
pub mod systems;
|
||||||
pub mod texture;
|
pub mod texture;
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ mod asset;
|
|||||||
mod audio;
|
mod audio;
|
||||||
mod constants;
|
mod constants;
|
||||||
|
|
||||||
mod ecs;
|
|
||||||
mod entity;
|
mod entity;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod events;
|
||||||
mod game;
|
mod game;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod input;
|
|
||||||
mod map;
|
mod map;
|
||||||
mod platform;
|
mod platform;
|
||||||
|
mod systems;
|
||||||
mod texture;
|
mod texture;
|
||||||
|
|
||||||
/// The main entry point of the application.
|
/// The main entry point of the application.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
//! Map construction and building functionality.
|
//! Map construction and building functionality.
|
||||||
|
|
||||||
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE, RAW_BOARD};
|
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
|
||||||
use crate::ecs::NodeId;
|
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::direction::Direction;
|
||||||
use crate::entity::graph::{EdgePermissions, Graph, Node};
|
use crate::entity::graph::{EdgePermissions, Graph, Node};
|
||||||
use crate::map::parser::MapTileParser;
|
use crate::map::parser::MapTileParser;
|
||||||
use crate::map::render::MapRenderer;
|
use crate::map::render::MapRenderer;
|
||||||
use crate::texture::sprite::{Sprite, SpriteAtlas};
|
use crate::systems::components::NodeId;
|
||||||
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
use bevy_ecs::resource::Resource;
|
use bevy_ecs::resource::Resource;
|
||||||
use glam::{IVec2, Vec2};
|
use glam::{IVec2, Vec2};
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
//! 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 bevy_ecs::{bundle::Bundle, component::Component, resource::Resource};
|
||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
entity::{direction::Direction, graph::Graph, traversal},
|
entity::{direction::Direction, graph::Graph},
|
||||||
error::{EntityError, GameResult},
|
error::{EntityError, GameResult},
|
||||||
texture::{
|
texture::{animated::AnimatedTexture, sprite::AtlasTile},
|
||||||
animated::AnimatedTexture,
|
|
||||||
directional::DirectionalAnimatedTexture,
|
|
||||||
sprite::{AtlasTile, Sprite},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A tag component for entities that are controlled by the player.
|
/// A tag component for entities that are controlled by the player.
|
||||||
#[derive(Default, Component)]
|
#[derive(Default, Component)]
|
||||||
pub struct PlayerControlled;
|
pub struct PlayerControlled;
|
||||||
|
|
||||||
|
/// A tag component denoting the type of entity.
|
||||||
|
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum EntityType {
|
||||||
|
Player,
|
||||||
|
Ghost,
|
||||||
|
Pellet,
|
||||||
|
PowerPellet,
|
||||||
|
Wall,
|
||||||
|
}
|
||||||
|
|
||||||
/// A component for entities that have a sprite, with a layer for ordering.
|
/// A component for entities that have a sprite, with a layer for ordering.
|
||||||
///
|
///
|
||||||
/// This is intended to be modified by other entities allowing animation.
|
/// This is intended to be modified by other entities allowing animation.
|
||||||
@@ -125,7 +126,8 @@ impl Position {
|
|||||||
#[derive(Default, Component)]
|
#[derive(Default, Component)]
|
||||||
pub struct Velocity {
|
pub struct Velocity {
|
||||||
pub direction: Direction,
|
pub direction: Direction,
|
||||||
pub speed: Option<f32>,
|
pub next_direction: Option<(Direction, u8)>,
|
||||||
|
pub speed: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Bundle)]
|
#[derive(Bundle)]
|
||||||
@@ -135,6 +137,7 @@ pub struct PlayerBundle {
|
|||||||
pub velocity: Velocity,
|
pub velocity: Velocity,
|
||||||
pub sprite: Renderable,
|
pub sprite: Renderable,
|
||||||
pub directional_animated: DirectionalAnimated,
|
pub directional_animated: DirectionalAnimated,
|
||||||
|
pub entity_type: EntityType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
@@ -144,6 +147,3 @@ pub struct GlobalState {
|
|||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct DeltaTime(pub f32);
|
pub struct DeltaTime(pub f32);
|
||||||
|
|
||||||
pub mod interact;
|
|
||||||
pub mod render;
|
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
event::{EventReader, EventWriter},
|
event::{EventReader, EventWriter},
|
||||||
query::With,
|
|
||||||
system::{Query, ResMut},
|
system::{Query, ResMut},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ecs::{GlobalState, PlayerControlled, Velocity},
|
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::events::GameEvent,
|
events::GameEvent,
|
||||||
input::commands::GameCommand,
|
systems::{
|
||||||
|
components::{GlobalState, PlayerControlled, Velocity},
|
||||||
|
input::GameCommand,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles
|
// Handles
|
||||||
pub fn interact_system(
|
pub fn player_system(
|
||||||
mut events: EventReader<GameEvent>,
|
mut events: EventReader<GameEvent>,
|
||||||
mut state: ResMut<GlobalState>,
|
mut state: ResMut<GlobalState>,
|
||||||
mut players: Query<(&PlayerControlled, &mut Velocity)>,
|
mut players: Query<(&PlayerControlled, &mut Velocity)>,
|
||||||
@@ -32,7 +33,7 @@ pub fn interact_system(
|
|||||||
match event {
|
match event {
|
||||||
GameEvent::Command(command) => match command {
|
GameEvent::Command(command) => match command {
|
||||||
GameCommand::MovePlayer(direction) => {
|
GameCommand::MovePlayer(direction) => {
|
||||||
velocity.direction = *direction;
|
velocity.next_direction = Some((*direction, 90));
|
||||||
}
|
}
|
||||||
GameCommand::Exit => {
|
GameCommand::Exit => {
|
||||||
state.exit = true;
|
state.exit = true;
|
||||||
@@ -3,13 +3,21 @@ use std::collections::HashMap;
|
|||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
event::EventWriter,
|
event::EventWriter,
|
||||||
resource::Resource,
|
resource::Resource,
|
||||||
system::{Commands, NonSendMut, Res},
|
system::{NonSendMut, Res},
|
||||||
};
|
};
|
||||||
use sdl2::{event::Event, keyboard::Keycode, EventPump};
|
use sdl2::{event::Event, keyboard::Keycode, EventPump};
|
||||||
|
|
||||||
use crate::{entity::direction::Direction, game::events::GameEvent, input::commands::GameCommand};
|
use crate::{entity::direction::Direction, events::GameEvent};
|
||||||
|
|
||||||
pub mod commands;
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum GameCommand {
|
||||||
|
MovePlayer(Direction),
|
||||||
|
Exit,
|
||||||
|
TogglePause,
|
||||||
|
ToggleDebug,
|
||||||
|
MuteAudio,
|
||||||
|
ResetLevel,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Resource)]
|
#[derive(Debug, Clone, Resource)]
|
||||||
pub struct Bindings {
|
pub struct Bindings {
|
||||||
@@ -42,7 +50,7 @@ impl Default for Bindings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>, mut pump: NonSendMut<&'static mut EventPump>) {
|
pub fn input_system(bindings: Res<Bindings>, mut writer: EventWriter<GameEvent>, mut pump: NonSendMut<&'static mut EventPump>) {
|
||||||
for event in pump.poll_iter() {
|
for event in pump.poll_iter() {
|
||||||
match event {
|
match event {
|
||||||
Event::Quit { .. } => {
|
Event::Quit { .. } => {
|
||||||
10
src/systems/mod.rs
Normal file
10
src/systems/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//! The Entity-Component-System (ECS) module.
|
||||||
|
//!
|
||||||
|
//! This module contains all the ECS-related logic, including components, systems,
|
||||||
|
//! and resources.
|
||||||
|
|
||||||
|
pub mod components;
|
||||||
|
pub mod control;
|
||||||
|
pub mod input;
|
||||||
|
pub mod movement;
|
||||||
|
pub mod render;
|
||||||
133
src/systems/movement.rs
Normal file
133
src/systems/movement.rs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
use crate::entity::graph::{Edge, EdgePermissions, Graph};
|
||||||
|
use crate::error::{EntityError, GameError};
|
||||||
|
use crate::map::builder::Map;
|
||||||
|
use crate::systems::components::{DeltaTime, EntityType, Position, Velocity};
|
||||||
|
use bevy_ecs::event::EventWriter;
|
||||||
|
use bevy_ecs::system::{Query, Res};
|
||||||
|
|
||||||
|
fn can_traverse(entity_type: EntityType, edge: Edge) -> bool {
|
||||||
|
match entity_type {
|
||||||
|
EntityType::Player => matches!(edge.permissions, EdgePermissions::All),
|
||||||
|
EntityType::Ghost => matches!(edge.permissions, EdgePermissions::All | EdgePermissions::GhostsOnly),
|
||||||
|
_ => matches!(edge.permissions, EdgePermissions::All),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn movement_system(
|
||||||
|
map: Res<Map>,
|
||||||
|
delta_time: Res<DeltaTime>,
|
||||||
|
mut entities: Query<(&mut Velocity, &mut Position, &EntityType)>,
|
||||||
|
mut errors: EventWriter<GameError>,
|
||||||
|
) {
|
||||||
|
for (mut velocity, mut position, entity_type) in entities.iter_mut() {
|
||||||
|
let distance = velocity.speed * 60.0 * delta_time.0;
|
||||||
|
|
||||||
|
// Decrement the remaining frames for the next direction
|
||||||
|
if let Some((direction, remaining)) = velocity.next_direction {
|
||||||
|
if remaining > 0 {
|
||||||
|
velocity.next_direction = Some((direction, remaining - 1));
|
||||||
|
} else {
|
||||||
|
velocity.next_direction = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match *position {
|
||||||
|
Position::AtNode(node_id) => {
|
||||||
|
// We're not moving, but a buffered direction is available.
|
||||||
|
if let Some((next_direction, _)) = velocity.next_direction {
|
||||||
|
if let Some(edge) = map.graph.find_edge_in_direction(node_id, next_direction) {
|
||||||
|
if can_traverse(*entity_type, edge) {
|
||||||
|
// Start moving in that direction
|
||||||
|
*position = Position::BetweenNodes {
|
||||||
|
from: node_id,
|
||||||
|
to: edge.target,
|
||||||
|
traversed: distance,
|
||||||
|
};
|
||||||
|
velocity.direction = next_direction;
|
||||||
|
velocity.next_direction = None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.write(
|
||||||
|
EntityError::InvalidMovement(format!(
|
||||||
|
"No edge found in direction {:?} from node {}",
|
||||||
|
next_direction, node_id
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Position::BetweenNodes { from, to, traversed } => {
|
||||||
|
// There is no point in any of the next logic if we don't travel at all
|
||||||
|
if distance <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let edge = map
|
||||||
|
.graph
|
||||||
|
.find_edge(from, to)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
errors.write(
|
||||||
|
EntityError::InvalidMovement(format!(
|
||||||
|
"Inconsistent state: Traverser is on a non-existent edge from {} to {}.",
|
||||||
|
from, to
|
||||||
|
))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let new_traversed = traversed + distance;
|
||||||
|
|
||||||
|
if new_traversed < edge.distance {
|
||||||
|
// Still on the same edge, just update the distance.
|
||||||
|
*position = Position::BetweenNodes {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
traversed: new_traversed,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let overflow = new_traversed - edge.distance;
|
||||||
|
let mut moved = false;
|
||||||
|
|
||||||
|
// If we buffered a direction, try to find an edge in that direction
|
||||||
|
if let Some((next_dir, _)) = velocity.next_direction {
|
||||||
|
if let Some(edge) = map.graph.find_edge_in_direction(to, next_dir) {
|
||||||
|
if can_traverse(*entity_type, edge) {
|
||||||
|
*position = Position::BetweenNodes {
|
||||||
|
from: to,
|
||||||
|
to: edge.target,
|
||||||
|
traversed: overflow,
|
||||||
|
};
|
||||||
|
|
||||||
|
velocity.direction = next_dir; // Remember our new direction
|
||||||
|
velocity.next_direction = None; // Consume the buffered direction
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't move, try to continue in the current direction
|
||||||
|
if !moved {
|
||||||
|
if let Some(edge) = map.graph.find_edge_in_direction(to, velocity.direction) {
|
||||||
|
if can_traverse(*entity_type, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::ecs::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity};
|
use crate::error::{GameError, TextureError};
|
||||||
use crate::error::{EntityError, GameError, TextureError};
|
|
||||||
use crate::map::builder::Map;
|
use crate::map::builder::Map;
|
||||||
|
use crate::systems::components::{DeltaTime, DirectionalAnimated, Position, Renderable, Velocity};
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
use bevy_ecs::entity::Entity;
|
use bevy_ecs::entity::Entity;
|
||||||
use bevy_ecs::event::EventWriter;
|
use bevy_ecs::event::EventWriter;
|
||||||
@@ -9,20 +9,25 @@ use sdl2::render::{Canvas, Texture};
|
|||||||
use sdl2::video::Window;
|
use sdl2::video::Window;
|
||||||
|
|
||||||
/// Updates the directional animated texture of an entity.
|
/// Updates the directional animated texture of an entity.
|
||||||
|
///
|
||||||
|
/// This runs before the render system so it can update the sprite based on the current direction of travel, as well as whether the entity is moving.
|
||||||
pub fn directional_render_system(
|
pub fn directional_render_system(
|
||||||
dt: Res<DeltaTime>,
|
dt: Res<DeltaTime>,
|
||||||
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable)>,
|
mut renderables: Query<(&Velocity, &mut DirectionalAnimated, &mut Renderable, &Position)>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
for (velocity, mut texture, mut renderable) in renderables.iter_mut() {
|
for (velocity, mut texture, mut renderable, position) in renderables.iter_mut() {
|
||||||
let texture = if velocity.speed.is_none() {
|
let stopped = matches!(position, Position::AtNode(_));
|
||||||
|
let texture = if stopped {
|
||||||
texture.stopped_textures[velocity.direction.as_usize()].as_mut()
|
texture.stopped_textures[velocity.direction.as_usize()].as_mut()
|
||||||
} else {
|
} else {
|
||||||
texture.textures[velocity.direction.as_usize()].as_mut()
|
texture.textures[velocity.direction.as_usize()].as_mut()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(texture) = texture {
|
if let Some(texture) = texture {
|
||||||
texture.tick(dt.0);
|
if !stopped {
|
||||||
|
texture.tick(dt.0);
|
||||||
|
}
|
||||||
renderable.sprite = *texture.current_tile();
|
renderable.sprite = *texture.current_tile();
|
||||||
} else {
|
} else {
|
||||||
errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into());
|
errors.write(TextureError::RenderFailed(format!("Entity has no texture")).into());
|
||||||
@@ -31,7 +36,10 @@ pub fn directional_render_system(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A non-send resource for the map texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource.
|
||||||
pub struct MapTextureResource(pub Texture<'static>);
|
pub struct MapTextureResource(pub Texture<'static>);
|
||||||
|
|
||||||
|
/// A non-send resource for the backbuffer texture. This just wraps the texture with a type so it can be differentiated when exposed as a resource.
|
||||||
pub struct BackbufferResource(pub Texture<'static>);
|
pub struct BackbufferResource(pub Texture<'static>);
|
||||||
|
|
||||||
pub fn render_system(
|
pub fn render_system(
|
||||||
@@ -40,7 +48,7 @@ pub fn render_system(
|
|||||||
mut backbuffer: NonSendMut<BackbufferResource>,
|
mut backbuffer: NonSendMut<BackbufferResource>,
|
||||||
mut atlas: NonSendMut<SpriteAtlas>,
|
mut atlas: NonSendMut<SpriteAtlas>,
|
||||||
map: Res<Map>,
|
map: Res<Map>,
|
||||||
mut renderables: Query<(Entity, &mut Renderable, &Position)>,
|
renderables: Query<(Entity, &mut Renderable, &Position)>,
|
||||||
mut errors: EventWriter<GameError>,
|
mut errors: EventWriter<GameError>,
|
||||||
) {
|
) {
|
||||||
// Clear the main canvas first
|
// Clear the main canvas first
|
||||||
@@ -61,7 +69,11 @@ pub fn render_system(
|
|||||||
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
.map(|e| errors.write(TextureError::RenderFailed(e.to_string()).into()));
|
||||||
|
|
||||||
// Render all entities to the backbuffer
|
// Render all entities to the backbuffer
|
||||||
for (_, mut renderable, position) in renderables.iter_mut() {
|
for (_, mut renderable, position) in renderables
|
||||||
|
// .iter_mut()
|
||||||
|
// .sort_by_key::<&mut Renderable, _, _>(|(renderable, renderable, renderable)| renderable.layer)
|
||||||
|
// .collect()
|
||||||
|
{
|
||||||
let pos = position.get_pixel_pos(&map.graph);
|
let pos = position.get_pixel_pos(&map.graph);
|
||||||
match pos {
|
match pos {
|
||||||
Ok(pos) => {
|
Ok(pos) => {
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
use sdl2::rect::Rect;
|
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
|
||||||
|
|
||||||
use crate::error::{AnimatedTextureError, GameError, GameResult, TextureError};
|
use crate::error::{AnimatedTextureError, GameError, GameResult, TextureError};
|
||||||
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
use crate::texture::sprite::AtlasTile;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AnimatedTexture {
|
pub struct AnimatedTexture {
|
||||||
@@ -40,12 +37,6 @@ impl AnimatedTexture {
|
|||||||
&self.tiles[self.current_frame]
|
&self.tiles[self.current_frame]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, dest: Rect) -> GameResult<()> {
|
|
||||||
let mut tile = *self.current_tile();
|
|
||||||
tile.render(canvas, atlas, dest)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the current frame index.
|
/// Returns the current frame index.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn current_frame(&self) -> usize {
|
pub fn current_frame(&self) -> usize {
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
use sdl2::rect::Rect;
|
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
|
||||||
|
|
||||||
use crate::entity::direction::Direction;
|
|
||||||
use crate::error::GameResult;
|
|
||||||
use crate::texture::animated::AnimatedTexture;
|
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct DirectionalAnimatedTexture {
|
|
||||||
textures: [Option<AnimatedTexture>; 4],
|
|
||||||
stopped_textures: [Option<AnimatedTexture>; 4],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DirectionalAnimatedTexture {
|
|
||||||
pub fn new(textures: [Option<AnimatedTexture>; 4], stopped_textures: [Option<AnimatedTexture>; 4]) -> Self {
|
|
||||||
Self {
|
|
||||||
textures,
|
|
||||||
stopped_textures,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&mut self, dt: f32) {
|
|
||||||
for texture in self.textures.iter_mut().flatten() {
|
|
||||||
texture.tick(dt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render<T: RenderTarget>(
|
|
||||||
&self,
|
|
||||||
canvas: &mut Canvas<T>,
|
|
||||||
atlas: &mut SpriteAtlas,
|
|
||||||
dest: Rect,
|
|
||||||
direction: Direction,
|
|
||||||
) -> GameResult<()> {
|
|
||||||
if let Some(texture) = &self.textures[direction.as_usize()] {
|
|
||||||
texture.render(canvas, atlas, dest)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_stopped<T: RenderTarget>(
|
|
||||||
&self,
|
|
||||||
canvas: &mut Canvas<T>,
|
|
||||||
atlas: &mut SpriteAtlas,
|
|
||||||
dest: Rect,
|
|
||||||
direction: Direction,
|
|
||||||
) -> GameResult<()> {
|
|
||||||
if let Some(texture) = &self.stopped_textures[direction.as_usize()] {
|
|
||||||
texture.render(canvas, atlas, dest)
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the texture has a direction.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn has_direction(&self, direction: Direction) -> bool {
|
|
||||||
self.textures[direction.as_usize()].is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the texture has a stopped direction.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn has_stopped_direction(&self, direction: Direction) -> bool {
|
|
||||||
self.stopped_textures[direction.as_usize()].is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of textures.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn texture_count(&self) -> usize {
|
|
||||||
self.textures.iter().filter(|t| t.is_some()).count()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of stopped textures.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn stopped_texture_count(&self) -> usize {
|
|
||||||
self.stopped_textures.iter().filter(|t| t.is_some()).count()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod animated;
|
pub mod animated;
|
||||||
pub mod blinking;
|
pub mod blinking;
|
||||||
pub mod directional;
|
|
||||||
pub mod sprite;
|
pub mod sprite;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bevy_ecs::resource::Resource;
|
|
||||||
use glam::U16Vec2;
|
use glam::U16Vec2;
|
||||||
use sdl2::pixels::Color;
|
use sdl2::pixels::Color;
|
||||||
use sdl2::rect::Rect;
|
use sdl2::rect::Rect;
|
||||||
@@ -9,33 +8,6 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use crate::error::TextureError;
|
use crate::error::TextureError;
|
||||||
|
|
||||||
/// A simple sprite for stationary items like pellets and energizers.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Sprite {
|
|
||||||
pub atlas_tile: AtlasTile,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sprite {
|
|
||||||
pub fn new(atlas_tile: AtlasTile) -> Self {
|
|
||||||
Self { atlas_tile }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render<C: RenderTarget>(
|
|
||||||
&self,
|
|
||||||
canvas: &mut Canvas<C>,
|
|
||||||
atlas: &mut SpriteAtlas,
|
|
||||||
position: glam::Vec2,
|
|
||||||
) -> Result<(), TextureError> {
|
|
||||||
let dest = crate::helpers::centered_with_size(
|
|
||||||
glam::IVec2::new(position.x as i32, position.y as i32),
|
|
||||||
glam::UVec2::new(self.atlas_tile.size.x as u32, self.atlas_tile.size.y as u32),
|
|
||||||
);
|
|
||||||
let mut tile = self.atlas_tile;
|
|
||||||
tile.render(canvas, atlas, dest)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct AtlasMapper {
|
pub struct AtlasMapper {
|
||||||
pub frames: HashMap<String, MapperFrame>,
|
pub frames: HashMap<String, MapperFrame>,
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
use pacman::entity::collision::{Collidable, CollisionSystem};
|
|
||||||
use pacman::entity::traversal::Position;
|
|
||||||
|
|
||||||
struct MockCollidable {
|
|
||||||
pos: Position,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collidable for MockCollidable {
|
|
||||||
fn position(&self) -> Position {
|
|
||||||
self.pos
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_colliding_with() {
|
|
||||||
let entity1 = MockCollidable {
|
|
||||||
pos: Position::AtNode(1),
|
|
||||||
};
|
|
||||||
let entity2 = MockCollidable {
|
|
||||||
pos: Position::AtNode(1),
|
|
||||||
};
|
|
||||||
let entity3 = MockCollidable {
|
|
||||||
pos: Position::AtNode(2),
|
|
||||||
};
|
|
||||||
let entity4 = MockCollidable {
|
|
||||||
pos: Position::BetweenNodes {
|
|
||||||
from: 1,
|
|
||||||
to: 2,
|
|
||||||
traversed: 0.5,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(entity1.is_colliding_with(&entity2));
|
|
||||||
assert!(!entity1.is_colliding_with(&entity3));
|
|
||||||
assert!(entity1.is_colliding_with(&entity4));
|
|
||||||
assert!(entity3.is_colliding_with(&entity4));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_collision_system_register_and_query() {
|
|
||||||
let mut collision_system = CollisionSystem::default();
|
|
||||||
|
|
||||||
let pos1 = Position::AtNode(1);
|
|
||||||
let entity1 = collision_system.register_entity(pos1);
|
|
||||||
|
|
||||||
let pos2 = Position::BetweenNodes {
|
|
||||||
from: 1,
|
|
||||||
to: 2,
|
|
||||||
traversed: 0.5,
|
|
||||||
};
|
|
||||||
let entity2 = collision_system.register_entity(pos2);
|
|
||||||
|
|
||||||
let pos3 = Position::AtNode(3);
|
|
||||||
let entity3 = collision_system.register_entity(pos3);
|
|
||||||
|
|
||||||
// Test entities_at_node
|
|
||||||
assert_eq!(collision_system.entities_at_node(1), &[entity1, entity2]);
|
|
||||||
assert_eq!(collision_system.entities_at_node(2), &[entity2]);
|
|
||||||
assert_eq!(collision_system.entities_at_node(3), &[entity3]);
|
|
||||||
assert_eq!(collision_system.entities_at_node(4), &[] as &[u32]);
|
|
||||||
|
|
||||||
// Test potential_collisions
|
|
||||||
let mut collisions1 = collision_system.potential_collisions(&pos1);
|
|
||||||
collisions1.sort_unstable();
|
|
||||||
assert_eq!(collisions1, vec![entity1, entity2]);
|
|
||||||
|
|
||||||
let mut collisions2 = collision_system.potential_collisions(&pos2);
|
|
||||||
collisions2.sort_unstable();
|
|
||||||
assert_eq!(collisions2, vec![entity1, entity2]);
|
|
||||||
|
|
||||||
let mut collisions3 = collision_system.potential_collisions(&pos3);
|
|
||||||
collisions3.sort_unstable();
|
|
||||||
assert_eq!(collisions3, vec![entity3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_collision_system_update() {
|
|
||||||
let mut collision_system = CollisionSystem::default();
|
|
||||||
|
|
||||||
let entity1 = collision_system.register_entity(Position::AtNode(1));
|
|
||||||
|
|
||||||
assert_eq!(collision_system.entities_at_node(1), &[entity1]);
|
|
||||||
assert_eq!(collision_system.entities_at_node(2), &[] as &[u32]);
|
|
||||||
|
|
||||||
collision_system.update_position(entity1, Position::AtNode(2));
|
|
||||||
|
|
||||||
assert_eq!(collision_system.entities_at_node(1), &[] as &[u32]);
|
|
||||||
assert_eq!(collision_system.entities_at_node(2), &[entity1]);
|
|
||||||
|
|
||||||
collision_system.update_position(
|
|
||||||
entity1,
|
|
||||||
Position::BetweenNodes {
|
|
||||||
from: 2,
|
|
||||||
to: 3,
|
|
||||||
traversed: 0.1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(collision_system.entities_at_node(1), &[] as &[u32]);
|
|
||||||
assert_eq!(collision_system.entities_at_node(2), &[entity1]);
|
|
||||||
assert_eq!(collision_system.entities_at_node(3), &[entity1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_collision_system_remove() {
|
|
||||||
let mut collision_system = CollisionSystem::default();
|
|
||||||
|
|
||||||
let entity1 = collision_system.register_entity(Position::AtNode(1));
|
|
||||||
let entity2 = collision_system.register_entity(Position::AtNode(1));
|
|
||||||
|
|
||||||
assert_eq!(collision_system.entities_at_node(1), &[entity1, entity2]);
|
|
||||||
|
|
||||||
collision_system.remove_entity(entity1);
|
|
||||||
|
|
||||||
assert_eq!(collision_system.entities_at_node(1), &[entity2]);
|
|
||||||
|
|
||||||
collision_system.remove_entity(entity2);
|
|
||||||
assert_eq!(collision_system.entities_at_node(1), &[] as &[u32]);
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use pacman::{
|
use pacman::{
|
||||||
asset::{get_asset_bytes, Asset},
|
asset::{get_asset_bytes, Asset},
|
||||||
game::state::ATLAS_FRAMES,
|
game::ATLAS_FRAMES,
|
||||||
texture::sprite::{AtlasMapper, SpriteAtlas},
|
texture::sprite::{AtlasMapper, SpriteAtlas},
|
||||||
};
|
};
|
||||||
use sdl2::{
|
use sdl2::{
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
use glam::U16Vec2;
|
|
||||||
use pacman::entity::direction::Direction;
|
|
||||||
use pacman::texture::animated::AnimatedTexture;
|
|
||||||
use pacman::texture::directional::DirectionalAnimatedTexture;
|
|
||||||
use pacman::texture::sprite::AtlasTile;
|
|
||||||
use sdl2::pixels::Color;
|
|
||||||
|
|
||||||
fn mock_atlas_tile(id: u32) -> AtlasTile {
|
|
||||||
AtlasTile {
|
|
||||||
pos: U16Vec2::new(0, 0),
|
|
||||||
size: U16Vec2::new(16, 16),
|
|
||||||
color: Some(Color::RGB(id as u8, 0, 0)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mock_animated_texture(id: u32) -> AnimatedTexture {
|
|
||||||
AnimatedTexture::new(vec![mock_atlas_tile(id)], 0.1).expect("Invalid frame duration")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_directional_texture_partial_directions() {
|
|
||||||
let mut textures = [None, None, None, None];
|
|
||||||
textures[Direction::Up.as_usize()] = Some(mock_animated_texture(1));
|
|
||||||
|
|
||||||
let texture = DirectionalAnimatedTexture::new(textures, [None, None, None, None]);
|
|
||||||
|
|
||||||
assert_eq!(texture.texture_count(), 1);
|
|
||||||
assert!(texture.has_direction(Direction::Up));
|
|
||||||
assert!(!texture.has_direction(Direction::Down));
|
|
||||||
assert!(!texture.has_direction(Direction::Left));
|
|
||||||
assert!(!texture.has_direction(Direction::Right));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_directional_texture_all_directions() {
|
|
||||||
let mut textures = [None, None, None, None];
|
|
||||||
let directions = [
|
|
||||||
(Direction::Up, 1),
|
|
||||||
(Direction::Down, 2),
|
|
||||||
(Direction::Left, 3),
|
|
||||||
(Direction::Right, 4),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (direction, id) in directions {
|
|
||||||
textures[direction.as_usize()] = Some(mock_animated_texture(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let texture = DirectionalAnimatedTexture::new(textures, [None, None, None, None]);
|
|
||||||
|
|
||||||
assert_eq!(texture.texture_count(), 4);
|
|
||||||
for direction in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
|
|
||||||
assert!(texture.has_direction(*direction));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_directional_texture_stopped() {
|
|
||||||
let mut stopped_textures = [None, None, None, None];
|
|
||||||
stopped_textures[Direction::Up.as_usize()] = Some(mock_animated_texture(1));
|
|
||||||
|
|
||||||
let texture = DirectionalAnimatedTexture::new([None, None, None, None], stopped_textures);
|
|
||||||
|
|
||||||
assert_eq!(texture.stopped_texture_count(), 1);
|
|
||||||
assert!(texture.has_stopped_direction(Direction::Up));
|
|
||||||
assert!(!texture.has_stopped_direction(Direction::Down));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_directional_texture_tick() {
|
|
||||||
let mut textures = [None, None, None, None];
|
|
||||||
textures[Direction::Up.as_usize()] = Some(mock_animated_texture(1));
|
|
||||||
let mut texture = DirectionalAnimatedTexture::new(textures, [None, None, None, None]);
|
|
||||||
|
|
||||||
// This is a bit of a placeholder, since we can't inspect the inner state easily.
|
|
||||||
// We're just ensuring the tick method runs without panicking.
|
|
||||||
texture.tick(0.1);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
use pacman::constants::RAW_BOARD;
|
use pacman::constants::RAW_BOARD;
|
||||||
use pacman::map::builder::Map;
|
use pacman::map::builder::Map;
|
||||||
|
|
||||||
mod collision;
|
|
||||||
mod item;
|
mod item;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(ghost.ghost_type, GhostType::Blinky);
|
|
||||||
assert_eq!(ghost.traverser.position.from_node_id(), 0);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use pacman::entity::direction::Direction;
|
use pacman::entity::direction::Direction;
|
||||||
use pacman::entity::graph::{EdgePermissions, Graph, Node};
|
use pacman::entity::graph::{EdgePermissions, Graph, Node};
|
||||||
use pacman::entity::traversal::{Position, Traverser};
|
|
||||||
|
|
||||||
fn create_test_graph() -> Graph {
|
fn create_test_graph() -> Graph {
|
||||||
let mut graph = Graph::new();
|
let mut graph = Graph::new();
|
||||||
@@ -150,68 +149,3 @@ fn should_find_edge_between_nodes() {
|
|||||||
let non_existent_edge = graph.find_edge(0, 99);
|
let non_existent_edge = graph.find_edge(0, 99);
|
||||||
assert!(non_existent_edge.is_none());
|
assert!(non_existent_edge.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_traverser_basic() {
|
|
||||||
let graph = create_test_graph();
|
|
||||||
let mut traverser = Traverser::new(&graph, 0, Direction::Left, &|_| true);
|
|
||||||
|
|
||||||
traverser.set_next_direction(Direction::Up);
|
|
||||||
assert!(traverser.next_direction.is_some());
|
|
||||||
assert_eq!(traverser.next_direction.unwrap().0, Direction::Up);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_traverser_advance() {
|
|
||||||
let graph = create_test_graph();
|
|
||||||
let mut traverser = Traverser::new(&graph, 0, Direction::Right, &|_| true);
|
|
||||||
|
|
||||||
traverser.advance(&graph, 5.0, &|_| true).unwrap();
|
|
||||||
|
|
||||||
match traverser.position {
|
|
||||||
Position::BetweenNodes { from, to, traversed } => {
|
|
||||||
assert_eq!(from, 0);
|
|
||||||
assert_eq!(to, 1);
|
|
||||||
assert_eq!(traversed, 5.0);
|
|
||||||
}
|
|
||||||
_ => panic!("Expected to be between nodes"),
|
|
||||||
}
|
|
||||||
|
|
||||||
traverser.advance(&graph, 3.0, &|_| true).unwrap();
|
|
||||||
|
|
||||||
match traverser.position {
|
|
||||||
Position::BetweenNodes { from, to, traversed } => {
|
|
||||||
assert_eq!(from, 0);
|
|
||||||
assert_eq!(to, 1);
|
|
||||||
assert_eq!(traversed, 8.0);
|
|
||||||
}
|
|
||||||
_ => panic!("Expected to be between nodes"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_traverser_with_permissions() {
|
|
||||||
let mut graph = Graph::new();
|
|
||||||
let node1 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(0.0, 0.0),
|
|
||||||
});
|
|
||||||
let node2 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(16.0, 0.0),
|
|
||||||
});
|
|
||||||
|
|
||||||
graph
|
|
||||||
.add_edge(node1, node2, false, None, Direction::Right, EdgePermissions::GhostsOnly)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Pacman can't traverse ghost-only edges
|
|
||||||
let mut traverser = Traverser::new(&graph, node1, Direction::Right, &|edge| {
|
|
||||||
matches!(edge.permissions, EdgePermissions::All)
|
|
||||||
});
|
|
||||||
|
|
||||||
traverser
|
|
||||||
.advance(&graph, 5.0, &|edge| matches!(edge.permissions, EdgePermissions::All))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Should still be at the node since it can't traverse
|
|
||||||
assert!(traverser.position.is_at_node());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,53 +1,46 @@
|
|||||||
use glam::U16Vec2;
|
// use glam::U16Vec2;
|
||||||
use pacman::{
|
// use pacman::texture::sprite::{AtlasTile, Sprite};
|
||||||
entity::{
|
|
||||||
collision::Collidable,
|
|
||||||
item::{FruitKind, Item, ItemType},
|
|
||||||
},
|
|
||||||
texture::sprite::{AtlasTile, Sprite},
|
|
||||||
};
|
|
||||||
use strum::{EnumCount, IntoEnumIterator};
|
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_item_type_get_score() {
|
// fn test_item_type_get_score() {
|
||||||
assert_eq!(ItemType::Pellet.get_score(), 10);
|
// assert_eq!(ItemType::Pellet.get_score(), 10);
|
||||||
assert_eq!(ItemType::Energizer.get_score(), 50);
|
// assert_eq!(ItemType::Energizer.get_score(), 50);
|
||||||
|
|
||||||
let fruit = ItemType::Fruit { kind: FruitKind::Apple };
|
// let fruit = ItemType::Fruit { kind: FruitKind::Apple };
|
||||||
assert_eq!(fruit.get_score(), 100);
|
// assert_eq!(fruit.get_score(), 100);
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_fruit_kind_increasing_score() {
|
// fn test_fruit_kind_increasing_score() {
|
||||||
// Build a list of fruit kinds, sorted by their index
|
// // Build a list of fruit kinds, sorted by their index
|
||||||
let mut kinds = FruitKind::iter()
|
// let mut kinds = FruitKind::iter()
|
||||||
.map(|kind| (kind.index(), kind.get_score()))
|
// .map(|kind| (kind.index(), kind.get_score()))
|
||||||
.collect::<Vec<_>>();
|
// .collect::<Vec<_>>();
|
||||||
kinds.sort_unstable_by_key(|(index, _)| *index);
|
// kinds.sort_unstable_by_key(|(index, _)| *index);
|
||||||
|
|
||||||
assert_eq!(kinds.len(), FruitKind::COUNT);
|
// assert_eq!(kinds.len(), FruitKind::COUNT);
|
||||||
|
|
||||||
// Check that the score increases as expected
|
// // Check that the score increases as expected
|
||||||
for window in kinds.windows(2) {
|
// for window in kinds.windows(2) {
|
||||||
let ((_, prev), (_, next)) = (window[0], window[1]);
|
// let ((_, prev), (_, next)) = (window[0], window[1]);
|
||||||
assert!(prev < next, "Fruits should have increasing scores, but {prev:?} < {next:?}");
|
// assert!(prev < next, "Fruits should have increasing scores, but {prev:?} < {next:?}");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_item_creation_and_collection() {
|
// fn test_item_creation_and_collection() {
|
||||||
let atlas_tile = AtlasTile {
|
// let atlas_tile = AtlasTile {
|
||||||
pos: U16Vec2::new(0, 0),
|
// pos: U16Vec2::new(0, 0),
|
||||||
size: U16Vec2::new(16, 16),
|
// size: U16Vec2::new(16, 16),
|
||||||
color: None,
|
// color: None,
|
||||||
};
|
// };
|
||||||
let sprite = Sprite::new(atlas_tile);
|
// let sprite = Sprite::new(atlas_tile);
|
||||||
let mut item = Item::new(0, ItemType::Pellet, sprite);
|
// let mut item = Item::new(0, ItemType::Pellet, sprite);
|
||||||
|
|
||||||
assert!(!item.is_collected());
|
// assert!(!item.is_collected());
|
||||||
assert_eq!(item.get_score(), 10);
|
// assert_eq!(item.get_score(), 10);
|
||||||
assert_eq!(item.position().from_node_id(), 0);
|
// assert_eq!(item.position().from_node_id(), 0);
|
||||||
|
|
||||||
item.collect();
|
// item.collect();
|
||||||
assert!(item.is_collected());
|
// assert!(item.is_collected());
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use pacman::constants::{CELL_SIZE, RAW_BOARD};
|
use pacman::constants::{CELL_SIZE, RAW_BOARD};
|
||||||
use pacman::map::builder::Map;
|
use pacman::map::builder::Map;
|
||||||
use sdl2::render::Texture;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_map_creation() {
|
fn test_map_creation() {
|
||||||
@@ -34,60 +33,60 @@ fn test_map_node_positions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_generate_items() {
|
// fn test_generate_items() {
|
||||||
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
// use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
||||||
use std::collections::HashMap;
|
// use std::collections::HashMap;
|
||||||
|
|
||||||
let map = Map::new(RAW_BOARD).unwrap();
|
// let map = Map::new(RAW_BOARD).unwrap();
|
||||||
|
|
||||||
// Create a minimal atlas for testing
|
// // Create a minimal atlas for testing
|
||||||
let mut frames = HashMap::new();
|
// let mut frames = HashMap::new();
|
||||||
frames.insert(
|
// frames.insert(
|
||||||
"maze/pellet.png".to_string(),
|
// "maze/pellet.png".to_string(),
|
||||||
MapperFrame {
|
// MapperFrame {
|
||||||
x: 0,
|
// x: 0,
|
||||||
y: 0,
|
// y: 0,
|
||||||
width: 8,
|
// width: 8,
|
||||||
height: 8,
|
// height: 8,
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
frames.insert(
|
// frames.insert(
|
||||||
"maze/energizer.png".to_string(),
|
// "maze/energizer.png".to_string(),
|
||||||
MapperFrame {
|
// MapperFrame {
|
||||||
x: 8,
|
// x: 8,
|
||||||
y: 0,
|
// y: 0,
|
||||||
width: 8,
|
// width: 8,
|
||||||
height: 8,
|
// height: 8,
|
||||||
},
|
// },
|
||||||
);
|
// );
|
||||||
|
|
||||||
let mapper = AtlasMapper { frames };
|
// let mapper = AtlasMapper { frames };
|
||||||
let texture = unsafe { std::mem::transmute::<usize, Texture<'static>>(0usize) };
|
// let texture = unsafe { std::mem::transmute::<usize, Texture<'static>>(0usize) };
|
||||||
let atlas = SpriteAtlas::new(texture, mapper);
|
// let atlas = SpriteAtlas::new(texture, mapper);
|
||||||
|
|
||||||
let items = map.generate_items(&atlas).unwrap();
|
// let items = map.generate_items(&atlas).unwrap();
|
||||||
|
|
||||||
// Verify we have items
|
// // Verify we have items
|
||||||
assert!(!items.is_empty());
|
// assert!(!items.is_empty());
|
||||||
|
|
||||||
// Count different types
|
// // Count different types
|
||||||
let pellet_count = items
|
// let pellet_count = items
|
||||||
.iter()
|
// .iter()
|
||||||
.filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Pellet))
|
// .filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Pellet))
|
||||||
.count();
|
// .count();
|
||||||
let energizer_count = items
|
// let energizer_count = items
|
||||||
.iter()
|
// .iter()
|
||||||
.filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Energizer))
|
// .filter(|item| matches!(item.item_type, pacman::entity::item::ItemType::Energizer))
|
||||||
.count();
|
// .count();
|
||||||
|
|
||||||
// Should have both types
|
// // Should have both types
|
||||||
assert_eq!(pellet_count, 240);
|
// assert_eq!(pellet_count, 240);
|
||||||
assert_eq!(energizer_count, 4);
|
// assert_eq!(energizer_count, 4);
|
||||||
|
|
||||||
// All items should be uncollected initially
|
// // All items should be uncollected initially
|
||||||
assert!(items.iter().all(|item| !item.is_collected()));
|
// assert!(items.iter().all(|item| !item.is_collected()));
|
||||||
|
|
||||||
// All items should have valid node indices
|
// // All items should have valid node indices
|
||||||
assert!(items.iter().all(|item| item.node_index < map.graph.node_count()));
|
// assert!(items.iter().all(|item| item.node_index < map.graph.node_count()));
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
use pacman::entity::direction::Direction;
|
|
||||||
use pacman::entity::graph::{Graph, Node};
|
|
||||||
use pacman::entity::pacman::Pacman;
|
|
||||||
use pacman::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
fn create_test_graph() -> Graph {
|
|
||||||
let mut graph = Graph::new();
|
|
||||||
let node1 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(0.0, 0.0),
|
|
||||||
});
|
|
||||||
let node2 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(16.0, 0.0),
|
|
||||||
});
|
|
||||||
let node3 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(0.0, 16.0),
|
|
||||||
});
|
|
||||||
|
|
||||||
graph.connect(node1, node2, false, None, Direction::Right).unwrap();
|
|
||||||
graph.connect(node1, node3, false, None, Direction::Down).unwrap();
|
|
||||||
|
|
||||||
graph
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_test_atlas() -> SpriteAtlas {
|
|
||||||
let mut frames = HashMap::new();
|
|
||||||
let directions = ["up", "down", "left", "right"];
|
|
||||||
|
|
||||||
for (i, dir) in directions.iter().enumerate() {
|
|
||||||
frames.insert(
|
|
||||||
format!("pacman/{dir}_a.png"),
|
|
||||||
MapperFrame {
|
|
||||||
x: i as u16 * 16,
|
|
||||||
y: 0,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
frames.insert(
|
|
||||||
format!("pacman/{dir}_b.png"),
|
|
||||||
MapperFrame {
|
|
||||||
x: i as u16 * 16,
|
|
||||||
y: 16,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
frames.insert(
|
|
||||||
"pacman/full.png".to_string(),
|
|
||||||
MapperFrame {
|
|
||||||
x: 64,
|
|
||||||
y: 0,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mapper = AtlasMapper { frames };
|
|
||||||
let dummy_texture = unsafe { std::mem::zeroed() };
|
|
||||||
SpriteAtlas::new(dummy_texture, mapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_pacman_creation() {
|
|
||||||
let graph = create_test_graph();
|
|
||||||
let atlas = create_test_atlas();
|
|
||||||
let pacman = Pacman::new(&graph, 0, &atlas).unwrap();
|
|
||||||
|
|
||||||
assert!(pacman.traverser.position.is_at_node());
|
|
||||||
assert_eq!(pacman.traverser.direction, Direction::Left);
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
use pacman::entity::direction::Direction;
|
|
||||||
use pacman::entity::ghost::{Ghost, GhostType};
|
|
||||||
use pacman::entity::graph::{Graph, Node};
|
|
||||||
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_pathfinding() {
|
|
||||||
// Create a simple test graph
|
|
||||||
let mut graph = Graph::new();
|
|
||||||
|
|
||||||
// Add nodes in a simple line: 0 -> 1 -> 2
|
|
||||||
let node0 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(0.0, 0.0),
|
|
||||||
});
|
|
||||||
let node1 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(10.0, 0.0),
|
|
||||||
});
|
|
||||||
let node2 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(20.0, 0.0),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect the nodes
|
|
||||||
graph.connect(node0, node1, false, None, Direction::Right).unwrap();
|
|
||||||
graph.connect(node1, node2, false, None, Direction::Right).unwrap();
|
|
||||||
|
|
||||||
// Create a test atlas for the ghost
|
|
||||||
let atlas = create_test_atlas();
|
|
||||||
|
|
||||||
// Create a ghost at node 0
|
|
||||||
let ghost = Ghost::new(&graph, node0, GhostType::Blinky, &atlas).unwrap();
|
|
||||||
|
|
||||||
// Test pathfinding from node 0 to node 2
|
|
||||||
let path = ghost.calculate_path_to_target(&graph, node2);
|
|
||||||
|
|
||||||
assert!(path.is_ok());
|
|
||||||
let path = path.unwrap();
|
|
||||||
assert!(
|
|
||||||
path == vec![node0, node1, node2] || path == vec![node2, node1, node0],
|
|
||||||
"Path was not what was expected"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ghost_pathfinding_no_path() {
|
|
||||||
// Create a test graph with disconnected components
|
|
||||||
let mut graph = Graph::new();
|
|
||||||
|
|
||||||
let node0 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(0.0, 0.0),
|
|
||||||
});
|
|
||||||
let node1 = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(10.0, 0.0),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Don't connect the nodes
|
|
||||||
let atlas = create_test_atlas();
|
|
||||||
let ghost = Ghost::new(&graph, node0, GhostType::Blinky, &atlas).unwrap();
|
|
||||||
|
|
||||||
// Test pathfinding when no path exists
|
|
||||||
let path = ghost.calculate_path_to_target(&graph, node1);
|
|
||||||
|
|
||||||
assert!(path.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ghost_debug_colors() {
|
|
||||||
let atlas = create_test_atlas();
|
|
||||||
let mut graph = Graph::new();
|
|
||||||
let node = graph.add_node(Node {
|
|
||||||
position: glam::Vec2::new(0.0, 0.0),
|
|
||||||
});
|
|
||||||
|
|
||||||
let blinky = Ghost::new(&graph, node, GhostType::Blinky, &atlas).unwrap();
|
|
||||||
let pinky = Ghost::new(&graph, node, GhostType::Pinky, &atlas).unwrap();
|
|
||||||
let inky = Ghost::new(&graph, node, GhostType::Inky, &atlas).unwrap();
|
|
||||||
let clyde = Ghost::new(&graph, node, GhostType::Clyde, &atlas).unwrap();
|
|
||||||
|
|
||||||
// Test that each ghost has a different debug color
|
|
||||||
let colors = std::collections::HashSet::from([
|
|
||||||
blinky.debug_color(),
|
|
||||||
pinky.debug_color(),
|
|
||||||
inky.debug_color(),
|
|
||||||
clyde.debug_color(),
|
|
||||||
]);
|
|
||||||
assert_eq!(colors.len(), 4, "All ghost colors should be unique");
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use glam::U16Vec2;
|
use glam::U16Vec2;
|
||||||
use pacman::texture::sprite::{AtlasMapper, AtlasTile, MapperFrame, Sprite, SpriteAtlas};
|
use pacman::texture::sprite::{AtlasMapper, AtlasTile, MapperFrame, SpriteAtlas};
|
||||||
use sdl2::pixels::Color;
|
use sdl2::pixels::Color;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -92,12 +92,3 @@ fn test_atlas_tile_new_and_with_color() {
|
|||||||
let tile_with_color = tile.with_color(color);
|
let tile_with_color = tile.with_color(color);
|
||||||
assert_eq!(tile_with_color.color, Some(color));
|
assert_eq!(tile_with_color.color, Some(color));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sprite_new() {
|
|
||||||
let atlas_tile = AtlasTile::new(U16Vec2::new(0, 0), U16Vec2::new(16, 16), None);
|
|
||||||
let sprite = Sprite::new(atlas_tile);
|
|
||||||
|
|
||||||
assert_eq!(sprite.atlas_tile.pos, atlas_tile.pos);
|
|
||||||
assert_eq!(sprite.atlas_tile.size, atlas_tile.size);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user