refactor: huge refactor into node/graph-based movement system

This commit is contained in:
2025-07-28 12:23:57 -05:00
parent 413f9f156f
commit 464d6f9ca6
24 changed files with 868 additions and 2067 deletions

View File

@@ -1,205 +1,3 @@
pub mod direction;
pub mod edible;
pub mod ghost;
pub mod graph;
pub mod pacman;
pub mod speed;
use crate::{
constants::{MapTile, BOARD_CELL_OFFSET, BOARD_CELL_SIZE, CELL_SIZE},
entity::{direction::Direction, speed::SimpleTickModulator},
map::Map,
};
use anyhow::Result;
use glam::{IVec2, UVec2};
use sdl2::render::WindowCanvas;
use std::cell::RefCell;
use std::rc::Rc;
/// A trait for game objects that can be moved and rendered.
pub trait Entity {
/// Returns a reference to the base entity (position, etc).
fn base(&self) -> &StaticEntity;
/// Returns true if the entity is colliding with the other entity.
fn is_colliding(&self, other: &dyn Entity) -> bool {
let a = self.base().cell_position;
let b = other.base().cell_position;
a == b
}
}
/// A trait for entities that can move and interact with the map.
pub trait Moving {
fn tick(&mut self) {
self.base_tick();
}
fn base_tick(&mut self) {
if self.is_grid_aligned() {
self.on_grid_aligned();
}
self.tick_movement();
}
/// Called when the entity is grid-aligned. Default does nothing.
fn on_grid_aligned(&mut self) {}
/// Handles movement and wall collision. Default uses tick logic from MovableEntity.
fn tick_movement(&mut self);
fn update_cell_position(&mut self);
fn next_cell(&self, direction: Option<Direction>) -> IVec2;
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool;
fn handle_tunnel(&mut self) -> bool;
fn is_grid_aligned(&self) -> bool;
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool;
}
/// Trait for entities that support queued direction changes.
pub trait QueuedDirection: Moving {
fn next_direction(&self) -> Option<Direction>;
fn set_next_direction(&mut self, dir: Option<Direction>);
/// Handles a requested direction change if possible.
fn handle_direction_change(&mut self) -> bool {
if let Some(next_direction) = self.next_direction() {
if self.set_direction_if_valid(next_direction) {
self.set_next_direction(None);
return true;
}
}
false
}
}
/// A struct for static (non-moving) entities with position only.
pub struct StaticEntity {
pub pixel_position: IVec2,
pub cell_position: UVec2,
}
impl StaticEntity {
pub fn new(pixel_position: IVec2, cell_position: UVec2) -> Self {
Self {
pixel_position,
cell_position,
}
}
}
/// A struct for movable game entities with position, direction, speed, and modulation.
pub struct MovableEntity {
pub base: StaticEntity,
pub direction: Direction,
pub speed: SimpleTickModulator,
pub in_tunnel: bool,
pub map: Rc<RefCell<Map>>,
}
impl MovableEntity {
pub fn new(
pixel_position: IVec2,
cell_position: UVec2,
direction: Direction,
speed: SimpleTickModulator,
map: Rc<RefCell<Map>>,
) -> Self {
Self {
base: StaticEntity::new(pixel_position, cell_position),
direction,
speed,
in_tunnel: false,
map,
}
}
/// Returns the position within the current cell, in pixels.
pub fn internal_position(&self) -> UVec2 {
UVec2::new(
(self.base.pixel_position.x as u32) % CELL_SIZE,
(self.base.pixel_position.y as u32) % CELL_SIZE,
)
}
}
impl Entity for MovableEntity {
fn base(&self) -> &StaticEntity {
&self.base
}
}
impl Moving for MovableEntity {
fn tick_movement(&mut self) {
if self.speed.next() && !self.is_wall_ahead(None) {
match self.direction {
Direction::Right => self.base.pixel_position.x += 1,
Direction::Left => self.base.pixel_position.x -= 1,
Direction::Up => self.base.pixel_position.y -= 1,
Direction::Down => self.base.pixel_position.y += 1,
}
if self.is_grid_aligned() {
self.update_cell_position();
}
}
if self.is_grid_aligned() {
self.update_cell_position();
}
}
fn update_cell_position(&mut self) {
self.base.cell_position = UVec2::new(
(self.base.pixel_position.x as u32 / CELL_SIZE) - BOARD_CELL_OFFSET.x,
(self.base.pixel_position.y as u32 / CELL_SIZE) - BOARD_CELL_OFFSET.y,
);
}
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
let IVec2 { x, y } = direction.unwrap_or(self.direction).offset();
IVec2::new(self.base.cell_position.x as i32 + x, self.base.cell_position.y as i32 + y)
}
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
let next_cell = self.next_cell(direction);
matches!(self.map.borrow().get_tile(next_cell), Some(MapTile::Wall))
}
fn handle_tunnel(&mut self) -> bool {
let x = self.base.cell_position.x;
let at_left_tunnel = x == 0;
let at_right_tunnel = x == BOARD_CELL_SIZE.x - 1;
// Reset tunnel state if we're not at a tunnel position
if !at_left_tunnel && !at_right_tunnel {
self.in_tunnel = false;
return false;
}
// If we're already in a tunnel, stay in tunnel state
if self.in_tunnel {
return true;
}
// Enter the tunnel and teleport to the other side
let new_x = if at_left_tunnel { BOARD_CELL_SIZE.x - 2 } else { 1 };
self.base.cell_position.x = new_x;
self.base.pixel_position = Map::cell_to_pixel(self.base.cell_position);
self.in_tunnel = true;
true
}
fn is_grid_aligned(&self) -> bool {
self.internal_position() == UVec2::ZERO
}
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
if new_direction == self.direction {
return false;
}
if self.is_wall_ahead(Some(new_direction)) {
return false;
}
self.direction = new_direction;
true
}
}
impl Entity for StaticEntity {
fn base(&self) -> &StaticEntity {
self
}
}
/// A trait for entities that can be rendered to the screen.
pub trait Renderable {
fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()>;
}