mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-08 10:07:51 -06:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33672d8d5a | |||
| 1dc8aca373 | |||
| 02089a78da | |||
| 1f8e7c6d71 |
8
.github/workflows/build.yaml
vendored
8
.github/workflows/build.yaml
vendored
@@ -14,19 +14,19 @@ jobs:
|
|||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
artifact_name: pacman
|
artifact_name: pacman
|
||||||
toolchain: 1.88.0
|
toolchain: 1.86.0
|
||||||
- os: macos-13
|
- os: macos-13
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
artifact_name: pacman
|
artifact_name: pacman
|
||||||
toolchain: 1.88.0
|
toolchain: 1.86.0
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
artifact_name: pacman
|
artifact_name: pacman
|
||||||
toolchain: 1.88.0
|
toolchain: 1.86.0
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: x86_64-pc-windows-gnu
|
target: x86_64-pc-windows-gnu
|
||||||
artifact_name: pacman.exe
|
artifact_name: pacman.exe
|
||||||
toolchain: 1.88.0
|
toolchain: 1.86.0
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@@ -4,7 +4,7 @@ on: ["push", "pull_request"]
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_TOOLCHAIN: 1.88.0
|
RUST_TOOLCHAIN: 1.86.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.86.0"
|
||||||
128
src/entity/collision.rs
Normal file
128
src/entity/collision.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::entity::traversal::Position;
|
||||||
|
|
||||||
|
/// Trait for entities that can participate in collision detection.
|
||||||
|
pub trait Collidable {
|
||||||
|
/// Returns the current position of this entity.
|
||||||
|
fn position(&self) -> Position;
|
||||||
|
|
||||||
|
/// Checks if this entity is colliding with another entity.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn is_colliding_with(&self, other: &dyn Collidable) -> bool {
|
||||||
|
positions_overlap(&self.position(), &other.position())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// System for tracking entities by their positions for efficient collision detection.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CollisionSystem {
|
||||||
|
/// Maps node IDs to lists of entity IDs that are at that node
|
||||||
|
node_entities: HashMap<usize, Vec<EntityId>>,
|
||||||
|
/// Maps entity IDs to their current positions
|
||||||
|
entity_positions: HashMap<EntityId, Position>,
|
||||||
|
/// Next available entity ID
|
||||||
|
next_id: EntityId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unique identifier for an entity in the collision system
|
||||||
|
pub type EntityId = u32;
|
||||||
|
|
||||||
|
impl CollisionSystem {
|
||||||
|
/// Registers an entity with the collision system and returns its ID
|
||||||
|
pub fn register_entity(&mut self, position: Position) -> EntityId {
|
||||||
|
let id = self.next_id;
|
||||||
|
self.next_id += 1;
|
||||||
|
|
||||||
|
self.entity_positions.insert(id, position);
|
||||||
|
self.update_node_entities(id, position);
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates an entity's position
|
||||||
|
pub fn update_position(&mut self, entity_id: EntityId, new_position: Position) {
|
||||||
|
if let Some(old_position) = self.entity_positions.get(&entity_id) {
|
||||||
|
// Remove from old nodes
|
||||||
|
self.remove_from_nodes(entity_id, *old_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update position and add to new nodes
|
||||||
|
self.entity_positions.insert(entity_id, new_position);
|
||||||
|
self.update_node_entities(entity_id, new_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an entity from the collision system
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn remove_entity(&mut self, entity_id: EntityId) {
|
||||||
|
if let Some(position) = self.entity_positions.remove(&entity_id) {
|
||||||
|
self.remove_from_nodes(entity_id, position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all entity IDs at a specific node
|
||||||
|
pub fn entities_at_node(&self, node: usize) -> &[EntityId] {
|
||||||
|
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
|
||||||
|
pub fn potential_collisions(&self, position: &Position) -> Vec<EntityId> {
|
||||||
|
let mut collisions = Vec::new();
|
||||||
|
let nodes = get_nodes(position);
|
||||||
|
|
||||||
|
for node in nodes {
|
||||||
|
collisions.extend(self.entities_at_node(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates
|
||||||
|
collisions.sort_unstable();
|
||||||
|
collisions.dedup();
|
||||||
|
collisions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the node_entities map when an entity's position changes
|
||||||
|
fn update_node_entities(&mut self, entity_id: EntityId, position: Position) {
|
||||||
|
let nodes = get_nodes(&position);
|
||||||
|
for node in nodes {
|
||||||
|
self.node_entities.entry(node).or_default().push(entity_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes an entity from all nodes it was previously at
|
||||||
|
fn remove_from_nodes(&mut self, entity_id: EntityId, position: Position) {
|
||||||
|
let nodes = get_nodes(&position);
|
||||||
|
for node in nodes {
|
||||||
|
if let Some(entities) = self.node_entities.get_mut(&node) {
|
||||||
|
entities.retain(|&id| id != entity_id);
|
||||||
|
if entities.is_empty() {
|
||||||
|
self.node_entities.remove(&node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if two positions overlap (entities are at the same location).
|
||||||
|
fn positions_overlap(a: &Position, b: &Position) -> bool {
|
||||||
|
let a_nodes = get_nodes(a);
|
||||||
|
let b_nodes = get_nodes(b);
|
||||||
|
|
||||||
|
// Check if any nodes overlap
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all nodes that an entity is currently at or between.
|
||||||
|
fn get_nodes(pos: &Position) -> SmallVec<[usize; 2]> {
|
||||||
|
let mut nodes = SmallVec::new();
|
||||||
|
match pos {
|
||||||
|
Position::AtNode(node) => nodes.push(*node),
|
||||||
|
Position::BetweenNodes { from, to, .. } => {
|
||||||
|
nodes.push(*from);
|
||||||
|
nodes.push(*to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodes
|
||||||
|
}
|
||||||
@@ -7,11 +7,15 @@
|
|||||||
use pathfinding::prelude::dijkstra;
|
use pathfinding::prelude::dijkstra;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::{
|
||||||
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
|
collision::Collidable,
|
||||||
use crate::entity::r#trait::Entity;
|
direction::Direction,
|
||||||
use crate::entity::traversal::Traverser;
|
graph::{Edge, EdgePermissions, Graph, NodeId},
|
||||||
|
r#trait::Entity,
|
||||||
|
traversal::Traverser,
|
||||||
|
};
|
||||||
use crate::texture::animated::AnimatedTexture;
|
use crate::texture::animated::AnimatedTexture;
|
||||||
use crate::texture::directional::DirectionalAnimatedTexture;
|
use crate::texture::directional::DirectionalAnimatedTexture;
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
@@ -104,7 +108,7 @@ impl Entity for Ghost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
eprintln!("Ghost movement error: {}", e);
|
error!("Ghost movement error: {}", e);
|
||||||
}
|
}
|
||||||
self.texture.tick(dt);
|
self.texture.tick(dt);
|
||||||
}
|
}
|
||||||
@@ -244,3 +248,9 @@ impl Ghost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Collidable for Ghost {
|
||||||
|
fn position(&self) -> crate::entity::traversal::Position {
|
||||||
|
self.traverser.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
101
src/entity/item.rs
Normal file
101
src/entity/item.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
use crate::{
|
||||||
|
constants,
|
||||||
|
entity::{collision::Collidable, graph::Graph},
|
||||||
|
error::EntityError,
|
||||||
|
texture::sprite::{Sprite, SpriteAtlas},
|
||||||
|
};
|
||||||
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ItemType {
|
||||||
|
Pellet,
|
||||||
|
Energizer,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Fruit {
|
||||||
|
kind: FruitKind,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemType {
|
||||||
|
pub fn get_score(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
ItemType::Pellet => 10,
|
||||||
|
ItemType::Energizer => 50,
|
||||||
|
ItemType::Fruit { kind } => kind.get_score(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum FruitKind {
|
||||||
|
Apple,
|
||||||
|
Strawberry,
|
||||||
|
Orange,
|
||||||
|
Melon,
|
||||||
|
Bell,
|
||||||
|
Key,
|
||||||
|
Galaxian,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FruitKind {
|
||||||
|
pub fn get_score(self) -> u32 {
|
||||||
|
match self {
|
||||||
|
FruitKind::Apple => 100,
|
||||||
|
FruitKind::Strawberry => 300,
|
||||||
|
FruitKind::Orange => 500,
|
||||||
|
FruitKind::Melon => 700,
|
||||||
|
FruitKind::Bell => 1000,
|
||||||
|
FruitKind::Key => 2000,
|
||||||
|
FruitKind::Galaxian => 3000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Item {
|
||||||
|
pub node_index: usize,
|
||||||
|
pub item_type: ItemType,
|
||||||
|
pub sprite: Sprite,
|
||||||
|
pub collected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
pub fn new(node_index: usize, item_type: ItemType, sprite: Sprite) -> Self {
|
||||||
|
Self {
|
||||||
|
node_index,
|
||||||
|
item_type,
|
||||||
|
sprite,
|
||||||
|
collected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_collected(&self) -> bool {
|
||||||
|
self.collected
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect(&mut self) {
|
||||||
|
self.collected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_score(&self) -> u32 {
|
||||||
|
self.item_type.get_score()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) -> anyhow::Result<()> {
|
||||||
|
if !self.collected {
|
||||||
|
let node = graph
|
||||||
|
.get_node(self.node_index)
|
||||||
|
.ok_or(EntityError::NodeNotFound(self.node_index))?;
|
||||||
|
let position = node.position + constants::BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
self.sprite.render(canvas, atlas, position)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collidable for Item {
|
||||||
|
fn position(&self) -> crate::entity::traversal::Position {
|
||||||
|
crate::entity::traversal::Position::AtNode(self.node_index)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
pub mod collision;
|
||||||
pub mod direction;
|
pub mod direction;
|
||||||
pub mod ghost;
|
pub mod ghost;
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
pub mod item;
|
||||||
pub mod pacman;
|
pub mod pacman;
|
||||||
pub mod r#trait;
|
pub mod r#trait;
|
||||||
pub mod traversal;
|
pub mod traversal;
|
||||||
|
|||||||
@@ -4,14 +4,18 @@
|
|||||||
//! 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::direction::Direction;
|
use crate::entity::{
|
||||||
use crate::entity::graph::{Edge, EdgePermissions, Graph, NodeId};
|
collision::Collidable,
|
||||||
use crate::entity::r#trait::Entity;
|
direction::Direction,
|
||||||
use crate::entity::traversal::Traverser;
|
graph::{Edge, EdgePermissions, Graph, NodeId},
|
||||||
|
r#trait::Entity,
|
||||||
|
traversal::Traverser,
|
||||||
|
};
|
||||||
use crate::texture::animated::AnimatedTexture;
|
use crate::texture::animated::AnimatedTexture;
|
||||||
use crate::texture::directional::DirectionalAnimatedTexture;
|
use crate::texture::directional::DirectionalAnimatedTexture;
|
||||||
use crate::texture::sprite::SpriteAtlas;
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
use sdl2::keyboard::Keycode;
|
use sdl2::keyboard::Keycode;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::error::{GameError, GameResult, TextureError};
|
use crate::error::{GameError, GameResult, TextureError};
|
||||||
|
|
||||||
@@ -60,7 +64,7 @@ impl Entity for Pacman {
|
|||||||
|
|
||||||
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) {
|
||||||
eprintln!("Pac-Man movement error: {}", e);
|
error!("Pac-Man movement error: {}", e);
|
||||||
}
|
}
|
||||||
self.texture.tick(dt);
|
self.texture.tick(dt);
|
||||||
}
|
}
|
||||||
@@ -124,3 +128,9 @@ impl Pacman {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Collidable for Pacman {
|
||||||
|
fn position(&self) -> crate::entity::traversal::Position {
|
||||||
|
self.traverser.position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
124
src/game.rs
124
src/game.rs
@@ -17,7 +17,9 @@ use crate::{
|
|||||||
audio::Audio,
|
audio::Audio,
|
||||||
constants::{CELL_SIZE, RAW_BOARD},
|
constants::{CELL_SIZE, RAW_BOARD},
|
||||||
entity::{
|
entity::{
|
||||||
|
collision::{Collidable, CollisionSystem, EntityId},
|
||||||
ghost::{Ghost, GhostType},
|
ghost::{Ghost, GhostType},
|
||||||
|
item::Item,
|
||||||
pacman::Pacman,
|
pacman::Pacman,
|
||||||
r#trait::Entity,
|
r#trait::Entity,
|
||||||
},
|
},
|
||||||
@@ -37,8 +39,15 @@ pub struct Game {
|
|||||||
pub map: Map,
|
pub map: Map,
|
||||||
pub pacman: Pacman,
|
pub pacman: Pacman,
|
||||||
pub ghosts: Vec<Ghost>,
|
pub ghosts: Vec<Ghost>,
|
||||||
|
pub items: Vec<Item>,
|
||||||
pub debug_mode: bool,
|
pub debug_mode: bool,
|
||||||
|
|
||||||
|
// Collision system
|
||||||
|
collision_system: CollisionSystem,
|
||||||
|
pacman_id: EntityId,
|
||||||
|
ghost_ids: Vec<EntityId>,
|
||||||
|
item_ids: Vec<EntityId>,
|
||||||
|
|
||||||
// Rendering resources
|
// Rendering resources
|
||||||
atlas: SpriteAtlas,
|
atlas: SpriteAtlas,
|
||||||
map_texture: AtlasTile,
|
map_texture: AtlasTile,
|
||||||
@@ -68,7 +77,7 @@ impl Game {
|
|||||||
let atlas_texture = unsafe {
|
let atlas_texture = unsafe {
|
||||||
let texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
let texture = texture_creator.load_texture_bytes(&atlas_bytes).map_err(|e| {
|
||||||
if e.to_string().contains("format") || e.to_string().contains("unsupported") {
|
if e.to_string().contains("format") || e.to_string().contains("unsupported") {
|
||||||
GameError::Texture(TextureError::InvalidFormat(format!("Unsupported texture format: {}", e)))
|
GameError::Texture(TextureError::InvalidFormat(format!("Unsupported texture format: {e}")))
|
||||||
} else {
|
} else {
|
||||||
GameError::Texture(TextureError::LoadFailed(e.to_string()))
|
GameError::Texture(TextureError::LoadFailed(e.to_string()))
|
||||||
}
|
}
|
||||||
@@ -87,6 +96,9 @@ impl Game {
|
|||||||
let audio = Audio::new();
|
let audio = Audio::new();
|
||||||
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?;
|
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas)?;
|
||||||
|
|
||||||
|
// Generate items (pellets and energizers)
|
||||||
|
let items = map.generate_items(&atlas)?;
|
||||||
|
|
||||||
// Create ghosts at random positions
|
// Create ghosts at random positions
|
||||||
let mut ghosts = Vec::new();
|
let mut ghosts = Vec::new();
|
||||||
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
||||||
@@ -94,6 +106,7 @@ impl Game {
|
|||||||
|
|
||||||
if map.graph.node_count() == 0 {
|
if map.graph.node_count() == 0 {
|
||||||
return Err(GameError::Config("Game map has no nodes - invalid configuration".to_string()));
|
return Err(GameError::Config("Game map has no nodes - invalid configuration".to_string()));
|
||||||
|
// TODO: This is a bug, we should handle this better
|
||||||
}
|
}
|
||||||
|
|
||||||
for &ghost_type in &ghost_types {
|
for &ghost_type in &ghost_types {
|
||||||
@@ -103,12 +116,37 @@ impl Game {
|
|||||||
ghosts.push(ghost);
|
ghosts.push(ghost);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize collision system
|
||||||
|
let mut collision_system = CollisionSystem::default();
|
||||||
|
|
||||||
|
// Register Pac-Man
|
||||||
|
let pacman_id = collision_system.register_entity(pacman.position());
|
||||||
|
|
||||||
|
// Register items
|
||||||
|
let mut item_ids = Vec::new();
|
||||||
|
for item in &items {
|
||||||
|
let item_id = collision_system.register_entity(item.position());
|
||||||
|
item_ids.push(item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register ghosts
|
||||||
|
let mut ghost_ids = Vec::new();
|
||||||
|
for ghost in &ghosts {
|
||||||
|
let ghost_id = collision_system.register_entity(ghost.position());
|
||||||
|
ghost_ids.push(ghost_id);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Game {
|
Ok(Game {
|
||||||
score: 0,
|
score: 0,
|
||||||
map,
|
map,
|
||||||
pacman,
|
pacman,
|
||||||
ghosts,
|
ghosts,
|
||||||
|
items,
|
||||||
debug_mode: false,
|
debug_mode: false,
|
||||||
|
collision_system,
|
||||||
|
pacman_id,
|
||||||
|
ghost_ids,
|
||||||
|
item_ids,
|
||||||
map_texture,
|
map_texture,
|
||||||
text_texture,
|
text_texture,
|
||||||
audio,
|
audio,
|
||||||
@@ -145,6 +183,9 @@ impl Game {
|
|||||||
|
|
||||||
self.pacman = Pacman::new(&self.map.graph, pacman_start_node, &self.atlas)?;
|
self.pacman = Pacman::new(&self.map.graph, pacman_start_node, &self.atlas)?;
|
||||||
|
|
||||||
|
// Reset items
|
||||||
|
self.items = self.map.generate_items(&self.atlas)?;
|
||||||
|
|
||||||
// Randomize ghost positions
|
// Randomize ghost positions
|
||||||
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
let ghost_types = [GhostType::Blinky, GhostType::Pinky, GhostType::Inky, GhostType::Clyde];
|
||||||
let mut rng = SmallRng::from_os_rng();
|
let mut rng = SmallRng::from_os_rng();
|
||||||
@@ -154,6 +195,26 @@ impl Game {
|
|||||||
*ghost = Ghost::new(&self.map.graph, random_node, ghost_types[i], &self.atlas)?;
|
*ghost = Ghost::new(&self.map.graph, random_node, ghost_types[i], &self.atlas)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset collision system
|
||||||
|
self.collision_system = CollisionSystem::default();
|
||||||
|
|
||||||
|
// Re-register Pac-Man
|
||||||
|
self.pacman_id = self.collision_system.register_entity(self.pacman.position());
|
||||||
|
|
||||||
|
// Re-register items
|
||||||
|
self.item_ids.clear();
|
||||||
|
for item in &self.items {
|
||||||
|
let item_id = self.collision_system.register_entity(item.position());
|
||||||
|
self.item_ids.push(item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-register ghosts
|
||||||
|
self.ghost_ids.clear();
|
||||||
|
for ghost in &self.ghosts {
|
||||||
|
let ghost_id = self.collision_system.register_entity(ghost.position());
|
||||||
|
self.ghost_ids.push(ghost_id);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +225,60 @@ impl Game {
|
|||||||
for ghost in &mut self.ghosts {
|
for ghost in &mut self.ghosts {
|
||||||
ghost.tick(dt, &self.map.graph);
|
ghost.tick(dt, &self.map.graph);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update collision system positions
|
||||||
|
self.update_collision_positions();
|
||||||
|
|
||||||
|
// Check for collisions
|
||||||
|
self.check_collisions();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_collision_positions(&mut self) {
|
||||||
|
// Update Pac-Man's position
|
||||||
|
self.collision_system.update_position(self.pacman_id, self.pacman.position());
|
||||||
|
|
||||||
|
// Update ghost positions
|
||||||
|
for (ghost, &ghost_id) in self.ghosts.iter().zip(&self.ghost_ids) {
|
||||||
|
self.collision_system.update_position(ghost_id, ghost.position());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_collisions(&mut self) {
|
||||||
|
// Check Pac-Man vs Items
|
||||||
|
let potential_collisions = self.collision_system.potential_collisions(&self.pacman.position());
|
||||||
|
|
||||||
|
for entity_id in potential_collisions {
|
||||||
|
if entity_id != self.pacman_id {
|
||||||
|
// Check if this is an item collision
|
||||||
|
if let Some(item_index) = self.find_item_by_id(entity_id) {
|
||||||
|
let item = &mut self.items[item_index];
|
||||||
|
if !item.is_collected() {
|
||||||
|
item.collect();
|
||||||
|
self.score += item.get_score();
|
||||||
|
|
||||||
|
// Handle energizer effects
|
||||||
|
if matches!(item.item_type, crate::entity::item::ItemType::Energizer) {
|
||||||
|
// TODO: Make ghosts frightened
|
||||||
|
tracing::info!("Energizer collected! Ghosts should become frightened.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a ghost collision
|
||||||
|
if let Some(_ghost_index) = self.find_ghost_by_id(entity_id) {
|
||||||
|
// TODO: Handle Pac-Man being eaten by ghost
|
||||||
|
tracing::info!("Pac-Man collided with ghost!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_item_by_id(&self, entity_id: EntityId) -> Option<usize> {
|
||||||
|
self.item_ids.iter().position(|&id| id == entity_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_ghost_by_id(&self, entity_id: EntityId) -> Option<usize> {
|
||||||
|
self.ghost_ids.iter().position(|&id| id == entity_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
|
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> GameResult<()> {
|
||||||
@@ -173,6 +288,13 @@ impl Game {
|
|||||||
canvas.clear();
|
canvas.clear();
|
||||||
self.map.render(canvas, &mut self.atlas, &mut self.map_texture);
|
self.map.render(canvas, &mut self.atlas, &mut self.map_texture);
|
||||||
|
|
||||||
|
// Render all items
|
||||||
|
for item in &self.items {
|
||||||
|
if let Err(e) = item.render(canvas, &mut self.atlas, &self.map.graph) {
|
||||||
|
tracing::error!("Failed to render item: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Render all ghosts
|
// Render all ghosts
|
||||||
for ghost in &self.ghosts {
|
for ghost in &self.ghosts {
|
||||||
if let Err(e) = ghost.render(canvas, &mut self.atlas, &self.map.graph) {
|
if let Err(e) = ghost.render(canvas, &mut self.atlas, &self.map.graph) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
//! Map construction and building functionality.
|
//! Map construction and building functionality.
|
||||||
|
|
||||||
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE};
|
use crate::constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE, RAW_BOARD};
|
||||||
use crate::entity::direction::Direction;
|
use crate::entity::direction::Direction;
|
||||||
use crate::entity::graph::{EdgePermissions, Graph, Node, NodeId};
|
use crate::entity::graph::{EdgePermissions, Graph, Node, NodeId};
|
||||||
|
use crate::entity::item::{Item, ItemType};
|
||||||
use crate::map::parser::MapTileParser;
|
use crate::map::parser::MapTileParser;
|
||||||
use crate::map::render::MapRenderer;
|
use crate::map::render::MapRenderer;
|
||||||
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
use crate::texture::sprite::{AtlasTile, Sprite, SpriteAtlas};
|
||||||
use glam::{IVec2, UVec2, Vec2};
|
use glam::{IVec2, UVec2, Vec2};
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
@@ -117,7 +118,7 @@ impl Map {
|
|||||||
// Connect the new node to the source node
|
// Connect the new node to the source node
|
||||||
graph
|
graph
|
||||||
.connect(*source_node_id, new_node_id, false, None, dir)
|
.connect(*source_node_id, new_node_id, false, None, dir)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {e}")))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +133,7 @@ impl Map {
|
|||||||
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
|
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
|
||||||
graph
|
graph
|
||||||
.connect(node_id, neighbor_id, false, None, dir)
|
.connect(node_id, neighbor_id, false, None, dir)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to add edge: {e}")))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +188,44 @@ impl Map {
|
|||||||
MapRenderer::render_map(canvas, atlas, map_texture);
|
MapRenderer::render_map(canvas, atlas, map_texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates Item entities for pellets and energizers from the parsed map.
|
||||||
|
pub fn generate_items(&self, atlas: &SpriteAtlas) -> GameResult<Vec<Item>> {
|
||||||
|
// Pre-load sprites to avoid repeated texture lookups
|
||||||
|
let pellet_sprite = SpriteAtlas::get_tile(atlas, "maze/pellet.png")
|
||||||
|
.ok_or_else(|| MapError::InvalidConfig("Pellet texture not found".to_string()))?;
|
||||||
|
let energizer_sprite = SpriteAtlas::get_tile(atlas, "maze/energizer.png")
|
||||||
|
.ok_or_else(|| MapError::InvalidConfig("Energizer texture not found".to_string()))?;
|
||||||
|
|
||||||
|
// Pre-allocate with estimated capacity (typical Pac-Man maps have ~240 pellets + 4 energizers)
|
||||||
|
let mut items = Vec::with_capacity(250);
|
||||||
|
|
||||||
|
// Parse the raw board once
|
||||||
|
let parsed_map = MapTileParser::parse_board(RAW_BOARD)?;
|
||||||
|
let map = parsed_map.tiles;
|
||||||
|
|
||||||
|
// Iterate through the map and collect items more efficiently
|
||||||
|
for (x, row) in map.iter().enumerate() {
|
||||||
|
for (y, tile) in row.iter().enumerate() {
|
||||||
|
match tile {
|
||||||
|
MapTile::Pellet | MapTile::PowerPellet => {
|
||||||
|
let grid_pos = IVec2::new(x as i32, y as i32);
|
||||||
|
if let Some(&node_id) = self.grid_to_node.get(&grid_pos) {
|
||||||
|
let (item_type, sprite) = match tile {
|
||||||
|
MapTile::Pellet => (ItemType::Pellet, Sprite::new(pellet_sprite)),
|
||||||
|
MapTile::PowerPellet => (ItemType::Energizer, Sprite::new(energizer_sprite)),
|
||||||
|
_ => unreachable!(), // We already filtered for these types
|
||||||
|
};
|
||||||
|
items.push(Item::new(node_id, item_type, sprite));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders a debug visualization with cursor-based highlighting.
|
/// Renders a debug visualization with cursor-based highlighting.
|
||||||
///
|
///
|
||||||
/// This function provides interactive debugging by highlighting the nearest node
|
/// This function provides interactive debugging by highlighting the nearest node
|
||||||
@@ -241,10 +280,10 @@ impl Map {
|
|||||||
// Connect the house door to the left and right nodes
|
// Connect the house door to the left and right nodes
|
||||||
graph
|
graph
|
||||||
.connect(node_id, *left_node, true, None, Direction::Left)
|
.connect(node_id, *left_node, true, None, Direction::Left)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to left node: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to left node: {e}")))?;
|
||||||
graph
|
graph
|
||||||
.connect(node_id, *right_node, true, None, Direction::Right)
|
.connect(node_id, *right_node, true, None, Direction::Right)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to right node: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house door to right node: {e}")))?;
|
||||||
|
|
||||||
(node_id, node_position)
|
(node_id, node_position)
|
||||||
};
|
};
|
||||||
@@ -263,10 +302,10 @@ impl Map {
|
|||||||
// Connect the center node to the top and bottom nodes
|
// Connect the center node to the top and bottom nodes
|
||||||
graph
|
graph
|
||||||
.connect(center_node_id, top_node_id, false, None, Direction::Up)
|
.connect(center_node_id, top_node_id, false, None, Direction::Up)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to top node: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to top node: {e}")))?;
|
||||||
graph
|
graph
|
||||||
.connect(center_node_id, bottom_node_id, false, None, Direction::Down)
|
.connect(center_node_id, bottom_node_id, false, None, Direction::Down)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to bottom node: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house line to bottom node: {e}")))?;
|
||||||
|
|
||||||
Ok((center_node_id, top_node_id))
|
Ok((center_node_id, top_node_id))
|
||||||
};
|
};
|
||||||
@@ -289,7 +328,7 @@ impl Map {
|
|||||||
Direction::Down,
|
Direction::Down,
|
||||||
EdgePermissions::GhostsOnly,
|
EdgePermissions::GhostsOnly,
|
||||||
)
|
)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only entrance to house: {e}")))?;
|
||||||
|
|
||||||
graph
|
graph
|
||||||
.add_edge(
|
.add_edge(
|
||||||
@@ -300,7 +339,7 @@ impl Map {
|
|||||||
Direction::Up,
|
Direction::Up,
|
||||||
EdgePermissions::GhostsOnly,
|
EdgePermissions::GhostsOnly,
|
||||||
)
|
)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to create ghost-only exit from house: {e}")))?;
|
||||||
|
|
||||||
// Create the left line
|
// Create the left line
|
||||||
let (left_center_node_id, _) = create_house_line(
|
let (left_center_node_id, _) = create_house_line(
|
||||||
@@ -319,11 +358,11 @@ impl Map {
|
|||||||
// Connect the center line to the left and right lines
|
// Connect the center line to the left and right lines
|
||||||
graph
|
graph
|
||||||
.connect(center_center_node_id, left_center_node_id, false, None, Direction::Left)
|
.connect(center_center_node_id, left_center_node_id, false, None, Direction::Left)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to left top line: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to left top line: {e}")))?;
|
||||||
|
|
||||||
graph
|
graph
|
||||||
.connect(center_center_node_id, right_center_node_id, false, None, Direction::Right)
|
.connect(center_center_node_id, right_center_node_id, false, None, Direction::Right)
|
||||||
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to right top line: {}", e)))?;
|
.map_err(|e| MapError::InvalidConfig(format!("Failed to connect house entrance to right top line: {e}")))?;
|
||||||
|
|
||||||
debug!("House entrance node id: {house_entrance_node_id}");
|
debug!("House entrance node id: {house_entrance_node_id}");
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,27 @@ use sdl2::render::{Canvas, RenderTarget, Texture};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// 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<()> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct AtlasMapper {
|
pub struct AtlasMapper {
|
||||||
pub frames: HashMap<String, MapperFrame>,
|
pub frames: HashMap<String, MapperFrame>,
|
||||||
|
|||||||
Reference in New Issue
Block a user