mirror of
https://github.com/Xevion/Pac-Man.git
synced 2025-12-07 07:15:45 -06:00
refactor: huge refactor into node/graph-based movement system
This commit is contained in:
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -192,6 +192,7 @@ dependencies = [
|
|||||||
"sdl2",
|
"sdl2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"smallvec",
|
||||||
"spin_sleep",
|
"spin_sleep",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -392,9 +393,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.11.0"
|
version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin_sleep"
|
name = "spin_sleep"
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ pathfinding = "4.14"
|
|||||||
once_cell = "1.21.3"
|
once_cell = "1.21.3"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
glam = "0.30.4"
|
glam = { version = "0.30.4", features = [] }
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
serde_json = "1.0.141"
|
serde_json = "1.0.141"
|
||||||
|
smallvec = "1.15.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
panic-strategy = "abort"
|
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies.winapi]
|
[target.'cfg(target_os = "windows")'.dependencies.winapi]
|
||||||
|
|||||||
152
src/app.rs
Normal file
152
src/app.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use sdl2::event::{Event, WindowEvent};
|
||||||
|
use sdl2::keyboard::Keycode;
|
||||||
|
use sdl2::render::{Canvas, ScaleMode, Texture, TextureCreator};
|
||||||
|
use sdl2::video::{Window, WindowContext};
|
||||||
|
use sdl2::EventPump;
|
||||||
|
use tracing::{error, event};
|
||||||
|
|
||||||
|
use crate::constants::{CANVAS_SIZE, LOOP_TIME, SCALE};
|
||||||
|
use crate::game::Game;
|
||||||
|
|
||||||
|
#[cfg(target_os = "emscripten")]
|
||||||
|
use crate::emscripten;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "emscripten"))]
|
||||||
|
fn sleep(value: Duration) {
|
||||||
|
spin_sleep::sleep(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "emscripten")]
|
||||||
|
fn sleep(value: Duration) {
|
||||||
|
emscripten::emscripten::sleep(value.as_millis() as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App<'a> {
|
||||||
|
game: Game,
|
||||||
|
canvas: Canvas<Window>,
|
||||||
|
event_pump: EventPump,
|
||||||
|
backbuffer: Texture<'a>,
|
||||||
|
paused: bool,
|
||||||
|
last_tick: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> App<'a> {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let sdl_context = sdl2::init().map_err(|e| anyhow!(e))?;
|
||||||
|
let video_subsystem = sdl_context.video().map_err(|e| anyhow!(e))?;
|
||||||
|
let audio_subsystem = sdl_context.audio().map_err(|e| anyhow!(e))?;
|
||||||
|
let ttf_context = sdl2::ttf::init().map_err(|e| anyhow!(e.to_string()))?;
|
||||||
|
|
||||||
|
let window = video_subsystem
|
||||||
|
.window(
|
||||||
|
"Pac-Man",
|
||||||
|
(CANVAS_SIZE.x as f32 * SCALE).round() as u32,
|
||||||
|
(CANVAS_SIZE.y as f32 * SCALE).round() as u32,
|
||||||
|
)
|
||||||
|
.resizable()
|
||||||
|
.position_centered()
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let mut canvas = window.into_canvas().build()?;
|
||||||
|
canvas.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)?;
|
||||||
|
|
||||||
|
let texture_creator_static: &'static TextureCreator<WindowContext> = Box::leak(Box::new(canvas.texture_creator()));
|
||||||
|
|
||||||
|
let mut game = Game::new(texture_creator_static, &ttf_context, &audio_subsystem);
|
||||||
|
game.audio.set_mute(cfg!(debug_assertions));
|
||||||
|
|
||||||
|
let mut backbuffer = texture_creator_static.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)?;
|
||||||
|
backbuffer.set_scale_mode(ScaleMode::Nearest);
|
||||||
|
|
||||||
|
let event_pump = sdl_context.event_pump().map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
|
// Initial draw
|
||||||
|
game.draw(&mut canvas, &mut backbuffer)?;
|
||||||
|
game.present_backbuffer(&mut canvas, &backbuffer)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
game,
|
||||||
|
canvas,
|
||||||
|
event_pump,
|
||||||
|
backbuffer,
|
||||||
|
paused: false,
|
||||||
|
last_tick: Instant::now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&mut self) -> bool {
|
||||||
|
{
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
for event in self.event_pump.poll_iter() {
|
||||||
|
match event {
|
||||||
|
Event::Window { win_event, .. } => match win_event {
|
||||||
|
WindowEvent::Hidden => {
|
||||||
|
event!(tracing::Level::DEBUG, "Window hidden");
|
||||||
|
}
|
||||||
|
WindowEvent::Shown => {
|
||||||
|
event!(tracing::Level::DEBUG, "Window shown");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Event::Quit { .. }
|
||||||
|
| Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
event!(tracing::Level::INFO, "Exit requested. Exiting...");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::P),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.paused = !self.paused;
|
||||||
|
event!(tracing::Level::INFO, "{}", if self.paused { "Paused" } else { "Unpaused" });
|
||||||
|
}
|
||||||
|
Event::KeyDown {
|
||||||
|
keycode: Some(Keycode::Space),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.game.debug_mode = !self.game.debug_mode;
|
||||||
|
}
|
||||||
|
Event::KeyDown { keycode, .. } => {
|
||||||
|
self.game.keyboard_event(keycode.unwrap());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let dt = self.last_tick.elapsed().as_secs_f32();
|
||||||
|
self.last_tick = Instant::now();
|
||||||
|
|
||||||
|
if !self.paused {
|
||||||
|
self.game.tick(dt);
|
||||||
|
if let Err(e) = self.game.draw(&mut self.canvas, &mut self.backbuffer) {
|
||||||
|
error!("Failed to draw game: {e}");
|
||||||
|
}
|
||||||
|
if let Err(e) = self.game.present_backbuffer(&mut self.canvas, &self.backbuffer) {
|
||||||
|
error!("Failed to present backbuffer: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start.elapsed() < LOOP_TIME {
|
||||||
|
let time = LOOP_TIME.saturating_sub(start.elapsed());
|
||||||
|
if time != Duration::ZERO {
|
||||||
|
sleep(time);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event!(
|
||||||
|
tracing::Level::WARN,
|
||||||
|
"Game loop behind schedule by: {:?}",
|
||||||
|
start.elapsed() - LOOP_TIME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@ pub enum Asset {
|
|||||||
Wav2,
|
Wav2,
|
||||||
Wav3,
|
Wav3,
|
||||||
Wav4,
|
Wav4,
|
||||||
FontKonami,
|
|
||||||
Atlas,
|
Atlas,
|
||||||
AtlasJson,
|
AtlasJson,
|
||||||
// Add more as needed
|
// Add more as needed
|
||||||
@@ -37,7 +36,6 @@ impl Asset {
|
|||||||
Wav2 => "sound/waka/2.ogg",
|
Wav2 => "sound/waka/2.ogg",
|
||||||
Wav3 => "sound/waka/3.ogg",
|
Wav3 => "sound/waka/3.ogg",
|
||||||
Wav4 => "sound/waka/4.ogg",
|
Wav4 => "sound/waka/4.ogg",
|
||||||
FontKonami => "konami.ttf",
|
|
||||||
Atlas => "atlas.png",
|
Atlas => "atlas.png",
|
||||||
AtlasJson => "atlas.json",
|
AtlasJson => "atlas.json",
|
||||||
}
|
}
|
||||||
@@ -54,7 +52,6 @@ mod imp {
|
|||||||
Asset::Wav2 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/2.ogg")),
|
Asset::Wav2 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/2.ogg")),
|
||||||
Asset::Wav3 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/3.ogg")),
|
Asset::Wav3 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/3.ogg")),
|
||||||
Asset::Wav4 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/4.ogg")),
|
Asset::Wav4 => Cow::Borrowed(include_bytes!("../assets/game/sound/waka/4.ogg")),
|
||||||
Asset::FontKonami => Cow::Borrowed(include_bytes!("../assets/game/konami.ttf")),
|
|
||||||
Asset::Atlas => Cow::Borrowed(include_bytes!("../assets/game/atlas.png")),
|
Asset::Atlas => Cow::Borrowed(include_bytes!("../assets/game/atlas.png")),
|
||||||
Asset::AtlasJson => Cow::Borrowed(include_bytes!("../assets/game/atlas.json")),
|
Asset::AtlasJson => Cow::Borrowed(include_bytes!("../assets/game/atlas.json")),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ impl Audio {
|
|||||||
self.muted = mute;
|
self.muted = mute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the audio is muted.
|
||||||
pub fn is_muted(&self) -> bool {
|
pub fn is_muted(&self) -> bool {
|
||||||
self.muted
|
self.muted
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
//! This module contains all the constants used in the game.
|
//! This module contains all the constants used in the game.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use glam::UVec2;
|
use glam::UVec2;
|
||||||
|
|
||||||
|
pub const LOOP_TIME: Duration = Duration::from_nanos((1_000_000_000.0 / 60.0) as u64);
|
||||||
|
|
||||||
/// The size of each cell, in pixels.
|
/// The size of each cell, in pixels.
|
||||||
pub const CELL_SIZE: u32 = 8;
|
pub const CELL_SIZE: u32 = 8;
|
||||||
/// The size of the game board, in cells.
|
/// The size of the game board, in cells.
|
||||||
@@ -39,58 +43,6 @@ pub enum MapTile {
|
|||||||
Tunnel,
|
Tunnel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
#[repr(u8)]
|
|
||||||
pub enum FruitType {
|
|
||||||
Cherry,
|
|
||||||
Strawberry,
|
|
||||||
Orange,
|
|
||||||
Apple,
|
|
||||||
Melon,
|
|
||||||
Galaxian,
|
|
||||||
Bell,
|
|
||||||
Key,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FruitType {
|
|
||||||
pub const ALL: [FruitType; 8] = [
|
|
||||||
FruitType::Cherry,
|
|
||||||
FruitType::Strawberry,
|
|
||||||
FruitType::Orange,
|
|
||||||
FruitType::Apple,
|
|
||||||
FruitType::Melon,
|
|
||||||
FruitType::Galaxian,
|
|
||||||
FruitType::Bell,
|
|
||||||
FruitType::Key,
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn score(self) -> u32 {
|
|
||||||
match self {
|
|
||||||
FruitType::Cherry => 100,
|
|
||||||
FruitType::Strawberry => 300,
|
|
||||||
FruitType::Orange => 500,
|
|
||||||
FruitType::Apple => 700,
|
|
||||||
FruitType::Melon => 1000,
|
|
||||||
FruitType::Galaxian => 2000,
|
|
||||||
FruitType::Bell => 3000,
|
|
||||||
FruitType::Key => 5000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn index(self) -> usize {
|
|
||||||
match self {
|
|
||||||
FruitType::Cherry => 0,
|
|
||||||
FruitType::Strawberry => 1,
|
|
||||||
FruitType::Orange => 2,
|
|
||||||
FruitType::Apple => 3,
|
|
||||||
FruitType::Melon => 4,
|
|
||||||
FruitType::Galaxian => 5,
|
|
||||||
FruitType::Bell => 6,
|
|
||||||
FruitType::Key => 7,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The raw layout of the game board, as a 2D array of characters.
|
/// The raw layout of the game board, as a 2D array of characters.
|
||||||
pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [
|
pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [
|
||||||
"############################",
|
"############################",
|
||||||
@@ -106,9 +58,9 @@ pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [
|
|||||||
" #.##### ## #####.# ",
|
" #.##### ## #####.# ",
|
||||||
" #.## 1 ##.# ",
|
" #.## 1 ##.# ",
|
||||||
" #.## ###==### ##.# ",
|
" #.## ###==### ##.# ",
|
||||||
"######.## # # ##.######",
|
"######.## ######## ##.######",
|
||||||
"T . #2 3 4 # . T",
|
"T . ######## . T",
|
||||||
"######.## # # ##.######",
|
"######.## ######## ##.######",
|
||||||
" #.## ######## ##.# ",
|
" #.## ######## ##.# ",
|
||||||
" #.## ##.# ",
|
" #.## ##.# ",
|
||||||
" #.## ######## ##.# ",
|
" #.## ######## ##.# ",
|
||||||
|
|||||||
73
src/debug.rs
73
src/debug.rs
@@ -1,73 +0,0 @@
|
|||||||
//! Debug rendering utilities for Pac-Man.
|
|
||||||
use crate::{
|
|
||||||
constants::{MapTile, BOARD_CELL_SIZE, CELL_SIZE},
|
|
||||||
entity::ghost::Ghost,
|
|
||||||
map::Map,
|
|
||||||
};
|
|
||||||
use glam::{IVec2, UVec2};
|
|
||||||
use sdl2::{pixels::Color, render::Canvas, video::Window};
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
|
||||||
pub enum DebugMode {
|
|
||||||
None,
|
|
||||||
Grid,
|
|
||||||
Pathfinding,
|
|
||||||
ValidPositions,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DebugRenderer;
|
|
||||||
|
|
||||||
impl DebugRenderer {
|
|
||||||
pub fn draw_cell(canvas: &mut Canvas<Window>, _map: &Map, cell: UVec2, color: Color) {
|
|
||||||
let position = Map::cell_to_pixel(cell);
|
|
||||||
canvas.set_draw_color(color);
|
|
||||||
canvas
|
|
||||||
.draw_rect(sdl2::rect::Rect::new(position.x, position.y, CELL_SIZE, CELL_SIZE))
|
|
||||||
.expect("Could not draw rectangle");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_debug_grid(canvas: &mut Canvas<Window>, map: &Map, pacman_cell: UVec2) {
|
|
||||||
for x in 0..BOARD_CELL_SIZE.x {
|
|
||||||
for y in 0..BOARD_CELL_SIZE.y {
|
|
||||||
let tile = map.get_tile(IVec2::new(x as i32, y as i32)).unwrap_or(MapTile::Empty);
|
|
||||||
let cell = UVec2::new(x, y);
|
|
||||||
let mut color = None;
|
|
||||||
if cell == pacman_cell {
|
|
||||||
Self::draw_cell(canvas, map, cell, Color::CYAN);
|
|
||||||
} else {
|
|
||||||
color = match tile {
|
|
||||||
MapTile::Empty => None,
|
|
||||||
MapTile::Wall => Some(Color::BLUE),
|
|
||||||
MapTile::Pellet => Some(Color::RED),
|
|
||||||
MapTile::PowerPellet => Some(Color::MAGENTA),
|
|
||||||
MapTile::StartingPosition(_) => Some(Color::GREEN),
|
|
||||||
MapTile::Tunnel => Some(Color::CYAN),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if let Some(color) = color {
|
|
||||||
Self::draw_cell(canvas, map, cell, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_next_cell(canvas: &mut Canvas<Window>, map: &Map, next_cell: UVec2) {
|
|
||||||
Self::draw_cell(canvas, map, next_cell, Color::YELLOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_valid_positions(canvas: &mut Canvas<Window>, map: &mut Map) {
|
|
||||||
let valid_positions_vec = map.get_valid_playable_positions().clone();
|
|
||||||
for &pos in &valid_positions_vec {
|
|
||||||
Self::draw_cell(canvas, map, pos, Color::RGB(255, 140, 0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_pathfinding(canvas: &mut Canvas<Window>, ghost: &Ghost, map: &Map) {
|
|
||||||
let target = ghost.get_target_tile();
|
|
||||||
if let Some((path, _)) = ghost.get_path_to_target(target.unwrap().as_uvec2()) {
|
|
||||||
for pos in &path {
|
|
||||||
Self::draw_cell(canvas, map, *pos, Color::YELLOW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
//! This module defines the `Direction` enum, which is used to represent the
|
|
||||||
//! direction of an entity.
|
|
||||||
use glam::IVec2;
|
use glam::IVec2;
|
||||||
use sdl2::keyboard::Keycode;
|
|
||||||
|
|
||||||
/// An enum representing the direction of an entity.
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
Up,
|
Up,
|
||||||
Down,
|
Down,
|
||||||
@@ -13,48 +9,29 @@ pub enum Direction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Direction {
|
impl Direction {
|
||||||
/// Returns the angle of the direction in degrees.
|
|
||||||
pub fn angle(&self) -> f64 {
|
|
||||||
match self {
|
|
||||||
Direction::Right => 0f64,
|
|
||||||
Direction::Down => 90f64,
|
|
||||||
Direction::Left => 180f64,
|
|
||||||
Direction::Up => 270f64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the offset of the direction as a tuple of (x, y).
|
|
||||||
pub fn offset(&self) -> IVec2 {
|
|
||||||
match self {
|
|
||||||
Direction::Right => IVec2::new(1, 0),
|
|
||||||
Direction::Down => IVec2::new(0, 1),
|
|
||||||
Direction::Left => IVec2::new(-1, 0),
|
|
||||||
Direction::Up => IVec2::new(0, -1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the opposite direction.
|
|
||||||
pub fn opposite(&self) -> Direction {
|
pub fn opposite(&self) -> Direction {
|
||||||
match self {
|
match self {
|
||||||
Direction::Right => Direction::Left,
|
Direction::Up => Direction::Down,
|
||||||
Direction::Down => Direction::Up,
|
Direction::Down => Direction::Up,
|
||||||
Direction::Left => Direction::Right,
|
Direction::Left => Direction::Right,
|
||||||
Direction::Up => Direction::Down,
|
Direction::Right => Direction::Left,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a `Direction` from a `Keycode`.
|
pub fn to_ivec2(&self) -> IVec2 {
|
||||||
///
|
(*self).into()
|
||||||
/// # Arguments
|
}
|
||||||
///
|
}
|
||||||
/// * `keycode` - The keycode to convert.
|
|
||||||
pub fn from_keycode(keycode: Keycode) -> Option<Direction> {
|
impl From<Direction> for IVec2 {
|
||||||
match keycode {
|
fn from(dir: Direction) -> Self {
|
||||||
Keycode::D | Keycode::Right => Some(Direction::Right),
|
match dir {
|
||||||
Keycode::A | Keycode::Left => Some(Direction::Left),
|
Direction::Up => -IVec2::Y,
|
||||||
Keycode::W | Keycode::Up => Some(Direction::Up),
|
Direction::Down => IVec2::Y,
|
||||||
Keycode::S | Keycode::Down => Some(Direction::Down),
|
Direction::Left => -IVec2::X,
|
||||||
_ => None,
|
Direction::Right => IVec2::X,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const DIRECTIONS: [Direction; 4] = [Direction::Up, Direction::Down, Direction::Left, Direction::Right];
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
//! Edible entity for Pac-Man: pellets, power pellets, and fruits.
|
|
||||||
use crate::constants::{FruitType, MapTile, BOARD_CELL_SIZE};
|
|
||||||
use crate::entity::{Entity, Renderable, StaticEntity};
|
|
||||||
use crate::map::Map;
|
|
||||||
use crate::texture::animated::AnimatedTexture;
|
|
||||||
use crate::texture::blinking::BlinkingTexture;
|
|
||||||
use anyhow::Result;
|
|
||||||
use glam::{IVec2, UVec2};
|
|
||||||
use sdl2::render::WindowCanvas;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum EdibleKind {
|
|
||||||
Pellet,
|
|
||||||
PowerPellet,
|
|
||||||
Fruit(FruitType),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum EdibleSprite {
|
|
||||||
Pellet(AnimatedTexture),
|
|
||||||
PowerPellet(BlinkingTexture),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Edible {
|
|
||||||
pub base: StaticEntity,
|
|
||||||
pub kind: EdibleKind,
|
|
||||||
pub sprite: EdibleSprite,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Edible {
|
|
||||||
pub fn new_pellet(cell_position: UVec2, sprite: AnimatedTexture) -> Self {
|
|
||||||
let pixel_position = Map::cell_to_pixel(cell_position);
|
|
||||||
Edible {
|
|
||||||
base: StaticEntity::new(pixel_position, cell_position),
|
|
||||||
kind: EdibleKind::Pellet,
|
|
||||||
sprite: EdibleSprite::Pellet(sprite),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn new_power_pellet(cell_position: UVec2, sprite: BlinkingTexture) -> Self {
|
|
||||||
let pixel_position = Map::cell_to_pixel(cell_position);
|
|
||||||
Edible {
|
|
||||||
base: StaticEntity::new(pixel_position, cell_position),
|
|
||||||
kind: EdibleKind::PowerPellet,
|
|
||||||
sprite: EdibleSprite::PowerPellet(sprite),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks collision with Pac-Man (or any entity)
|
|
||||||
pub fn collide(&self, pacman: &dyn Entity) -> bool {
|
|
||||||
self.base.cell_position == pacman.base().cell_position
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for Edible {
|
|
||||||
fn base(&self) -> &StaticEntity {
|
|
||||||
&self.base
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Renderable for Edible {
|
|
||||||
fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> {
|
|
||||||
let pos = self.base.pixel_position;
|
|
||||||
let dest = match &mut self.sprite {
|
|
||||||
EdibleSprite::Pellet(sprite) => {
|
|
||||||
let tile = sprite.current_tile();
|
|
||||||
let x = pos.x + ((crate::constants::CELL_SIZE as i32 - tile.size.x as i32) / 2);
|
|
||||||
let y = pos.y + ((crate::constants::CELL_SIZE as i32 - tile.size.y as i32) / 2);
|
|
||||||
sdl2::rect::Rect::new(x, y, tile.size.x as u32, tile.size.y as u32)
|
|
||||||
}
|
|
||||||
EdibleSprite::PowerPellet(sprite) => {
|
|
||||||
let tile = sprite.animation.current_tile();
|
|
||||||
let x = pos.x + ((crate::constants::CELL_SIZE as i32 - tile.size.x as i32) / 2);
|
|
||||||
let y = pos.y + ((crate::constants::CELL_SIZE as i32 - tile.size.y as i32) / 2);
|
|
||||||
sdl2::rect::Rect::new(x, y, tile.size.x as u32, tile.size.y as u32)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match &mut self.sprite {
|
|
||||||
EdibleSprite::Pellet(sprite) => sprite.render(canvas, dest),
|
|
||||||
EdibleSprite::PowerPellet(sprite) => sprite.render(canvas, dest),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reconstruct all edibles from the original map layout
|
|
||||||
pub fn reconstruct_edibles(
|
|
||||||
map: Rc<RefCell<Map>>,
|
|
||||||
pellet_sprite: AnimatedTexture,
|
|
||||||
power_pellet_sprite: BlinkingTexture,
|
|
||||||
_fruit_sprite: AnimatedTexture,
|
|
||||||
) -> Vec<Edible> {
|
|
||||||
let mut edibles = Vec::new();
|
|
||||||
for x in 0..BOARD_CELL_SIZE.x {
|
|
||||||
for y in 0..BOARD_CELL_SIZE.y {
|
|
||||||
let tile = map.borrow().get_tile(IVec2::new(x as i32, y as i32));
|
|
||||||
match tile {
|
|
||||||
Some(MapTile::Pellet) => {
|
|
||||||
edibles.push(Edible::new_pellet(UVec2::new(x, y), pellet_sprite.clone()));
|
|
||||||
}
|
|
||||||
Some(MapTile::PowerPellet) => {
|
|
||||||
edibles.push(Edible::new_power_pellet(UVec2::new(x, y), power_pellet_sprite.clone()));
|
|
||||||
}
|
|
||||||
// Fruits can be added here if you have fruit positions
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
edibles
|
|
||||||
}
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::Rng;
|
|
||||||
use rand::SeedableRng;
|
|
||||||
|
|
||||||
use crate::constants::MapTile;
|
|
||||||
use crate::constants::BOARD_CELL_SIZE;
|
|
||||||
use crate::entity::direction::Direction;
|
|
||||||
use crate::entity::pacman::Pacman;
|
|
||||||
use crate::entity::speed::SimpleTickModulator;
|
|
||||||
use crate::entity::{Entity, MovableEntity, Moving, Renderable};
|
|
||||||
use crate::map::Map;
|
|
||||||
use crate::texture::{
|
|
||||||
animated::AnimatedTexture, blinking::BlinkingTexture, directional::DirectionalAnimatedTexture, get_atlas_tile,
|
|
||||||
sprite::SpriteAtlas,
|
|
||||||
};
|
|
||||||
use anyhow::Result;
|
|
||||||
use glam::{IVec2, UVec2};
|
|
||||||
use sdl2::pixels::Color;
|
|
||||||
use sdl2::render::WindowCanvas;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
/// The different modes a ghost can be in
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum GhostMode {
|
|
||||||
/// Chase mode - ghost actively pursues Pac-Man using its unique strategy
|
|
||||||
Chase,
|
|
||||||
/// Scatter mode - ghost heads to its home corner
|
|
||||||
Scatter,
|
|
||||||
/// Frightened mode - ghost moves randomly and can be eaten
|
|
||||||
Frightened,
|
|
||||||
/// Eyes mode - ghost returns to the ghost house after being eaten
|
|
||||||
Eyes,
|
|
||||||
/// House mode - ghost is in the ghost house, waiting to exit
|
|
||||||
House(HouseMode),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum HouseMode {
|
|
||||||
Entering,
|
|
||||||
Exiting,
|
|
||||||
Waiting,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The different ghost personalities
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum GhostType {
|
|
||||||
Blinky, // Red - Shadow
|
|
||||||
Pinky, // Pink - Speedy
|
|
||||||
Inky, // Cyan - Bashful
|
|
||||||
Clyde, // Orange - Pokey
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GhostType {
|
|
||||||
/// Returns the color of the ghost.
|
|
||||||
pub fn color(&self) -> Color {
|
|
||||||
match self {
|
|
||||||
GhostType::Blinky => Color::RGB(255, 0, 0),
|
|
||||||
GhostType::Pinky => Color::RGB(255, 184, 255),
|
|
||||||
GhostType::Inky => Color::RGB(0, 255, 255),
|
|
||||||
GhostType::Clyde => Color::RGB(255, 184, 82),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Base ghost struct that contains common functionality
|
|
||||||
pub struct Ghost {
|
|
||||||
/// Shared movement and position fields.
|
|
||||||
pub base: MovableEntity,
|
|
||||||
/// The current mode of the ghost
|
|
||||||
pub mode: GhostMode,
|
|
||||||
/// The type/personality of this ghost
|
|
||||||
pub ghost_type: GhostType,
|
|
||||||
/// Reference to Pac-Man for targeting
|
|
||||||
pub pacman: Rc<RefCell<Pacman>>,
|
|
||||||
pub texture: DirectionalAnimatedTexture,
|
|
||||||
pub frightened_texture: BlinkingTexture,
|
|
||||||
pub eyes_texture: DirectionalAnimatedTexture,
|
|
||||||
pub house_offset: i32,
|
|
||||||
pub current_house_offset: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ghost {
|
|
||||||
/// Creates a new ghost instance
|
|
||||||
pub fn new(
|
|
||||||
ghost_type: GhostType,
|
|
||||||
starting_position: UVec2,
|
|
||||||
atlas: Rc<RefCell<SpriteAtlas>>,
|
|
||||||
map: Rc<RefCell<Map>>,
|
|
||||||
pacman: Rc<RefCell<Pacman>>,
|
|
||||||
house_offset: i32,
|
|
||||||
) -> Ghost {
|
|
||||||
let pixel_position = Map::cell_to_pixel(starting_position);
|
|
||||||
let name = match ghost_type {
|
|
||||||
GhostType::Blinky => "blinky",
|
|
||||||
GhostType::Pinky => "pinky",
|
|
||||||
GhostType::Inky => "inky",
|
|
||||||
GhostType::Clyde => "clyde",
|
|
||||||
};
|
|
||||||
let get = |dir: &str, suffix: &str| get_atlas_tile(&atlas, &format!("ghost/{name}/{dir}_{suffix}.png"));
|
|
||||||
|
|
||||||
let texture = DirectionalAnimatedTexture::new(
|
|
||||||
vec![get("up", "a"), get("up", "b")],
|
|
||||||
vec![get("down", "a"), get("down", "b")],
|
|
||||||
vec![get("left", "a"), get("left", "b")],
|
|
||||||
vec![get("right", "a"), get("right", "b")],
|
|
||||||
25,
|
|
||||||
);
|
|
||||||
|
|
||||||
let frightened_texture = BlinkingTexture::new(
|
|
||||||
AnimatedTexture::new(
|
|
||||||
vec![
|
|
||||||
get_atlas_tile(&atlas, "ghost/frightened/blue_a.png"),
|
|
||||||
get_atlas_tile(&atlas, "ghost/frightened/blue_b.png"),
|
|
||||||
],
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
45,
|
|
||||||
15,
|
|
||||||
);
|
|
||||||
|
|
||||||
let eyes_get = |dir: &str| get_atlas_tile(&atlas, &format!("ghost/eyes/{dir}.png"));
|
|
||||||
|
|
||||||
let eyes_texture = DirectionalAnimatedTexture::new(
|
|
||||||
vec![eyes_get("up")],
|
|
||||||
vec![eyes_get("down")],
|
|
||||||
vec![eyes_get("left")],
|
|
||||||
vec![eyes_get("right")],
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ghost {
|
|
||||||
base: MovableEntity::new(
|
|
||||||
pixel_position,
|
|
||||||
starting_position,
|
|
||||||
Direction::Left,
|
|
||||||
SimpleTickModulator::new(0.9375),
|
|
||||||
map,
|
|
||||||
),
|
|
||||||
mode: GhostMode::House(HouseMode::Waiting),
|
|
||||||
ghost_type,
|
|
||||||
pacman,
|
|
||||||
texture,
|
|
||||||
frightened_texture,
|
|
||||||
eyes_texture,
|
|
||||||
house_offset,
|
|
||||||
current_house_offset: house_offset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the target tile for this ghost based on its current mode
|
|
||||||
pub fn get_target_tile(&self) -> Option<IVec2> {
|
|
||||||
match self.mode {
|
|
||||||
GhostMode::Scatter => Some(self.get_scatter_target()),
|
|
||||||
GhostMode::Chase => Some(self.get_chase_target()),
|
|
||||||
GhostMode::Frightened => Some(self.get_random_target()),
|
|
||||||
GhostMode::Eyes => Some(self.get_house_target()),
|
|
||||||
GhostMode::House(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets this ghost's home corner target for scatter mode
|
|
||||||
fn get_scatter_target(&self) -> IVec2 {
|
|
||||||
match self.ghost_type {
|
|
||||||
GhostType::Blinky => IVec2::new(25, 0), // Top right
|
|
||||||
GhostType::Pinky => IVec2::new(2, 0), // Top left
|
|
||||||
GhostType::Inky => IVec2::new(27, 35), // Bottom right
|
|
||||||
GhostType::Clyde => IVec2::new(0, 35), // Bottom left
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a random adjacent tile for frightened mode
|
|
||||||
fn get_random_target(&self) -> IVec2 {
|
|
||||||
let mut rng = SmallRng::from_os_rng();
|
|
||||||
let mut possible_moves = Vec::new();
|
|
||||||
|
|
||||||
// Check all four directions
|
|
||||||
for dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
|
|
||||||
// Don't allow reversing direction
|
|
||||||
if *dir == self.base.direction.opposite() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let next_cell = self.base.next_cell(Some(*dir));
|
|
||||||
if !matches!(self.base.map.borrow().get_tile(next_cell), Some(MapTile::Wall)) {
|
|
||||||
possible_moves.push(next_cell);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if possible_moves.is_empty() {
|
|
||||||
// No valid moves, must reverse
|
|
||||||
self.base.next_cell(Some(self.base.direction.opposite()))
|
|
||||||
} else {
|
|
||||||
// Choose a random valid move
|
|
||||||
possible_moves[rng.random_range(0..possible_moves.len())]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the ghost house target for returning eyes
|
|
||||||
fn get_house_target(&self) -> IVec2 {
|
|
||||||
IVec2::new(13, 14) // Center of ghost house
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets this ghost's chase mode target based on its personality
|
|
||||||
fn get_chase_target(&self) -> IVec2 {
|
|
||||||
let pacman = self.pacman.borrow();
|
|
||||||
let pacman_cell = pacman.base().cell_position;
|
|
||||||
let pacman_direction = pacman.base.direction;
|
|
||||||
|
|
||||||
match self.ghost_type {
|
|
||||||
GhostType::Blinky => {
|
|
||||||
// Blinky (Red) - Directly targets Pac-Man's current position
|
|
||||||
IVec2::new(pacman_cell.x as i32, pacman_cell.y as i32)
|
|
||||||
}
|
|
||||||
GhostType::Pinky => {
|
|
||||||
// Pinky (Pink) - Targets 4 cells ahead of Pac-Man in his direction
|
|
||||||
let offset = pacman_direction.offset();
|
|
||||||
let target_x = (pacman_cell.x as i32) + (offset.x * 4);
|
|
||||||
let target_y = (pacman_cell.y as i32) + (offset.y * 4);
|
|
||||||
IVec2::new(target_x, target_y)
|
|
||||||
}
|
|
||||||
GhostType::Inky => {
|
|
||||||
// Inky (Cyan) - Uses Blinky's position and Pac-Man's position to calculate target
|
|
||||||
// For now, just target Pac-Man with some randomness
|
|
||||||
let mut rng = SmallRng::from_os_rng();
|
|
||||||
let random_offset_x = rng.random_range(-2..=2);
|
|
||||||
let random_offset_y = rng.random_range(-2..=2);
|
|
||||||
IVec2::new(
|
|
||||||
(pacman_cell.x as i32) + random_offset_x,
|
|
||||||
(pacman_cell.y as i32) + random_offset_y,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
GhostType::Clyde => {
|
|
||||||
// Clyde (Orange) - Targets Pac-Man when far, runs to scatter corner when close
|
|
||||||
let distance = ((self.base.base.cell_position.x as i32 - pacman_cell.x as i32).pow(2)
|
|
||||||
+ (self.base.base.cell_position.y as i32 - pacman_cell.y as i32).pow(2))
|
|
||||||
as f32;
|
|
||||||
let distance = distance.sqrt();
|
|
||||||
|
|
||||||
if distance > 8.0 {
|
|
||||||
// Far from Pac-Man - chase
|
|
||||||
IVec2::new(pacman_cell.x as i32, pacman_cell.y as i32)
|
|
||||||
} else {
|
|
||||||
// Close to Pac-Man - scatter to bottom left
|
|
||||||
IVec2::new(0, 35)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the path to the target tile using the A* algorithm.
|
|
||||||
pub fn get_path_to_target(&self, target: UVec2) -> Option<(Vec<UVec2>, u32)> {
|
|
||||||
let start = self.base.base.cell_position;
|
|
||||||
let map = self.base.map.borrow();
|
|
||||||
use pathfinding::prelude::dijkstra;
|
|
||||||
dijkstra(
|
|
||||||
&start,
|
|
||||||
|&p| {
|
|
||||||
let mut successors = vec![];
|
|
||||||
let tile = map.get_tile(IVec2::new(p.x as i32, p.y as i32));
|
|
||||||
// Tunnel wrap: if currently in a tunnel, add the opposite exit as a neighbor
|
|
||||||
if let Some(MapTile::Tunnel) = tile {
|
|
||||||
if p.x == 0 {
|
|
||||||
successors.push((UVec2::new(BOARD_CELL_SIZE.x - 2, p.y), 1));
|
|
||||||
} else if p.x == BOARD_CELL_SIZE.x - 1 {
|
|
||||||
successors.push((UVec2::new(1, p.y), 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for dir in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
|
|
||||||
let offset = dir.offset();
|
|
||||||
let next_p = IVec2::new(p.x as i32 + offset.x, p.y as i32 + offset.y);
|
|
||||||
if let Some(tile) = map.get_tile(next_p) {
|
|
||||||
if tile == MapTile::Wall {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let next_u = UVec2::new(next_p.x as u32, next_p.y as u32);
|
|
||||||
successors.push((next_u, 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
successors
|
|
||||||
},
|
|
||||||
|&p| p == target,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Changes the ghost's mode and handles direction reversal
|
|
||||||
pub fn set_mode(&mut self, new_mode: GhostMode) {
|
|
||||||
// Don't reverse if going to/from frightened or if in house
|
|
||||||
let should_reverse = !matches!(self.mode, GhostMode::House(_))
|
|
||||||
&& !matches!(new_mode, GhostMode::House(_))
|
|
||||||
&& !matches!(self.mode, GhostMode::Frightened)
|
|
||||||
&& !matches!(new_mode, GhostMode::Frightened);
|
|
||||||
|
|
||||||
self.mode = new_mode;
|
|
||||||
|
|
||||||
self.base.speed.set_speed(match new_mode {
|
|
||||||
GhostMode::Chase => 0.9375,
|
|
||||||
GhostMode::Scatter => 0.85,
|
|
||||||
GhostMode::Frightened => 0.7,
|
|
||||||
GhostMode::Eyes => 1.5,
|
|
||||||
GhostMode::House(_) => 0.7,
|
|
||||||
});
|
|
||||||
|
|
||||||
if should_reverse {
|
|
||||||
self.base.set_direction_if_valid(self.base.direction.opposite());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tick(&mut self) {
|
|
||||||
if let GhostMode::House(house_mode) = self.mode {
|
|
||||||
match house_mode {
|
|
||||||
HouseMode::Waiting => {
|
|
||||||
// Ghosts in waiting mode move up and down
|
|
||||||
if self.base.is_grid_aligned() {
|
|
||||||
self.base.update_cell_position();
|
|
||||||
|
|
||||||
// Simple up and down movement
|
|
||||||
let current_pos = self.base.base.cell_position;
|
|
||||||
let start_pos = UVec2::new(13, 14); // Center of ghost house
|
|
||||||
|
|
||||||
if current_pos.y > start_pos.y + 1 {
|
|
||||||
// Too far down, move up
|
|
||||||
self.base.set_direction_if_valid(Direction::Up);
|
|
||||||
} else if current_pos.y < start_pos.y - 1 {
|
|
||||||
// Too far up, move down
|
|
||||||
self.base.set_direction_if_valid(Direction::Down);
|
|
||||||
} else if self.base.direction == Direction::Up {
|
|
||||||
// At top, switch to down
|
|
||||||
self.base.set_direction_if_valid(Direction::Down);
|
|
||||||
} else if self.base.direction == Direction::Down {
|
|
||||||
// At bottom, switch to up
|
|
||||||
self.base.set_direction_if_valid(Direction::Up);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HouseMode::Exiting => {
|
|
||||||
// Ghosts exiting move towards the exit
|
|
||||||
if self.base.is_grid_aligned() {
|
|
||||||
self.base.update_cell_position();
|
|
||||||
|
|
||||||
let exit_pos = UVec2::new(13, 11);
|
|
||||||
let current_pos = self.base.base.cell_position;
|
|
||||||
|
|
||||||
// Determine direction to exit
|
|
||||||
if current_pos.y > exit_pos.y {
|
|
||||||
// Need to move up
|
|
||||||
self.base.set_direction_if_valid(Direction::Up);
|
|
||||||
} else if current_pos.y == exit_pos.y && current_pos.x != exit_pos.x {
|
|
||||||
// At exit level, move horizontally to center
|
|
||||||
if current_pos.x < exit_pos.x {
|
|
||||||
self.base.set_direction_if_valid(Direction::Right);
|
|
||||||
} else {
|
|
||||||
self.base.set_direction_if_valid(Direction::Left);
|
|
||||||
}
|
|
||||||
} else if current_pos == exit_pos {
|
|
||||||
// Reached exit, transition to chase mode
|
|
||||||
self.mode = GhostMode::Chase;
|
|
||||||
self.current_house_offset = 0; // Reset offset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HouseMode::Entering => {
|
|
||||||
// Ghosts entering move towards their starting position
|
|
||||||
if self.base.is_grid_aligned() {
|
|
||||||
self.base.update_cell_position();
|
|
||||||
|
|
||||||
let start_pos = UVec2::new(13, 14); // Center of ghost house
|
|
||||||
let current_pos = self.base.base.cell_position;
|
|
||||||
|
|
||||||
// Determine direction to starting position
|
|
||||||
if current_pos.y < start_pos.y {
|
|
||||||
// Need to move down
|
|
||||||
self.base.set_direction_if_valid(Direction::Down);
|
|
||||||
} else if current_pos.y == start_pos.y && current_pos.x != start_pos.x {
|
|
||||||
// At house level, move horizontally to center
|
|
||||||
if current_pos.x < start_pos.x {
|
|
||||||
self.base.set_direction_if_valid(Direction::Right);
|
|
||||||
} else {
|
|
||||||
self.base.set_direction_if_valid(Direction::Left);
|
|
||||||
}
|
|
||||||
} else if current_pos == start_pos {
|
|
||||||
// Reached starting position, switch to waiting
|
|
||||||
self.mode = GhostMode::House(HouseMode::Waiting);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update house offset for smooth transitions
|
|
||||||
if self.current_house_offset != 0 {
|
|
||||||
// Gradually reduce offset when turning
|
|
||||||
if self.base.direction == Direction::Left || self.base.direction == Direction::Right {
|
|
||||||
if self.current_house_offset > 0 {
|
|
||||||
self.current_house_offset -= 1;
|
|
||||||
} else if self.current_house_offset < 0 {
|
|
||||||
self.current_house_offset += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.base.tick();
|
|
||||||
self.texture.tick();
|
|
||||||
self.frightened_texture.tick();
|
|
||||||
self.eyes_texture.tick();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normal ghost behavior
|
|
||||||
if self.base.is_grid_aligned() {
|
|
||||||
self.base.update_cell_position();
|
|
||||||
if !self.base.handle_tunnel() {
|
|
||||||
// Pathfinding logic (only if not in tunnel)
|
|
||||||
if let Some(target_tile) = self.get_target_tile() {
|
|
||||||
if let Some((path, _)) = self.get_path_to_target(target_tile.as_uvec2()) {
|
|
||||||
if path.len() > 1 {
|
|
||||||
let next_move = path[1];
|
|
||||||
let x = self.base.base.cell_position.x;
|
|
||||||
let y = self.base.base.cell_position.y;
|
|
||||||
let dx = next_move.x as i32 - x as i32;
|
|
||||||
let dy = next_move.y as i32 - y as i32;
|
|
||||||
let new_direction = if dx > 0 {
|
|
||||||
Direction::Right
|
|
||||||
} else if dx < 0 {
|
|
||||||
Direction::Left
|
|
||||||
} else if dy > 0 {
|
|
||||||
Direction::Down
|
|
||||||
} else {
|
|
||||||
Direction::Up
|
|
||||||
};
|
|
||||||
self.base.set_direction_if_valid(new_direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle house offset transition when turning
|
|
||||||
if self.current_house_offset != 0 {
|
|
||||||
if self.base.direction == Direction::Left || self.base.direction == Direction::Right {
|
|
||||||
if self.current_house_offset > 0 {
|
|
||||||
self.current_house_offset -= 1;
|
|
||||||
} else if self.current_house_offset < 0 {
|
|
||||||
self.current_house_offset += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.base.tick(); // Handles wall collision and movement
|
|
||||||
self.texture.tick();
|
|
||||||
self.frightened_texture.tick();
|
|
||||||
self.eyes_texture.tick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Moving for Ghost {
|
|
||||||
fn tick_movement(&mut self) {
|
|
||||||
self.base.tick_movement();
|
|
||||||
}
|
|
||||||
fn tick(&mut self) {
|
|
||||||
self.base.tick();
|
|
||||||
}
|
|
||||||
fn update_cell_position(&mut self) {
|
|
||||||
self.base.update_cell_position();
|
|
||||||
}
|
|
||||||
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
|
|
||||||
self.base.next_cell(direction)
|
|
||||||
}
|
|
||||||
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
|
|
||||||
self.base.is_wall_ahead(direction)
|
|
||||||
}
|
|
||||||
fn handle_tunnel(&mut self) -> bool {
|
|
||||||
self.base.handle_tunnel()
|
|
||||||
}
|
|
||||||
fn is_grid_aligned(&self) -> bool {
|
|
||||||
self.base.is_grid_aligned()
|
|
||||||
}
|
|
||||||
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
|
|
||||||
self.base.set_direction_if_valid(new_direction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Renderable for Ghost {
|
|
||||||
fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> {
|
|
||||||
let mut pos = self.base.base.pixel_position;
|
|
||||||
let dir = self.base.direction;
|
|
||||||
|
|
||||||
// Apply house offset if in house mode or transitioning
|
|
||||||
if matches!(self.mode, GhostMode::House(_)) || self.current_house_offset != 0 {
|
|
||||||
pos.x += self.current_house_offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.mode {
|
|
||||||
GhostMode::Frightened => {
|
|
||||||
let tile = self.frightened_texture.animation.current_tile();
|
|
||||||
let dest = sdl2::rect::Rect::new(pos.x - 4, pos.y - 4, tile.size.x as u32, tile.size.y as u32);
|
|
||||||
self.frightened_texture.render(canvas, dest)
|
|
||||||
}
|
|
||||||
GhostMode::Eyes => {
|
|
||||||
let tile = self.eyes_texture.up.first().unwrap();
|
|
||||||
let dest = sdl2::rect::Rect::new(pos.x - 4, pos.y - 4, tile.size.x as u32, tile.size.y as u32);
|
|
||||||
self.eyes_texture.render(canvas, dest, dir)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let tile = self.texture.up.first().unwrap();
|
|
||||||
let dest = sdl2::rect::Rect::new(pos.x - 4, pos.y - 4, tile.size.x as u32, tile.size.y as u32);
|
|
||||||
self.texture.render(canvas, dest, dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
274
src/entity/graph.rs
Normal file
274
src/entity/graph.rs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
use glam::Vec2;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use super::direction::Direction;
|
||||||
|
|
||||||
|
/// A unique identifier for a node, represented by its index in the graph's storage.
|
||||||
|
pub type NodeId = usize;
|
||||||
|
|
||||||
|
/// Represents a directed edge from one node to another with a given weight (e.g., distance).
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Edge {
|
||||||
|
pub target: NodeId,
|
||||||
|
pub distance: f32,
|
||||||
|
pub direction: Direction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Node {
|
||||||
|
pub position: Vec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A generic, arena-based graph.
|
||||||
|
/// The graph owns all node data and connection information.
|
||||||
|
pub struct Graph {
|
||||||
|
nodes: Vec<Node>,
|
||||||
|
adjacency_list: Vec<SmallVec<[Edge; 4]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Graph {
|
||||||
|
/// Creates a new, empty graph.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Graph {
|
||||||
|
nodes: Vec::new(),
|
||||||
|
adjacency_list: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new node with the given data to the graph and returns its ID.
|
||||||
|
pub fn add_node(&mut self, data: Node) -> NodeId {
|
||||||
|
let id = self.nodes.len();
|
||||||
|
self.nodes.push(data);
|
||||||
|
self.adjacency_list.push(SmallVec::new());
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a directed edge between two nodes.
|
||||||
|
pub fn add_edge(
|
||||||
|
&mut self,
|
||||||
|
from: NodeId,
|
||||||
|
to: NodeId,
|
||||||
|
distance: Option<f32>,
|
||||||
|
direction: Direction,
|
||||||
|
) -> Result<(), &'static str> {
|
||||||
|
let edge = Edge {
|
||||||
|
target: to,
|
||||||
|
distance: match distance {
|
||||||
|
Some(distance) => {
|
||||||
|
if distance <= 0.0 {
|
||||||
|
return Err("Edge distance must be positive.");
|
||||||
|
}
|
||||||
|
distance
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// If no distance is provided, calculate it based on the positions of the nodes
|
||||||
|
let from_pos = self.nodes[from].position;
|
||||||
|
let to_pos = self.nodes[to].position;
|
||||||
|
from_pos.distance(to_pos)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
direction,
|
||||||
|
};
|
||||||
|
|
||||||
|
if from >= self.adjacency_list.len() {
|
||||||
|
return Err("From node does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjacency_list = &mut self.adjacency_list[from];
|
||||||
|
|
||||||
|
// Check if the edge already exists in this direction or to the same target
|
||||||
|
if let Some(err) = adjacency_list.iter().find_map(|e| {
|
||||||
|
if e.direction == direction {
|
||||||
|
Some(Err("Edge already exists in this direction."))
|
||||||
|
} else if e.target == to {
|
||||||
|
Some(Err("Edge already exists."))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
adjacency_list.push(edge);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves an immutable reference to a node's data.
|
||||||
|
pub fn get_node(&self, id: NodeId) -> Option<&Node> {
|
||||||
|
self.nodes.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn node_count(&self) -> usize {
|
||||||
|
self.nodes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a specific edge from a source node to a target node.
|
||||||
|
pub fn find_edge(&self, from: NodeId, to: NodeId) -> Option<&Edge> {
|
||||||
|
self.adjacency_list.get(from)?.iter().find(|edge| edge.target == to)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_edge_in_direction(&self, from: NodeId, direction: Direction) -> Option<&Edge> {
|
||||||
|
self.adjacency_list.get(from)?.iter().find(|edge| edge.direction == direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default implementation for creating an empty graph.
|
||||||
|
impl Default for Graph {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Traversal State and Logic ---
|
||||||
|
|
||||||
|
/// Represents the traverser's current position within the graph.
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||||
|
pub enum Position {
|
||||||
|
/// The traverser is located exactly at a node.
|
||||||
|
AtNode(NodeId),
|
||||||
|
/// The traverser is on an edge between two nodes.
|
||||||
|
BetweenNodes {
|
||||||
|
from: NodeId,
|
||||||
|
to: NodeId,
|
||||||
|
/// The floating-point distance traversed along the edge from the `from` node.
|
||||||
|
traversed: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Position {
|
||||||
|
pub fn is_at_node(&self) -> bool {
|
||||||
|
matches!(self, Position::AtNode(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_node_id(&self) -> NodeId {
|
||||||
|
match self {
|
||||||
|
Position::AtNode(id) => *id,
|
||||||
|
Position::BetweenNodes { from, .. } => *from,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_node_id(&self) -> Option<NodeId> {
|
||||||
|
match self {
|
||||||
|
Position::AtNode(_) => None,
|
||||||
|
Position::BetweenNodes { to, .. } => Some(*to),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_stopped(&self) -> bool {
|
||||||
|
matches!(self, Position::AtNode(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages a traversal session over a graph.
|
||||||
|
/// It holds a reference to the graph and the current position state.
|
||||||
|
pub struct Traverser {
|
||||||
|
pub position: Position,
|
||||||
|
pub direction: Direction,
|
||||||
|
pub next_direction: Option<(Direction, u8)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Traverser {
|
||||||
|
/// Creates a new traverser starting at the given node ID.
|
||||||
|
pub fn new(graph: &Graph, start_node: NodeId, initial_direction: Direction) -> Self {
|
||||||
|
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
|
||||||
|
traverser.advance(graph, 0.0);
|
||||||
|
|
||||||
|
traverser
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_next_direction(&mut self, new_direction: Direction) {
|
||||||
|
if self.direction != new_direction {
|
||||||
|
self.next_direction = Some((new_direction, 30));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance(&mut self, graph: &Graph, distance: f32) {
|
||||||
|
// 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) {
|
||||||
|
// Start moving in that direction
|
||||||
|
self.position = Position::BetweenNodes {
|
||||||
|
from: node_id,
|
||||||
|
to: edge.target,
|
||||||
|
traversed: distance.max(0.0),
|
||||||
|
};
|
||||||
|
self.direction = next_direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let edge = graph
|
||||||
|
.find_edge(from, to)
|
||||||
|
.expect("Inconsistent state: Traverser is on a non-existent edge.");
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
self.position = Position::BetweenNodes {
|
||||||
|
from: to,
|
||||||
|
to: edge.target,
|
||||||
|
traversed: overflow,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.position = Position::AtNode(to);
|
||||||
|
self.next_direction = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,205 +1,3 @@
|
|||||||
pub mod direction;
|
pub mod direction;
|
||||||
pub mod edible;
|
pub mod graph;
|
||||||
pub mod ghost;
|
|
||||||
pub mod pacman;
|
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<()>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,143 +1,93 @@
|
|||||||
//! This module defines the Pac-Man entity, including its behavior and rendering.
|
use glam::Vec2;
|
||||||
use anyhow::Result;
|
|
||||||
use glam::{IVec2, UVec2};
|
|
||||||
use sdl2::render::WindowCanvas;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::constants::BOARD_PIXEL_OFFSET;
|
||||||
entity::speed::SimpleTickModulator,
|
use crate::entity::direction::Direction;
|
||||||
entity::{direction::Direction, Entity, MovableEntity, Moving, QueuedDirection, Renderable, StaticEntity},
|
use crate::entity::graph::{Graph, NodeId, Position, Traverser};
|
||||||
map::Map,
|
use crate::texture::animated::AnimatedTexture;
|
||||||
texture::{animated::AnimatedTexture, directional::DirectionalAnimatedTexture, get_atlas_tile, sprite::SpriteAtlas},
|
use crate::texture::directional::DirectionalAnimatedTexture;
|
||||||
};
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
|
use sdl2::keyboard::Keycode;
|
||||||
|
use sdl2::rect::Rect;
|
||||||
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// The Pac-Man entity.
|
|
||||||
pub struct Pacman {
|
pub struct Pacman {
|
||||||
/// Shared movement and position fields.
|
pub traverser: Traverser,
|
||||||
pub base: MovableEntity,
|
texture: DirectionalAnimatedTexture,
|
||||||
/// The next direction of Pac-Man, which will be applied when Pac-Man is next aligned with the grid.
|
|
||||||
pub next_direction: Option<Direction>,
|
|
||||||
/// Whether Pac-Man is currently stopped.
|
|
||||||
pub stopped: bool,
|
|
||||||
pub skip_move_tick: bool,
|
|
||||||
pub texture: DirectionalAnimatedTexture,
|
|
||||||
pub death_animation: AnimatedTexture,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Entity for Pacman {
|
|
||||||
fn base(&self) -> &StaticEntity {
|
|
||||||
&self.base.base
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Moving for Pacman {
|
|
||||||
fn tick_movement(&mut self) {
|
|
||||||
if self.skip_move_tick {
|
|
||||||
self.skip_move_tick = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.base.tick_movement();
|
|
||||||
}
|
|
||||||
fn update_cell_position(&mut self) {
|
|
||||||
self.base.update_cell_position();
|
|
||||||
}
|
|
||||||
fn next_cell(&self, direction: Option<Direction>) -> IVec2 {
|
|
||||||
self.base.next_cell(direction)
|
|
||||||
}
|
|
||||||
fn is_wall_ahead(&self, direction: Option<Direction>) -> bool {
|
|
||||||
self.base.is_wall_ahead(direction)
|
|
||||||
}
|
|
||||||
fn handle_tunnel(&mut self) -> bool {
|
|
||||||
self.base.handle_tunnel()
|
|
||||||
}
|
|
||||||
fn is_grid_aligned(&self) -> bool {
|
|
||||||
self.base.is_grid_aligned()
|
|
||||||
}
|
|
||||||
fn set_direction_if_valid(&mut self, new_direction: Direction) -> bool {
|
|
||||||
self.base.set_direction_if_valid(new_direction)
|
|
||||||
}
|
|
||||||
fn on_grid_aligned(&mut self) {
|
|
||||||
Pacman::update_cell_position(self);
|
|
||||||
if !<Pacman as Moving>::handle_tunnel(self) {
|
|
||||||
<Pacman as QueuedDirection>::handle_direction_change(self);
|
|
||||||
if !self.stopped && <Pacman as Moving>::is_wall_ahead(self, None) {
|
|
||||||
self.stopped = true;
|
|
||||||
} else if self.stopped && !<Pacman as Moving>::is_wall_ahead(self, None) {
|
|
||||||
self.stopped = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QueuedDirection for Pacman {
|
|
||||||
fn next_direction(&self) -> Option<Direction> {
|
|
||||||
self.next_direction
|
|
||||||
}
|
|
||||||
fn set_next_direction(&mut self, dir: Option<Direction>) {
|
|
||||||
self.next_direction = dir;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pacman {
|
impl Pacman {
|
||||||
/// Creates a new `Pacman` instance.
|
pub fn new(graph: &Graph, start_node: NodeId, atlas: &SpriteAtlas) -> Self {
|
||||||
pub fn new(starting_position: UVec2, atlas: Rc<RefCell<SpriteAtlas>>, map: Rc<RefCell<Map>>) -> Pacman {
|
let mut textures = HashMap::new();
|
||||||
let pixel_position = Map::cell_to_pixel(starting_position);
|
let mut stopped_textures = HashMap::new();
|
||||||
let get = |name: &str| get_atlas_tile(&atlas, name);
|
|
||||||
|
|
||||||
Pacman {
|
for &direction in &[Direction::Up, Direction::Down, Direction::Left, Direction::Right] {
|
||||||
base: MovableEntity::new(
|
let moving_prefix = match direction {
|
||||||
pixel_position,
|
Direction::Up => "pacman/up",
|
||||||
starting_position,
|
Direction::Down => "pacman/down",
|
||||||
Direction::Right,
|
Direction::Left => "pacman/left",
|
||||||
SimpleTickModulator::new(1f32),
|
Direction::Right => "pacman/right",
|
||||||
map,
|
};
|
||||||
),
|
let moving_tiles = vec![
|
||||||
next_direction: None,
|
SpriteAtlas::get_tile(&atlas, &format!("{}_a.png", moving_prefix)).unwrap(),
|
||||||
stopped: false,
|
SpriteAtlas::get_tile(&atlas, &format!("{}_b.png", moving_prefix)).unwrap(),
|
||||||
skip_move_tick: false,
|
SpriteAtlas::get_tile(&atlas, "pacman/full.png").unwrap(),
|
||||||
texture: DirectionalAnimatedTexture::new(
|
];
|
||||||
vec![get("pacman/up_a.png"), get("pacman/up_b.png"), get("pacman/full.png")],
|
|
||||||
vec![get("pacman/down_a.png"), get("pacman/down_b.png"), get("pacman/full.png")],
|
let stopped_tiles = vec![SpriteAtlas::get_tile(&atlas, &format!("{}_b.png", moving_prefix)).unwrap()];
|
||||||
vec![get("pacman/left_a.png"), get("pacman/left_b.png"), get("pacman/full.png")],
|
|
||||||
vec![get("pacman/right_a.png"), get("pacman/right_b.png"), get("pacman/full.png")],
|
textures.insert(direction, AnimatedTexture::new(moving_tiles, 0.08));
|
||||||
8,
|
stopped_textures.insert(direction, AnimatedTexture::new(stopped_tiles, 0.1));
|
||||||
),
|
}
|
||||||
death_animation: AnimatedTexture::new(
|
|
||||||
(0..=10)
|
Self {
|
||||||
.map(|i| get_atlas_tile(&atlas, &format!("pacman/death/{i}.png")))
|
traverser: Traverser::new(graph, start_node, Direction::Left),
|
||||||
.collect(),
|
texture: DirectionalAnimatedTexture::new(textures, stopped_textures),
|
||||||
5,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the internal position of Pac-Man, rounded down to the nearest even number.
|
pub fn tick(&mut self, dt: f32, graph: &Graph) {
|
||||||
fn internal_position_even(&self) -> UVec2 {
|
self.traverser.advance(graph, dt * 60.0 * 1.125);
|
||||||
let pos = self.base.internal_position();
|
self.texture.tick(dt);
|
||||||
UVec2::new((pos.x / 2) * 2, (pos.y / 2) * 2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick(&mut self) {
|
pub fn handle_key(&mut self, keycode: Keycode) {
|
||||||
<Pacman as Moving>::tick(self);
|
let direction = match keycode {
|
||||||
self.texture.tick();
|
Keycode::Up => Some(Direction::Up),
|
||||||
|
Keycode::Down => Some(Direction::Down),
|
||||||
|
Keycode::Left => Some(Direction::Left),
|
||||||
|
Keycode::Right => Some(Direction::Right),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(direction) = direction {
|
||||||
|
self.traverser.set_next_direction(direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderable for Pacman {
|
fn get_pixel_pos(&self, graph: &Graph) -> Vec2 {
|
||||||
fn render(&mut self, canvas: &mut WindowCanvas) -> Result<()> {
|
match self.traverser.position {
|
||||||
let pos = self.base.base.pixel_position;
|
Position::AtNode(node_id) => graph.get_node(node_id).unwrap().position,
|
||||||
let dir = self.base.direction;
|
Position::BetweenNodes { from, to, traversed } => {
|
||||||
|
let from_pos = graph.get_node(from).unwrap().position;
|
||||||
|
let to_pos = graph.get_node(to).unwrap().position;
|
||||||
|
let weight = from_pos.distance(to_pos);
|
||||||
|
from_pos.lerp(to_pos, traversed / weight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Center the 16x16 sprite on the 8x8 cell by offsetting by -4
|
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, graph: &Graph) {
|
||||||
let dest = sdl2::rect::Rect::new(pos.x - 4, pos.y - 4, 16, 16);
|
let pixel_pos = self.get_pixel_pos(graph).round().as_ivec2() + BOARD_PIXEL_OFFSET.as_ivec2();
|
||||||
|
let dest = Rect::new(pixel_pos.x - 8, pixel_pos.y - 8, 16, 16);
|
||||||
|
let is_stopped = self.traverser.position.is_stopped();
|
||||||
|
|
||||||
if self.stopped {
|
if is_stopped {
|
||||||
// When stopped, show the full sprite (mouth open)
|
self.texture
|
||||||
self.texture.render_stopped(canvas, dest, dir)?;
|
.render_stopped(canvas, atlas, dest, self.traverser.direction)
|
||||||
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
self.texture.render(canvas, dest, dir)?;
|
self.texture.render(canvas, atlas, dest, self.traverser.direction).unwrap();
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
//! This module provides a tick modulator, which can be used to slow down
|
|
||||||
//! operations by a percentage.
|
|
||||||
/// A tick modulator allows you to slow down operations by a percentage.
|
|
||||||
///
|
|
||||||
/// Unfortunately, switching to floating point numbers for entities can induce floating point errors, slow down calculations
|
|
||||||
/// and make the game less deterministic. This is why we use a speed modulator instead.
|
|
||||||
/// Additionally, with small integers, lowering the speed by a percentage is not possible. For example, if we have a speed of 2,
|
|
||||||
/// and we want to slow it down by 10%, we would need to slow it down by 0.2. However, since we are using integers, we can't.
|
|
||||||
/// The only amount you can slow it down by is 1, which is 50% of the speed.
|
|
||||||
///
|
|
||||||
/// The basic principle of the Speed Modulator is to instead 'skip' movement ticks every now and then.
|
|
||||||
/// At 60 ticks per second, skips could happen several times per second, or once every few seconds.
|
|
||||||
/// Whatever it be, as long as the tick rate is high enough, the human eye will not be able to tell the difference.
|
|
||||||
///
|
|
||||||
/// For example, if we want to slow down the speed by 10%, we would need to skip every 10th tick.
|
|
||||||
pub trait TickModulator {
|
|
||||||
/// Creates a new tick modulator.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `percent` - The percentage to slow down by, from 0.0 to 1.0.
|
|
||||||
fn new(percent: f32) -> Self;
|
|
||||||
/// Returns whether or not the operation should be performed on this tick.
|
|
||||||
fn next(&mut self) -> bool;
|
|
||||||
fn set_speed(&mut self, speed: f32);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A simple tick modulator that skips every Nth tick.
|
|
||||||
pub struct SimpleTickModulator {
|
|
||||||
accumulator: f32,
|
|
||||||
pixels_per_tick: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Add tests for the tick modulator to ensure that it is working correctly.
|
|
||||||
// TODO: Look into average precision and binary code modulation strategies to see
|
|
||||||
// if they would be a better fit for this use case.
|
|
||||||
impl SimpleTickModulator {
|
|
||||||
pub fn new(pixels_per_tick: f32) -> Self {
|
|
||||||
Self {
|
|
||||||
accumulator: 0f32,
|
|
||||||
pixels_per_tick: pixels_per_tick * 0.47,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn set_speed(&mut self, pixels_per_tick: f32) {
|
|
||||||
self.pixels_per_tick = pixels_per_tick;
|
|
||||||
}
|
|
||||||
pub fn next(&mut self) -> bool {
|
|
||||||
self.accumulator += self.pixels_per_tick;
|
|
||||||
if self.accumulator >= 1f32 {
|
|
||||||
self.accumulator -= 1f32;
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
391
src/game.rs
391
src/game.rs
@@ -1,71 +1,59 @@
|
|||||||
//! This module contains the main game logic and state.
|
//! This module contains the main game logic and state.
|
||||||
use std::cell::RefCell;
|
use std::time::{Duration, Instant};
|
||||||
use std::ops::Not;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use glam::UVec2;
|
use glam::UVec2;
|
||||||
|
use sdl2::{
|
||||||
|
image::LoadTexture,
|
||||||
|
keyboard::Keycode,
|
||||||
|
pixels::Color,
|
||||||
|
render::{Canvas, RenderTarget, Texture, TextureCreator},
|
||||||
|
video::WindowContext,
|
||||||
|
};
|
||||||
|
|
||||||
use sdl2::image::LoadTexture;
|
use crate::{
|
||||||
use sdl2::keyboard::Keycode;
|
asset::{get_asset_bytes, Asset},
|
||||||
|
audio::Audio,
|
||||||
use sdl2::render::{Texture, TextureCreator};
|
constants::RAW_BOARD,
|
||||||
use sdl2::video::WindowContext;
|
entity::pacman::Pacman,
|
||||||
use sdl2::{pixels::Color, render::Canvas, video::Window};
|
map::Map,
|
||||||
|
texture::{
|
||||||
use crate::asset::{get_asset_bytes, Asset};
|
sprite::{self, AtlasMapper, AtlasTile, SpriteAtlas},
|
||||||
use crate::audio::Audio;
|
text::TextTexture,
|
||||||
use crate::constants::RAW_BOARD;
|
},
|
||||||
use crate::debug::{DebugMode, DebugRenderer};
|
};
|
||||||
use crate::entity::direction::Direction;
|
|
||||||
use crate::entity::edible::{reconstruct_edibles, Edible, EdibleKind};
|
|
||||||
use crate::entity::ghost::{Ghost, GhostMode, GhostType, HouseMode};
|
|
||||||
use crate::entity::pacman::Pacman;
|
|
||||||
use crate::entity::Renderable;
|
|
||||||
use crate::map::Map;
|
|
||||||
use crate::texture::animated::AnimatedTexture;
|
|
||||||
use crate::texture::blinking::BlinkingTexture;
|
|
||||||
use crate::texture::sprite::{AtlasMapper, AtlasTile, SpriteAtlas};
|
|
||||||
use crate::texture::text::TextTexture;
|
|
||||||
use crate::texture::{get_atlas_tile, sprite};
|
|
||||||
|
|
||||||
/// The main game state.
|
/// The main game state.
|
||||||
///
|
///
|
||||||
/// Contains all the information necessary to run the game, including
|
/// Contains all the information necessary to run the game, including
|
||||||
/// the game state, rendering resources, and audio.
|
/// the game state, rendering resources, and audio.
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
// Game state
|
pub score: u32,
|
||||||
pacman: Rc<RefCell<Pacman>>,
|
pub map: Map,
|
||||||
blinky: Ghost,
|
pub pacman: Pacman,
|
||||||
pinky: Ghost,
|
pub debug_mode: bool,
|
||||||
inky: Ghost,
|
|
||||||
clyde: Ghost,
|
|
||||||
edibles: Vec<Edible>,
|
|
||||||
map: Rc<RefCell<Map>>,
|
|
||||||
score: u32,
|
|
||||||
debug_mode: DebugMode,
|
|
||||||
|
|
||||||
// FPS tracking
|
|
||||||
fps_1s: f64,
|
|
||||||
fps_10s: f64,
|
|
||||||
|
|
||||||
// Rendering resources
|
// Rendering resources
|
||||||
atlas: Rc<RefCell<SpriteAtlas>>,
|
atlas: SpriteAtlas,
|
||||||
map_texture: AtlasTile,
|
map_texture: AtlasTile,
|
||||||
text_texture: TextTexture,
|
text_texture: TextTexture,
|
||||||
|
debug_text_texture: TextTexture,
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
pub audio: Audio,
|
pub audio: Audio,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
/// Creates a new `Game` instance.
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
texture_creator: &TextureCreator<WindowContext>,
|
texture_creator: &TextureCreator<WindowContext>,
|
||||||
_ttf_context: &sdl2::ttf::Sdl2TtfContext,
|
_ttf_context: &sdl2::ttf::Sdl2TtfContext,
|
||||||
_audio_subsystem: &sdl2::AudioSubsystem,
|
_audio_subsystem: &sdl2::AudioSubsystem,
|
||||||
) -> Game {
|
) -> Game {
|
||||||
let map = Rc::new(RefCell::new(Map::new(RAW_BOARD)));
|
let map = Map::new(RAW_BOARD);
|
||||||
|
|
||||||
|
let _pacman_start_pos = map.find_starting_position(0).unwrap();
|
||||||
|
let pacman_start_node = 0; // TODO: Find the actual start node
|
||||||
|
|
||||||
let atlas_bytes = get_asset_bytes(Asset::Atlas).expect("Failed to load asset");
|
let atlas_bytes = get_asset_bytes(Asset::Atlas).expect("Failed to load asset");
|
||||||
let atlas_texture = unsafe {
|
let atlas_texture = unsafe {
|
||||||
let texture = texture_creator
|
let texture = texture_creator
|
||||||
@@ -75,318 +63,65 @@ impl Game {
|
|||||||
};
|
};
|
||||||
let atlas_json = get_asset_bytes(Asset::AtlasJson).expect("Failed to load asset");
|
let atlas_json = get_asset_bytes(Asset::AtlasJson).expect("Failed to load asset");
|
||||||
let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json).expect("Could not parse atlas JSON");
|
let atlas_mapper: AtlasMapper = serde_json::from_slice(&atlas_json).expect("Could not parse atlas JSON");
|
||||||
let atlas = Rc::new(RefCell::new(SpriteAtlas::new(atlas_texture, atlas_mapper)));
|
let atlas = SpriteAtlas::new(atlas_texture, atlas_mapper);
|
||||||
let pacman = Rc::new(RefCell::new(Pacman::new(
|
|
||||||
UVec2::new(1, 1),
|
|
||||||
Rc::clone(&atlas),
|
|
||||||
Rc::clone(&map),
|
|
||||||
)));
|
|
||||||
|
|
||||||
// Find starting positions
|
let mut map_texture = SpriteAtlas::get_tile(&atlas, "maze/full.png").expect("Failed to load map tile");
|
||||||
let pacman_start = map.borrow().find_starting_position(0).unwrap_or(UVec2::new(13, 23));
|
|
||||||
let blinky_start = map.borrow().find_starting_position(1).unwrap_or(UVec2::new(13, 11));
|
|
||||||
let pinky_start = map.borrow().find_starting_position(2).unwrap_or(UVec2::new(13, 14));
|
|
||||||
let inky_start = map.borrow().find_starting_position(3).unwrap_or(UVec2::new(13, 14));
|
|
||||||
let clyde_start = map.borrow().find_starting_position(4).unwrap_or(UVec2::new(13, 14));
|
|
||||||
|
|
||||||
// Update Pac-Man to proper starting position
|
|
||||||
{
|
|
||||||
let mut pacman_mut = pacman.borrow_mut();
|
|
||||||
pacman_mut.base.base.pixel_position = Map::cell_to_pixel(pacman_start);
|
|
||||||
pacman_mut.base.base.cell_position = pacman_start;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut blinky = Ghost::new(
|
|
||||||
GhostType::Blinky,
|
|
||||||
blinky_start,
|
|
||||||
Rc::clone(&atlas),
|
|
||||||
Rc::clone(&map),
|
|
||||||
Rc::clone(&pacman),
|
|
||||||
-8,
|
|
||||||
);
|
|
||||||
blinky.mode = GhostMode::Chase;
|
|
||||||
|
|
||||||
let mut pinky = Ghost::new(
|
|
||||||
GhostType::Pinky,
|
|
||||||
pinky_start,
|
|
||||||
Rc::clone(&atlas),
|
|
||||||
Rc::clone(&map),
|
|
||||||
Rc::clone(&pacman),
|
|
||||||
8,
|
|
||||||
);
|
|
||||||
pinky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting);
|
|
||||||
|
|
||||||
let mut inky = Ghost::new(
|
|
||||||
GhostType::Inky,
|
|
||||||
inky_start,
|
|
||||||
Rc::clone(&atlas),
|
|
||||||
Rc::clone(&map),
|
|
||||||
Rc::clone(&pacman),
|
|
||||||
-8,
|
|
||||||
);
|
|
||||||
inky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting);
|
|
||||||
|
|
||||||
let mut clyde = Ghost::new(
|
|
||||||
GhostType::Clyde,
|
|
||||||
clyde_start,
|
|
||||||
Rc::clone(&atlas),
|
|
||||||
Rc::clone(&map),
|
|
||||||
Rc::clone(&pacman),
|
|
||||||
8,
|
|
||||||
);
|
|
||||||
clyde.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting);
|
|
||||||
let mut map_texture = get_atlas_tile(&atlas, "maze/full.png");
|
|
||||||
map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9));
|
map_texture.color = Some(Color::RGB(0x20, 0x20, 0xf9));
|
||||||
|
|
||||||
let edibles = reconstruct_edibles(
|
let text_texture = TextTexture::new(1.0);
|
||||||
Rc::clone(&map),
|
let debug_text_texture = TextTexture::new(0.5);
|
||||||
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "maze/pellet.png")], 0),
|
|
||||||
BlinkingTexture::new(
|
|
||||||
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "maze/energizer.png")], 0),
|
|
||||||
17,
|
|
||||||
17,
|
|
||||||
),
|
|
||||||
AnimatedTexture::new(vec![get_atlas_tile(&atlas, "edible/cherry.png")], 0),
|
|
||||||
);
|
|
||||||
let text_texture = TextTexture::new(Rc::clone(&atlas), 1.0);
|
|
||||||
let audio = Audio::new();
|
let audio = Audio::new();
|
||||||
|
let pacman = Pacman::new(&map.graph, pacman_start_node, &atlas);
|
||||||
Game {
|
Game {
|
||||||
pacman,
|
|
||||||
blinky,
|
|
||||||
pinky,
|
|
||||||
inky,
|
|
||||||
clyde,
|
|
||||||
edibles,
|
|
||||||
map,
|
|
||||||
score: 0,
|
score: 0,
|
||||||
debug_mode: DebugMode::None,
|
map,
|
||||||
atlas,
|
pacman,
|
||||||
|
debug_mode: false,
|
||||||
map_texture,
|
map_texture,
|
||||||
text_texture,
|
text_texture,
|
||||||
|
debug_text_texture,
|
||||||
audio,
|
audio,
|
||||||
fps_1s: 0.0,
|
atlas,
|
||||||
fps_10s: 0.0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a keyboard event.
|
|
||||||
pub fn keyboard_event(&mut self, keycode: Keycode) {
|
pub fn keyboard_event(&mut self, keycode: Keycode) {
|
||||||
// Change direction
|
self.pacman.handle_key(keycode);
|
||||||
let direction = Direction::from_keycode(keycode);
|
|
||||||
if direction.is_some() {
|
|
||||||
self.pacman.borrow_mut().next_direction = direction;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle debug mode
|
|
||||||
if keycode == Keycode::Space {
|
|
||||||
self.debug_mode = match self.debug_mode {
|
|
||||||
DebugMode::None => DebugMode::Grid,
|
|
||||||
DebugMode::Grid => DebugMode::Pathfinding,
|
|
||||||
DebugMode::Pathfinding => DebugMode::ValidPositions,
|
|
||||||
DebugMode::ValidPositions => DebugMode::None,
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle mute
|
|
||||||
if keycode == Keycode::M {
|
if keycode == Keycode::M {
|
||||||
self.audio.set_mute(self.audio.is_muted().not());
|
self.audio.set_mute(!self.audio.is_muted());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset game
|
|
||||||
if keycode == Keycode::R {
|
|
||||||
self.reset();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds points to the score.
|
pub fn tick(&mut self, dt: f32) {
|
||||||
///
|
self.pacman.tick(dt, &self.map.graph);
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `points` - The number of points to add.
|
|
||||||
pub fn add_score(&mut self, points: u32) {
|
|
||||||
self.score += points;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the FPS tracking values.
|
pub fn draw<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &mut Texture) -> Result<()> {
|
||||||
pub fn update_fps(&mut self, fps_1s: f64, fps_10s: f64) {
|
canvas.with_texture_canvas(backbuffer, |canvas| {
|
||||||
self.fps_1s = fps_1s;
|
|
||||||
self.fps_10s = fps_10s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the game to its initial state.
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
// Reset the map to restore all pellets
|
|
||||||
{
|
|
||||||
let mut map = self.map.borrow_mut();
|
|
||||||
map.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset the score
|
|
||||||
self.score = 0;
|
|
||||||
|
|
||||||
// Reset entities to their proper starting positions
|
|
||||||
{
|
|
||||||
let map = self.map.borrow();
|
|
||||||
|
|
||||||
// Reset Pac-Man to proper starting position
|
|
||||||
if let Some(pacman_start) = map.find_starting_position(0) {
|
|
||||||
let mut pacman = self.pacman.borrow_mut();
|
|
||||||
pacman.base.base.pixel_position = Map::cell_to_pixel(pacman_start);
|
|
||||||
pacman.base.base.cell_position = pacman_start;
|
|
||||||
pacman.base.in_tunnel = false;
|
|
||||||
pacman.base.direction = Direction::Right;
|
|
||||||
pacman.next_direction = None;
|
|
||||||
pacman.stopped = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset ghosts to their starting positions
|
|
||||||
if let Some(blinky_start) = map.find_starting_position(1) {
|
|
||||||
self.blinky.base.base.pixel_position = Map::cell_to_pixel(blinky_start);
|
|
||||||
self.blinky.base.base.cell_position = blinky_start;
|
|
||||||
self.blinky.base.in_tunnel = false;
|
|
||||||
self.blinky.base.direction = Direction::Left;
|
|
||||||
self.blinky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(pinky_start) = map.find_starting_position(2) {
|
|
||||||
self.pinky.base.base.pixel_position = Map::cell_to_pixel(pinky_start);
|
|
||||||
self.pinky.base.base.cell_position = pinky_start;
|
|
||||||
self.pinky.base.in_tunnel = false;
|
|
||||||
self.pinky.base.direction = Direction::Down;
|
|
||||||
self.pinky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(inky_start) = map.find_starting_position(3) {
|
|
||||||
self.inky.base.base.pixel_position = Map::cell_to_pixel(inky_start);
|
|
||||||
self.inky.base.base.cell_position = inky_start;
|
|
||||||
self.inky.base.in_tunnel = false;
|
|
||||||
self.inky.base.direction = Direction::Up;
|
|
||||||
self.inky.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(clyde_start) = map.find_starting_position(4) {
|
|
||||||
self.clyde.base.base.pixel_position = Map::cell_to_pixel(clyde_start);
|
|
||||||
self.clyde.base.base.cell_position = clyde_start;
|
|
||||||
self.clyde.base.in_tunnel = false;
|
|
||||||
self.clyde.base.direction = Direction::Down;
|
|
||||||
self.clyde.mode = crate::entity::ghost::GhostMode::House(crate::entity::ghost::HouseMode::Waiting);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.edibles = reconstruct_edibles(
|
|
||||||
Rc::clone(&self.map),
|
|
||||||
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "maze/pellet.png")], 0),
|
|
||||||
BlinkingTexture::new(
|
|
||||||
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "maze/energizer.png")], 0),
|
|
||||||
12,
|
|
||||||
12,
|
|
||||||
),
|
|
||||||
AnimatedTexture::new(vec![get_atlas_tile(&self.atlas, "edible/cherry.png")], 0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Advances the game by one tick.
|
|
||||||
pub fn tick(&mut self) {
|
|
||||||
self.tick_entities();
|
|
||||||
self.handle_edible_collisions();
|
|
||||||
self.tick_entities();
|
|
||||||
}
|
|
||||||
fn tick_entities(&mut self) {
|
|
||||||
self.pacman.borrow_mut().tick();
|
|
||||||
self.blinky.tick();
|
|
||||||
self.pinky.tick();
|
|
||||||
self.inky.tick();
|
|
||||||
self.clyde.tick();
|
|
||||||
for edible in self.edibles.iter_mut() {
|
|
||||||
if let EdibleKind::PowerPellet = edible.kind {
|
|
||||||
if let crate::entity::edible::EdibleSprite::PowerPellet(texture) = &mut edible.sprite {
|
|
||||||
texture.tick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn handle_edible_collisions(&mut self) {
|
|
||||||
let pacman = self.pacman.borrow();
|
|
||||||
let mut eaten_indices = vec![];
|
|
||||||
for (i, edible) in self.edibles.iter().enumerate() {
|
|
||||||
if edible.collide(&*pacman) {
|
|
||||||
eaten_indices.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(pacman);
|
|
||||||
for &i in eaten_indices.iter().rev() {
|
|
||||||
let edible = &self.edibles[i];
|
|
||||||
match edible.kind {
|
|
||||||
EdibleKind::Pellet => {
|
|
||||||
self.add_score(10);
|
|
||||||
self.audio.eat();
|
|
||||||
}
|
|
||||||
EdibleKind::PowerPellet => {
|
|
||||||
self.add_score(50);
|
|
||||||
self.audio.eat();
|
|
||||||
}
|
|
||||||
EdibleKind::Fruit(_fruit) => {
|
|
||||||
self.add_score(100);
|
|
||||||
self.audio.eat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.edibles.remove(i);
|
|
||||||
// Set Pac-Man to skip the next movement tick
|
|
||||||
self.pacman.borrow_mut().skip_move_tick = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draws the entire game to the canvas using a backbuffer.
|
|
||||||
pub fn draw(&mut self, window_canvas: &mut Canvas<Window>, backbuffer: &mut Texture) -> Result<()> {
|
|
||||||
window_canvas
|
|
||||||
.with_texture_canvas(backbuffer, |texture_canvas| {
|
|
||||||
let this = self as *mut Self;
|
|
||||||
let this = unsafe { &mut *this };
|
|
||||||
texture_canvas.set_draw_color(Color::BLACK);
|
|
||||||
texture_canvas.clear();
|
|
||||||
this.map.borrow_mut().render(texture_canvas, &mut this.map_texture);
|
|
||||||
for edible in this.edibles.iter_mut() {
|
|
||||||
let _ = edible.render(texture_canvas);
|
|
||||||
}
|
|
||||||
let _ = this.pacman.borrow_mut().render(texture_canvas);
|
|
||||||
let _ = this.blinky.render(texture_canvas);
|
|
||||||
let _ = this.pinky.render(texture_canvas);
|
|
||||||
let _ = this.inky.render(texture_canvas);
|
|
||||||
let _ = this.clyde.render(texture_canvas);
|
|
||||||
match this.debug_mode {
|
|
||||||
DebugMode::Grid => {
|
|
||||||
DebugRenderer::draw_debug_grid(
|
|
||||||
texture_canvas,
|
|
||||||
&this.map.borrow(),
|
|
||||||
this.pacman.borrow().base.base.cell_position,
|
|
||||||
);
|
|
||||||
let next_cell = <Pacman as crate::entity::Moving>::next_cell(&*this.pacman.borrow(), None);
|
|
||||||
DebugRenderer::draw_next_cell(texture_canvas, &this.map.borrow(), next_cell.as_uvec2());
|
|
||||||
}
|
|
||||||
DebugMode::ValidPositions => {
|
|
||||||
DebugRenderer::draw_valid_positions(texture_canvas, &mut this.map.borrow_mut());
|
|
||||||
}
|
|
||||||
DebugMode::Pathfinding => {
|
|
||||||
DebugRenderer::draw_pathfinding(texture_canvas, &this.blinky, &this.map.borrow());
|
|
||||||
}
|
|
||||||
DebugMode::None => {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map_err(|e| anyhow::anyhow!(format!("Failed to render to backbuffer: {e}")))
|
|
||||||
}
|
|
||||||
pub fn present_backbuffer(&mut self, canvas: &mut Canvas<Window>, backbuffer: &Texture) -> Result<()> {
|
|
||||||
canvas.set_draw_color(Color::BLACK);
|
canvas.set_draw_color(Color::BLACK);
|
||||||
canvas.clear();
|
canvas.clear();
|
||||||
|
self.map.render(canvas, &mut self.atlas, &mut self.map_texture);
|
||||||
|
self.pacman.render(canvas, &mut self.atlas, &self.map.graph);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn present_backbuffer<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>, backbuffer: &Texture) -> Result<()> {
|
||||||
canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?;
|
canvas.copy(backbuffer, None, None).map_err(anyhow::Error::msg)?;
|
||||||
self.render_ui_on(canvas);
|
if self.debug_mode {
|
||||||
|
self.map
|
||||||
|
.debug_render_nodes(canvas, &mut self.atlas, &mut self.debug_text_texture);
|
||||||
|
}
|
||||||
|
self.draw_hud(canvas)?;
|
||||||
canvas.present();
|
canvas.present();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_ui_on<C: sdl2::render::RenderTarget>(&mut self, canvas: &mut sdl2::render::Canvas<C>) {
|
fn draw_hud<T: RenderTarget>(&mut self, canvas: &mut Canvas<T>) -> Result<()> {
|
||||||
|
let score_text = self.score.to_string();
|
||||||
let lives = 3;
|
let lives = 3;
|
||||||
let score_text = format!("{:02}", self.score);
|
let score_text = format!("{:02}", self.score);
|
||||||
let x_offset = 4;
|
let x_offset = 4;
|
||||||
@@ -396,11 +131,13 @@ impl Game {
|
|||||||
self.text_texture.set_scale(1.0);
|
self.text_texture.set_scale(1.0);
|
||||||
let _ = self.text_texture.render(
|
let _ = self.text_texture.render(
|
||||||
canvas,
|
canvas,
|
||||||
|
&mut self.atlas,
|
||||||
&format!("{lives}UP HIGH SCORE "),
|
&format!("{lives}UP HIGH SCORE "),
|
||||||
UVec2::new(8 * lives_offset as u32 + x_offset, y_offset),
|
UVec2::new(8 * lives_offset as u32 + x_offset, y_offset),
|
||||||
);
|
);
|
||||||
let _ = self.text_texture.render(
|
let _ = self.text_texture.render(
|
||||||
canvas,
|
canvas,
|
||||||
|
&mut self.atlas,
|
||||||
&score_text,
|
&score_text,
|
||||||
UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset),
|
UVec2::new(8 * score_offset as u32 + x_offset, 8 + y_offset),
|
||||||
);
|
);
|
||||||
@@ -414,5 +151,7 @@ impl Game {
|
|||||||
// IVec2::new(10, 10),
|
// IVec2::new(10, 10),
|
||||||
// Color::RGB(255, 255, 0), // Yellow color for FPS display
|
// Color::RGB(255, 255, 0), // Yellow color for FPS display
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
107
src/helper.rs
107
src/helper.rs
@@ -1,107 +0,0 @@
|
|||||||
//! This module contains helper functions that are used throughout the game.
|
|
||||||
|
|
||||||
use glam::UVec2;
|
|
||||||
|
|
||||||
/// Checks if two grid positions are adjacent to each other
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `a` - First position as (x, y) coordinates
|
|
||||||
/// * `b` - Second position as (x, y) coordinates
|
|
||||||
/// * `diagonal` - Whether to consider diagonal adjacency (true) or only orthogonal (false)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// * `true` if positions are adjacent according to the diagonal parameter
|
|
||||||
/// * `false` otherwise
|
|
||||||
pub fn is_adjacent(a: UVec2, b: UVec2, diagonal: bool) -> bool {
|
|
||||||
let dx = a.x.abs_diff(b.x);
|
|
||||||
let dy = a.y.abs_diff(b.y);
|
|
||||||
if diagonal {
|
|
||||||
dx <= 1 && dy <= 1 && (dx != 0 || dy != 0)
|
|
||||||
} else {
|
|
||||||
(dx == 1 && dy == 0) || (dx == 0 && dy == 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_orthogonal_adjacency() {
|
|
||||||
// Test orthogonal adjacency (diagonal = false)
|
|
||||||
|
|
||||||
// Same position should not be adjacent
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 0), false));
|
|
||||||
|
|
||||||
// Adjacent positions should be true
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), false)); // Right
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), false)); // Down
|
|
||||||
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 1), false)); // Left
|
|
||||||
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(1, 0), false)); // Up
|
|
||||||
|
|
||||||
// Diagonal positions should be false
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), false));
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 1), UVec2::new(1, 0), false));
|
|
||||||
|
|
||||||
// Positions more than 1 step away should be false
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 0), false));
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 2), false));
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 2), false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_diagonal_adjacency() {
|
|
||||||
// Test diagonal adjacency (diagonal = true)
|
|
||||||
|
|
||||||
// Same position should not be adjacent
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 0), true));
|
|
||||||
|
|
||||||
// Orthogonal adjacent positions should be true
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), true)); // Right
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), true)); // Down
|
|
||||||
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 1), true)); // Left
|
|
||||||
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(1, 0), true)); // Up
|
|
||||||
|
|
||||||
// Diagonal adjacent positions should be true
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), true)); // Down-right
|
|
||||||
assert!(is_adjacent(UVec2::new(1, 0), UVec2::new(0, 1), true)); // Down-left
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 1), UVec2::new(1, 0), true)); // Up-right
|
|
||||||
assert!(is_adjacent(UVec2::new(1, 1), UVec2::new(0, 0), true)); // Up-left
|
|
||||||
|
|
||||||
// Positions more than 1 step away should be false
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 0), true));
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(0, 2), true));
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(2, 2), true));
|
|
||||||
assert!(!is_adjacent(UVec2::new(0, 0), UVec2::new(1, 2), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_edge_cases() {
|
|
||||||
// Test with larger coordinates
|
|
||||||
assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(101, 100), false));
|
|
||||||
assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(100, 101), false));
|
|
||||||
assert!(!is_adjacent(UVec2::new(100, 100), UVec2::new(102, 100), false));
|
|
||||||
|
|
||||||
assert!(is_adjacent(UVec2::new(100, 100), UVec2::new(101, 101), true));
|
|
||||||
assert!(!is_adjacent(UVec2::new(100, 100), UVec2::new(102, 102), true));
|
|
||||||
|
|
||||||
// Test with zero coordinates
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 0), false));
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(0, 1), false));
|
|
||||||
assert!(is_adjacent(UVec2::new(0, 0), UVec2::new(1, 1), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_commutative_property() {
|
|
||||||
// The function should work the same regardless of parameter order
|
|
||||||
assert_eq!(
|
|
||||||
is_adjacent(UVec2::new(1, 2), UVec2::new(2, 2), false),
|
|
||||||
is_adjacent(UVec2::new(2, 2), UVec2::new(1, 2), false)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
is_adjacent(UVec2::new(1, 2), UVec2::new(2, 3), true),
|
|
||||||
is_adjacent(UVec2::new(2, 3), UVec2::new(1, 2), true)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
196
src/main.rs
196
src/main.rs
@@ -1,11 +1,9 @@
|
|||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
use crate::constants::{CANVAS_SIZE, SCALE};
|
use std::time::Duration;
|
||||||
use crate::game::Game;
|
|
||||||
use sdl2::event::{Event, WindowEvent};
|
use crate::{app::App, constants::LOOP_TIME};
|
||||||
use sdl2::keyboard::Keycode;
|
use tracing::info;
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use tracing::event;
|
|
||||||
use tracing_error::ErrorLayer;
|
use tracing_error::ErrorLayer;
|
||||||
use tracing_subscriber::layer::SubscriberExt;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
|
||||||
@@ -52,28 +50,17 @@ unsafe fn attach_console() {
|
|||||||
// Do NOT call AllocConsole here - we don't want a console when launched from Explorer
|
// Do NOT call AllocConsole here - we don't want a console when launched from Explorer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod app;
|
||||||
mod asset;
|
mod asset;
|
||||||
mod audio;
|
mod audio;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod debug;
|
|
||||||
#[cfg(target_os = "emscripten")]
|
#[cfg(target_os = "emscripten")]
|
||||||
mod emscripten;
|
mod emscripten;
|
||||||
mod entity;
|
mod entity;
|
||||||
mod game;
|
mod game;
|
||||||
mod helper;
|
|
||||||
mod map;
|
mod map;
|
||||||
mod texture;
|
mod texture;
|
||||||
|
|
||||||
#[cfg(not(target_os = "emscripten"))]
|
|
||||||
fn sleep(value: Duration) {
|
|
||||||
spin_sleep::sleep(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "emscripten")]
|
|
||||||
fn sleep(value: Duration) {
|
|
||||||
emscripten::emscripten::sleep(value.as_millis() as u32);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main entry point of the application.
|
/// The main entry point of the application.
|
||||||
///
|
///
|
||||||
/// This function initializes SDL, the window, the game state, and then enters
|
/// This function initializes SDL, the window, the game state, and then enters
|
||||||
@@ -85,14 +72,6 @@ pub fn main() {
|
|||||||
attach_console();
|
attach_console();
|
||||||
}
|
}
|
||||||
|
|
||||||
let sdl_context = sdl2::init().unwrap();
|
|
||||||
let video_subsystem = sdl_context.video().unwrap();
|
|
||||||
let audio_subsystem = sdl_context.audio().unwrap();
|
|
||||||
let ttf_context = sdl2::ttf::init().unwrap();
|
|
||||||
|
|
||||||
// Set nearest-neighbor scaling for pixelated rendering
|
|
||||||
sdl2::hint::set("SDL_RENDER_SCALE_QUALITY", "nearest");
|
|
||||||
|
|
||||||
// Setup tracing
|
// Setup tracing
|
||||||
let subscriber = tracing_subscriber::fmt()
|
let subscriber = tracing_subscriber::fmt()
|
||||||
.with_ansi(cfg!(not(target_os = "emscripten")))
|
.with_ansi(cfg!(not(target_os = "emscripten")))
|
||||||
@@ -102,169 +81,16 @@ pub fn main() {
|
|||||||
|
|
||||||
tracing::subscriber::set_global_default(subscriber).expect("Could not set global default");
|
tracing::subscriber::set_global_default(subscriber).expect("Could not set global default");
|
||||||
|
|
||||||
let window = video_subsystem
|
let mut app = App::new().expect("Could not create app");
|
||||||
.window(
|
|
||||||
"Pac-Man",
|
|
||||||
(CANVAS_SIZE.x as f32 * SCALE).round() as u32,
|
|
||||||
(CANVAS_SIZE.y as f32 * SCALE).round() as u32,
|
|
||||||
)
|
|
||||||
.resizable()
|
|
||||||
.position_centered()
|
|
||||||
.build()
|
|
||||||
.expect("Could not initialize window");
|
|
||||||
|
|
||||||
let mut canvas = window.into_canvas().build().expect("Could not build canvas");
|
info!("Starting game loop ({:?})", LOOP_TIME);
|
||||||
|
|
||||||
canvas
|
#[cfg(target_os = "emscripten")]
|
||||||
.set_logical_size(CANVAS_SIZE.x, CANVAS_SIZE.y)
|
emscripten::set_main_loop_callback(app.run);
|
||||||
.expect("Could not set logical size");
|
|
||||||
|
|
||||||
let texture_creator = canvas.texture_creator();
|
|
||||||
let texture_creator_static: &'static sdl2::render::TextureCreator<sdl2::video::WindowContext> =
|
|
||||||
Box::leak(Box::new(texture_creator));
|
|
||||||
let mut game = Game::new(texture_creator_static, &ttf_context, &audio_subsystem);
|
|
||||||
game.audio.set_mute(cfg!(debug_assertions));
|
|
||||||
|
|
||||||
// Create a backbuffer texture for drawing
|
|
||||||
let mut backbuffer = texture_creator_static
|
|
||||||
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
|
|
||||||
.expect("Could not create backbuffer texture");
|
|
||||||
|
|
||||||
let mut event_pump = sdl_context.event_pump().expect("Could not get SDL EventPump");
|
|
||||||
|
|
||||||
// Initial draw and tick
|
|
||||||
if let Err(e) = game.draw(&mut canvas, &mut backbuffer) {
|
|
||||||
eprintln!("Initial draw failed: {e}");
|
|
||||||
}
|
|
||||||
if let Err(e) = game.present_backbuffer(&mut canvas, &backbuffer) {
|
|
||||||
eprintln!("Initial present failed: {e}");
|
|
||||||
}
|
|
||||||
game.tick();
|
|
||||||
|
|
||||||
// The target time for each frame of the game loop (60 FPS).
|
|
||||||
let loop_time = Duration::from_secs(1) / 60;
|
|
||||||
|
|
||||||
let mut paused = false;
|
|
||||||
|
|
||||||
// FPS tracking
|
|
||||||
let mut frame_times_1s = Vec::new();
|
|
||||||
let mut frame_times_10s = Vec::new();
|
|
||||||
let mut last_frame_time = Instant::now();
|
|
||||||
|
|
||||||
event!(tracing::Level::INFO, "Starting game loop ({:?})", loop_time);
|
|
||||||
let mut main_loop = move || {
|
|
||||||
let start = Instant::now();
|
|
||||||
let current_frame_time = Instant::now();
|
|
||||||
let frame_duration = current_frame_time.duration_since(last_frame_time);
|
|
||||||
last_frame_time = current_frame_time;
|
|
||||||
|
|
||||||
// Update FPS tracking
|
|
||||||
frame_times_1s.push(frame_duration);
|
|
||||||
frame_times_10s.push(frame_duration);
|
|
||||||
|
|
||||||
// Keep only last 1 second of data (assuming 60 FPS = ~60 frames)
|
|
||||||
while frame_times_1s.len() > 60 {
|
|
||||||
frame_times_1s.remove(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep only last 10 seconds of data
|
|
||||||
while frame_times_10s.len() > 600 {
|
|
||||||
frame_times_10s.remove(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate FPS averages
|
|
||||||
let fps_1s = if !frame_times_1s.is_empty() {
|
|
||||||
let total_time: Duration = frame_times_1s.iter().sum();
|
|
||||||
if total_time > Duration::ZERO {
|
|
||||||
frame_times_1s.len() as f64 / total_time.as_secs_f64()
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
let fps_10s = if !frame_times_10s.is_empty() {
|
|
||||||
let total_time: Duration = frame_times_10s.iter().sum();
|
|
||||||
if total_time > Duration::ZERO {
|
|
||||||
frame_times_10s.len() as f64 / total_time.as_secs_f64()
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Fix key repeat delay issues by using a queue for keyboard events.
|
|
||||||
// This would allow for instant key repeat without being affected by the
|
|
||||||
// main loop's tick rate.
|
|
||||||
for event in event_pump.poll_iter() {
|
|
||||||
match event {
|
|
||||||
Event::Window { win_event, .. } => match win_event {
|
|
||||||
WindowEvent::Hidden => {
|
|
||||||
event!(tracing::Level::DEBUG, "Window hidden");
|
|
||||||
}
|
|
||||||
WindowEvent::Shown => {
|
|
||||||
event!(tracing::Level::DEBUG, "Window shown");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
// Handle quitting keys or window close
|
|
||||||
Event::Quit { .. }
|
|
||||||
| Event::KeyDown {
|
|
||||||
keycode: Some(Keycode::Escape) | Some(Keycode::Q),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
event!(tracing::Level::INFO, "Exit requested. Exiting...");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Event::KeyDown {
|
|
||||||
keycode: Some(Keycode::P),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
paused = !paused;
|
|
||||||
event!(tracing::Level::INFO, "{}", if paused { "Paused" } else { "Unpaused" });
|
|
||||||
}
|
|
||||||
Event::KeyDown { keycode, .. } => {
|
|
||||||
game.keyboard_event(keycode.unwrap());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Implement a proper pausing mechanism that does not interfere with
|
|
||||||
// statistic gathering and other background tasks.
|
|
||||||
if !paused {
|
|
||||||
game.tick();
|
|
||||||
if let Err(e) = game.draw(&mut canvas, &mut backbuffer) {
|
|
||||||
eprintln!("Failed to draw game: {e}");
|
|
||||||
}
|
|
||||||
if let Err(e) = game.present_backbuffer(&mut canvas, &backbuffer) {
|
|
||||||
eprintln!("Failed to present backbuffer: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update game with FPS data
|
|
||||||
game.update_fps(fps_1s, fps_10s);
|
|
||||||
|
|
||||||
if start.elapsed() < loop_time {
|
|
||||||
let time = loop_time.saturating_sub(start.elapsed());
|
|
||||||
if time != Duration::ZERO {
|
|
||||||
sleep(time);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
event!(
|
|
||||||
tracing::Level::WARN,
|
|
||||||
"Game loop behind schedule by: {:?}",
|
|
||||||
start.elapsed() - loop_time
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "emscripten"))]
|
||||||
loop {
|
loop {
|
||||||
if !main_loop() {
|
if !app.run() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
251
src/map.rs
251
src/map.rs
@@ -1,16 +1,18 @@
|
|||||||
//! This module defines the game map and provides functions for interacting with it.
|
//! This module defines the game map and provides functions for interacting with it.
|
||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::seq::IteratorRandom;
|
|
||||||
use rand::SeedableRng;
|
|
||||||
|
|
||||||
use crate::constants::{MapTile, BOARD_CELL_OFFSET, BOARD_CELL_SIZE, BOARD_PIXEL_OFFSET, BOARD_PIXEL_SIZE, CELL_SIZE};
|
use crate::constants::{MapTile, BOARD_CELL_SIZE, BOARD_PIXEL_OFFSET, BOARD_PIXEL_SIZE, CELL_SIZE};
|
||||||
use crate::texture::sprite::AtlasTile;
|
use crate::entity::direction::DIRECTIONS;
|
||||||
use glam::{IVec2, UVec2};
|
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
||||||
use once_cell::sync::OnceCell;
|
use glam::{IVec2, UVec2, Vec2};
|
||||||
use sdl2::rect::Rect;
|
use sdl2::pixels::Color;
|
||||||
use sdl2::render::Canvas;
|
use sdl2::rect::{Point, Rect};
|
||||||
use sdl2::video::Window;
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
use std::collections::{HashSet, VecDeque};
|
use smallvec::SmallVec;
|
||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::entity::graph::{Graph, Node};
|
||||||
|
use crate::texture::text::TextTexture;
|
||||||
|
|
||||||
/// The game map.
|
/// The game map.
|
||||||
///
|
///
|
||||||
@@ -19,8 +21,8 @@ use std::collections::{HashSet, VecDeque};
|
|||||||
pub struct Map {
|
pub struct Map {
|
||||||
/// The current state of the map.
|
/// The current state of the map.
|
||||||
current: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
|
current: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
|
||||||
/// The default state of the map.
|
/// The node map for entity movement.
|
||||||
default: [[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize],
|
pub graph: Graph,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Map {
|
impl Map {
|
||||||
@@ -31,7 +33,7 @@ impl Map {
|
|||||||
/// * `raw_board` - A 2D array of characters representing the board layout.
|
/// * `raw_board` - A 2D array of characters representing the board layout.
|
||||||
pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> Map {
|
pub fn new(raw_board: [&str; BOARD_CELL_SIZE.y as usize]) -> Map {
|
||||||
let mut map = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize];
|
let mut map = [[MapTile::Empty; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize];
|
||||||
|
let mut house_door = SmallVec::<[IVec2; 2]>::new();
|
||||||
for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) {
|
for (y, line) in raw_board.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) {
|
||||||
for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) {
|
for (x, character) in line.chars().enumerate().take(BOARD_CELL_SIZE.x as usize) {
|
||||||
let tile = match character {
|
let tile = match character {
|
||||||
@@ -40,127 +42,114 @@ impl Map {
|
|||||||
'o' => MapTile::PowerPellet,
|
'o' => MapTile::PowerPellet,
|
||||||
' ' => MapTile::Empty,
|
' ' => MapTile::Empty,
|
||||||
'T' => MapTile::Tunnel,
|
'T' => MapTile::Tunnel,
|
||||||
c @ '0' | c @ '1' | c @ '2' | c @ '3' | c @ '4' => MapTile::StartingPosition(c.to_digit(10).unwrap() as u8),
|
c @ '0'..='4' => MapTile::StartingPosition(c.to_digit(10).unwrap() as u8),
|
||||||
'=' => MapTile::Empty,
|
'=' => {
|
||||||
|
house_door.push(IVec2::new(x as i32, y as i32));
|
||||||
|
MapTile::Wall
|
||||||
|
}
|
||||||
_ => panic!("Unknown character in board: {character}"),
|
_ => panic!("Unknown character in board: {character}"),
|
||||||
};
|
};
|
||||||
map[x][y] = tile;
|
map[x][y] = tile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map {
|
if house_door.len() != 2 {
|
||||||
current: map,
|
panic!("House door must have exactly 2 positions");
|
||||||
default: map,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resets the map to its original state.
|
let mut graph = Self::create_graph(&map);
|
||||||
pub fn reset(&mut self) {
|
|
||||||
// Restore the map to its original state
|
let house_door_node_id = {
|
||||||
for (x, col) in self.current.iter_mut().enumerate().take(BOARD_CELL_SIZE.x as usize) {
|
let offset = Vec2::splat(CELL_SIZE as f32 / 2.0);
|
||||||
for (y, cell) in col.iter_mut().enumerate().take(BOARD_CELL_SIZE.y as usize) {
|
|
||||||
*cell = self.default[x][y];
|
let position_a = house_door[0].as_vec2() * Vec2::splat(CELL_SIZE as f32) + offset;
|
||||||
}
|
let position_b = house_door[1].as_vec2() * Vec2::splat(CELL_SIZE as f32) + offset;
|
||||||
}
|
info!("Position A: {position_a}, Position B: {position_b}");
|
||||||
|
let position = position_a.lerp(position_b, 0.5);
|
||||||
|
|
||||||
|
graph.add_node(Node { position })
|
||||||
|
};
|
||||||
|
info!("House door node id: {house_door_node_id}");
|
||||||
|
|
||||||
|
Map { current: map, graph }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the tile at the given cell coordinates.
|
fn create_graph(map: &[[MapTile; BOARD_CELL_SIZE.y as usize]; BOARD_CELL_SIZE.x as usize]) -> Graph {
|
||||||
///
|
let mut graph = Graph::new();
|
||||||
/// # Arguments
|
let mut grid_to_node = HashMap::new();
|
||||||
///
|
|
||||||
/// * `cell` - The cell coordinates, in grid coordinates.
|
|
||||||
pub fn get_tile(&self, cell: IVec2) -> Option<MapTile> {
|
|
||||||
let x = cell.x as usize;
|
|
||||||
let y = cell.y as usize;
|
|
||||||
|
|
||||||
if x >= BOARD_CELL_SIZE.x as usize || y >= BOARD_CELL_SIZE.y as usize {
|
let cell_offset = Vec2::splat(CELL_SIZE as f32 / 2.0);
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(self.current[x][y])
|
// Find a starting point for the graph generation, preferably Pac-Man's position.
|
||||||
}
|
let start_pos = (0..BOARD_CELL_SIZE.y)
|
||||||
|
.flat_map(|y| (0..BOARD_CELL_SIZE.x).map(move |x| IVec2::new(x as i32, y as i32)))
|
||||||
/// Sets the tile at the given cell coordinates.
|
.find(|&p| matches!(map[p.x as usize][p.y as usize], MapTile::StartingPosition(0)))
|
||||||
///
|
.unwrap_or_else(|| {
|
||||||
/// # Arguments
|
// Fallback to any valid walkable tile if Pac-Man's start is not found
|
||||||
///
|
(0..BOARD_CELL_SIZE.y)
|
||||||
/// * `cell` - The cell coordinates, in grid coordinates.
|
.flat_map(|y| (0..BOARD_CELL_SIZE.x).map(move |x| IVec2::new(x as i32, y as i32)))
|
||||||
/// * `tile` - The tile to set.
|
.find(|&p| {
|
||||||
pub fn set_tile(&mut self, cell: IVec2, tile: MapTile) -> bool {
|
matches!(
|
||||||
let x = cell.x as usize;
|
map[p.x as usize][p.y as usize],
|
||||||
let y = cell.y as usize;
|
MapTile::Pellet
|
||||||
|
| MapTile::PowerPellet
|
||||||
if x >= BOARD_CELL_SIZE.x as usize || y >= BOARD_CELL_SIZE.y as usize {
|
| MapTile::Empty
|
||||||
return false;
|
| MapTile::Tunnel
|
||||||
}
|
| MapTile::StartingPosition(_)
|
||||||
|
|
||||||
self.current[x][y] = tile;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts cell coordinates to pixel coordinates.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `cell` - The cell coordinates, in grid coordinates.
|
|
||||||
pub fn cell_to_pixel(cell: UVec2) -> IVec2 {
|
|
||||||
IVec2::new(
|
|
||||||
(cell.x * CELL_SIZE) as i32,
|
|
||||||
((cell.y + BOARD_CELL_OFFSET.y) * CELL_SIZE) as i32,
|
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
.expect("No valid starting position found on map for graph generation")
|
||||||
|
});
|
||||||
|
|
||||||
/// Returns a reference to a cached vector of all valid playable positions in the maze.
|
|
||||||
/// This is computed once using a flood fill from a random pellet, and then cached.
|
|
||||||
pub fn get_valid_playable_positions(&mut self) -> &Vec<UVec2> {
|
|
||||||
use MapTile::*;
|
|
||||||
static CACHE: OnceCell<Vec<UVec2>> = OnceCell::new();
|
|
||||||
if let Some(cached) = CACHE.get() {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
// Find a random starting pellet
|
|
||||||
let mut pellet_positions = vec![];
|
|
||||||
for (x, col) in self.current.iter().enumerate().take(BOARD_CELL_SIZE.x as usize) {
|
|
||||||
for (y, &cell) in col.iter().enumerate().take(BOARD_CELL_SIZE.y as usize) {
|
|
||||||
match cell {
|
|
||||||
Pellet | PowerPellet => pellet_positions.push(UVec2::new(x as u32, y as u32)),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut rng = SmallRng::from_os_rng();
|
|
||||||
let &start = pellet_positions
|
|
||||||
.iter()
|
|
||||||
.choose(&mut rng)
|
|
||||||
.expect("No pellet found for flood fill");
|
|
||||||
// Flood fill
|
|
||||||
let mut visited = HashSet::new();
|
|
||||||
let mut queue = VecDeque::new();
|
let mut queue = VecDeque::new();
|
||||||
|
queue.push_back(start_pos);
|
||||||
|
|
||||||
queue.push_back(start);
|
let pos = Vec2::new(
|
||||||
while let Some(pos) = queue.pop_front() {
|
(start_pos.x * CELL_SIZE as i32) as f32,
|
||||||
if !visited.insert(pos) {
|
(start_pos.y * CELL_SIZE as i32) as f32,
|
||||||
|
) + cell_offset;
|
||||||
|
let node_id = graph.add_node(Node { position: pos });
|
||||||
|
grid_to_node.insert(start_pos, node_id);
|
||||||
|
|
||||||
|
while let Some(grid_pos) = queue.pop_front() {
|
||||||
|
for &dir in DIRECTIONS.iter() {
|
||||||
|
let neighbor = grid_pos + dir.to_ivec2();
|
||||||
|
|
||||||
|
if neighbor.x < 0
|
||||||
|
|| neighbor.x >= BOARD_CELL_SIZE.x as i32
|
||||||
|
|| neighbor.y < 0
|
||||||
|
|| neighbor.y >= BOARD_CELL_SIZE.y as i32
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.current[pos.x as usize][pos.y as usize] {
|
if grid_to_node.contains_key(&neighbor) {
|
||||||
Empty | Pellet | PowerPellet => {
|
continue;
|
||||||
for offset in [IVec2::new(-1, 0), IVec2::new(1, 0), IVec2::new(0, -1), IVec2::new(0, 1)] {
|
}
|
||||||
let neighbor = (pos.as_ivec2() + offset).as_uvec2();
|
|
||||||
if neighbor.x < BOARD_CELL_SIZE.x && neighbor.y < BOARD_CELL_SIZE.y {
|
if matches!(
|
||||||
let neighbor_tile = self.current[neighbor.x as usize][neighbor.y as usize];
|
map[neighbor.x as usize][neighbor.y as usize],
|
||||||
if matches!(neighbor_tile, Empty | Pellet | PowerPellet) {
|
MapTile::Pellet | MapTile::PowerPellet | MapTile::Empty | MapTile::Tunnel | MapTile::StartingPosition(_)
|
||||||
|
) {
|
||||||
|
let pos =
|
||||||
|
Vec2::new((neighbor.x * CELL_SIZE as i32) as f32, (neighbor.y * CELL_SIZE as i32) as f32) + cell_offset;
|
||||||
|
let node_id = graph.add_node(Node { position: pos });
|
||||||
|
grid_to_node.insert(neighbor, node_id);
|
||||||
queue.push_back(neighbor);
|
queue.push_back(neighbor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
StartingPosition(_) | Wall | Tunnel => {}
|
for (grid_pos, &node_id) in &grid_to_node {
|
||||||
|
for &dir in DIRECTIONS.iter() {
|
||||||
|
let neighbor = grid_pos + dir.to_ivec2();
|
||||||
|
|
||||||
|
if let Some(&neighbor_id) = grid_to_node.get(&neighbor) {
|
||||||
|
graph.add_edge(node_id, neighbor_id, None, dir).expect("Failed to add edge");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut result: Vec<UVec2> = visited.into_iter().collect();
|
}
|
||||||
result.sort_unstable_by_key(|v| (v.x, v.y));
|
graph
|
||||||
CACHE.get_or_init(|| result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the starting position for a given entity ID.
|
/// Finds the starting position for a given entity ID.
|
||||||
@@ -186,13 +175,51 @@ impl Map {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the map to the given canvas using the provided map texture.
|
/// Renders the map to the given canvas using the provided map texture.
|
||||||
pub fn render(&self, canvas: &mut Canvas<Window>, map_texture: &mut AtlasTile) {
|
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, map_texture: &mut AtlasTile) {
|
||||||
let dest = Rect::new(
|
let dest = Rect::new(
|
||||||
BOARD_PIXEL_OFFSET.x as i32,
|
BOARD_PIXEL_OFFSET.x as i32,
|
||||||
BOARD_PIXEL_OFFSET.y as i32,
|
BOARD_PIXEL_OFFSET.y as i32,
|
||||||
BOARD_PIXEL_SIZE.x,
|
BOARD_PIXEL_SIZE.x,
|
||||||
BOARD_PIXEL_SIZE.y,
|
BOARD_PIXEL_SIZE.y,
|
||||||
);
|
);
|
||||||
let _ = map_texture.render(canvas, dest);
|
let _ = map_texture.render(canvas, atlas, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_render_nodes<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, text: &mut TextTexture) {
|
||||||
|
for i in 0..self.graph.node_count() {
|
||||||
|
let node = self.graph.get_node(i).unwrap();
|
||||||
|
let pos = node.position + BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
|
||||||
|
// Draw connections
|
||||||
|
// TODO: fix this
|
||||||
|
// canvas.set_draw_color(Color::BLUE);
|
||||||
|
|
||||||
|
// for neighbor in node.neighbors() {
|
||||||
|
// let end_pos = neighbor.get(&self.node_map).position + BOARD_PIXEL_OFFSET.as_vec2();
|
||||||
|
// canvas
|
||||||
|
// .draw_line((pos.x as i32, pos.y as i32), (end_pos.x as i32, end_pos.y as i32))
|
||||||
|
// .unwrap();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Draw node
|
||||||
|
// let color = if pacman.position.from_node_idx() == i.into() {
|
||||||
|
// Color::GREEN
|
||||||
|
// } else if let Some(to_idx) = pacman.position.to_node_idx() {
|
||||||
|
// if to_idx == i.into() {
|
||||||
|
// Color::CYAN
|
||||||
|
// } else {
|
||||||
|
// Color::RED
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// Color::RED
|
||||||
|
// };
|
||||||
|
canvas.set_draw_color(Color::GREEN);
|
||||||
|
canvas
|
||||||
|
.fill_rect(Rect::new(0, 0, 3, 3).centered_on(Point::new(pos.x as i32, pos.y as i32)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Draw node index
|
||||||
|
// text.render(canvas, atlas, &i.to_string(), pos.as_uvec2()).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,41 @@
|
|||||||
//! This module provides a simple animation and atlas system for textures.
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use sdl2::render::WindowCanvas;
|
use sdl2::rect::Rect;
|
||||||
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
|
||||||
use crate::texture::sprite::AtlasTile;
|
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
||||||
|
|
||||||
/// An animated texture using a texture atlas.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AnimatedTexture {
|
pub struct AnimatedTexture {
|
||||||
pub frames: Vec<AtlasTile>,
|
tiles: Vec<AtlasTile>,
|
||||||
pub ticks_per_frame: u32,
|
frame_duration: f32,
|
||||||
pub ticker: u32,
|
current_frame: usize,
|
||||||
pub paused: bool,
|
time_bank: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnimatedTexture {
|
impl AnimatedTexture {
|
||||||
pub fn new(frames: Vec<AtlasTile>, ticks_per_frame: u32) -> Self {
|
pub fn new(tiles: Vec<AtlasTile>, frame_duration: f32) -> Self {
|
||||||
AnimatedTexture {
|
Self {
|
||||||
frames,
|
tiles,
|
||||||
ticks_per_frame,
|
frame_duration,
|
||||||
ticker: 0,
|
current_frame: 0,
|
||||||
paused: false,
|
time_bank: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advances the animation by one tick, unless paused.
|
pub fn tick(&mut self, dt: f32) {
|
||||||
pub fn tick(&mut self) {
|
self.time_bank += dt;
|
||||||
if self.paused || self.ticks_per_frame == 0 {
|
while self.time_bank >= self.frame_duration {
|
||||||
return;
|
self.time_bank -= self.frame_duration;
|
||||||
|
self.current_frame = (self.current_frame + 1) % self.tiles.len();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.ticker += 1;
|
pub fn current_tile(&self) -> &AtlasTile {
|
||||||
|
&self.tiles[self.current_frame]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_tile(&mut self) -> &mut AtlasTile {
|
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, dest: Rect) -> Result<()> {
|
||||||
if self.ticks_per_frame == 0 {
|
let mut tile = self.current_tile().clone();
|
||||||
return &mut self.frames[0];
|
tile.render(canvas, atlas, dest)
|
||||||
}
|
|
||||||
let frame_index = (self.ticker / self.ticks_per_frame) as usize % self.frames.len();
|
|
||||||
&mut self.frames[frame_index]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect) -> Result<()> {
|
|
||||||
let tile = self.current_tile();
|
|
||||||
tile.render(canvas, dest)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,36 @@
|
|||||||
//! A texture that blinks on/off for a specified number of ticks.
|
use crate::texture::sprite::AtlasTile;
|
||||||
use anyhow::Result;
|
|
||||||
use sdl2::render::WindowCanvas;
|
|
||||||
|
|
||||||
use crate::texture::animated::AnimatedTexture;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BlinkingTexture {
|
pub struct BlinkingTexture {
|
||||||
pub animation: AnimatedTexture,
|
tile: AtlasTile,
|
||||||
pub on_ticks: u32,
|
blink_duration: f32,
|
||||||
pub off_ticks: u32,
|
time_bank: f32,
|
||||||
pub ticker: u32,
|
is_on: bool,
|
||||||
pub visible: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlinkingTexture {
|
impl BlinkingTexture {
|
||||||
pub fn new(animation: AnimatedTexture, on_ticks: u32, off_ticks: u32) -> Self {
|
pub fn new(tile: AtlasTile, blink_duration: f32) -> Self {
|
||||||
BlinkingTexture {
|
Self {
|
||||||
animation,
|
tile,
|
||||||
on_ticks,
|
blink_duration,
|
||||||
off_ticks,
|
time_bank: 0.0,
|
||||||
ticker: 0,
|
is_on: true,
|
||||||
visible: true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Advances the blinking state by one tick.
|
pub fn tick(&mut self, dt: f32) {
|
||||||
pub fn tick(&mut self) {
|
self.time_bank += dt;
|
||||||
self.animation.tick();
|
if self.time_bank >= self.blink_duration {
|
||||||
self.ticker += 1;
|
self.time_bank -= self.blink_duration;
|
||||||
if self.visible && self.ticker >= self.on_ticks {
|
self.is_on = !self.is_on;
|
||||||
self.visible = false;
|
|
||||||
self.ticker = 0;
|
|
||||||
} else if !self.visible && self.ticker >= self.off_ticks {
|
|
||||||
self.visible = true;
|
|
||||||
self.ticker = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the blinking texture.
|
pub fn is_on(&self) -> bool {
|
||||||
pub fn render(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect) -> Result<()> {
|
self.is_on
|
||||||
if self.visible {
|
}
|
||||||
self.animation.render(canvas, dest)
|
|
||||||
} else {
|
pub fn tile(&self) -> &AtlasTile {
|
||||||
Ok(())
|
&self.tile
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,57 @@
|
|||||||
//! A texture that changes based on the direction of an entity.
|
|
||||||
use crate::entity::direction::Direction;
|
|
||||||
use crate::texture::sprite::AtlasTile;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use sdl2::render::WindowCanvas;
|
use sdl2::rect::Rect;
|
||||||
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::entity::direction::Direction;
|
||||||
|
use crate::texture::animated::AnimatedTexture;
|
||||||
|
use crate::texture::sprite::SpriteAtlas;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct DirectionalAnimatedTexture {
|
pub struct DirectionalAnimatedTexture {
|
||||||
pub up: Vec<AtlasTile>,
|
textures: HashMap<Direction, AnimatedTexture>,
|
||||||
pub down: Vec<AtlasTile>,
|
stopped_textures: HashMap<Direction, AnimatedTexture>,
|
||||||
pub left: Vec<AtlasTile>,
|
|
||||||
pub right: Vec<AtlasTile>,
|
|
||||||
pub ticker: u32,
|
|
||||||
pub ticks_per_frame: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DirectionalAnimatedTexture {
|
impl DirectionalAnimatedTexture {
|
||||||
pub fn new(
|
pub fn new(textures: HashMap<Direction, AnimatedTexture>, stopped_textures: HashMap<Direction, AnimatedTexture>) -> Self {
|
||||||
up: Vec<AtlasTile>,
|
|
||||||
down: Vec<AtlasTile>,
|
|
||||||
left: Vec<AtlasTile>,
|
|
||||||
right: Vec<AtlasTile>,
|
|
||||||
ticks_per_frame: u32,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
up,
|
textures,
|
||||||
down,
|
stopped_textures,
|
||||||
left,
|
|
||||||
right,
|
|
||||||
ticker: 0,
|
|
||||||
ticks_per_frame,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick(&mut self) {
|
pub fn tick(&mut self, dt: f32) {
|
||||||
self.ticker += 1;
|
for texture in self.textures.values_mut() {
|
||||||
}
|
texture.tick(dt);
|
||||||
|
}
|
||||||
pub fn render(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect, direction: Direction) -> Result<()> {
|
}
|
||||||
let frames = match direction {
|
|
||||||
Direction::Up => &mut self.up,
|
pub fn render<T: RenderTarget>(
|
||||||
Direction::Down => &mut self.down,
|
&self,
|
||||||
Direction::Left => &mut self.left,
|
canvas: &mut Canvas<T>,
|
||||||
Direction::Right => &mut self.right,
|
atlas: &mut SpriteAtlas,
|
||||||
};
|
dest: Rect,
|
||||||
|
direction: Direction,
|
||||||
let frame_index = (self.ticker / self.ticks_per_frame) as usize % frames.len();
|
) -> Result<()> {
|
||||||
let tile = &mut frames[frame_index];
|
if let Some(texture) = self.textures.get(&direction) {
|
||||||
|
texture.render(canvas, atlas, dest)
|
||||||
tile.render(canvas, dest)
|
} else {
|
||||||
}
|
Ok(())
|
||||||
|
}
|
||||||
pub fn render_stopped(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect, direction: Direction) -> Result<()> {
|
}
|
||||||
let frames = match direction {
|
|
||||||
Direction::Up => &mut self.up,
|
pub fn render_stopped<T: RenderTarget>(
|
||||||
Direction::Down => &mut self.down,
|
&self,
|
||||||
Direction::Left => &mut self.left,
|
canvas: &mut Canvas<T>,
|
||||||
Direction::Right => &mut self.right,
|
atlas: &mut SpriteAtlas,
|
||||||
};
|
dest: Rect,
|
||||||
|
direction: Direction,
|
||||||
// Show the last frame (full sprite) when stopped
|
) -> Result<()> {
|
||||||
let tile = &mut frames[1];
|
if let Some(texture) = self.stopped_textures.get(&direction) {
|
||||||
|
texture.render(canvas, atlas, dest)
|
||||||
tile.render(canvas, dest)
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
|
||||||
|
|
||||||
pub mod animated;
|
pub mod animated;
|
||||||
pub mod blinking;
|
pub mod blinking;
|
||||||
pub mod directional;
|
pub mod directional;
|
||||||
pub mod sprite;
|
pub mod sprite;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|
||||||
pub fn get_atlas_tile(atlas: &Rc<RefCell<SpriteAtlas>>, name: &str) -> AtlasTile {
|
|
||||||
SpriteAtlas::get_tile(atlas, name).unwrap_or_else(|| panic!("Could not find tile {name}"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ use sdl2::pixels::Color;
|
|||||||
use sdl2::rect::Rect;
|
use sdl2::rect::Rect;
|
||||||
use sdl2::render::{Canvas, RenderTarget, Texture};
|
use sdl2::render::{Canvas, RenderTarget, Texture};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct AtlasMapper {
|
pub struct AtlasMapper {
|
||||||
@@ -21,26 +19,28 @@ pub struct MapperFrame {
|
|||||||
pub height: u16,
|
pub height: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
pub struct AtlasTile {
|
pub struct AtlasTile {
|
||||||
pub atlas: Rc<RefCell<SpriteAtlas>>,
|
|
||||||
pub pos: U16Vec2,
|
pub pos: U16Vec2,
|
||||||
pub size: U16Vec2,
|
pub size: U16Vec2,
|
||||||
pub color: Option<Color>,
|
pub color: Option<Color>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AtlasTile {
|
impl AtlasTile {
|
||||||
pub fn render<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, dest: Rect) -> Result<()> {
|
pub fn render<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, atlas: &mut SpriteAtlas, dest: Rect) -> Result<()> {
|
||||||
let color = self
|
let color = self.color.unwrap_or(atlas.default_color.unwrap_or(Color::WHITE));
|
||||||
.color
|
self.render_with_color(canvas, atlas, dest, color)
|
||||||
.unwrap_or(self.atlas.borrow().default_color.unwrap_or(Color::WHITE));
|
|
||||||
self.render_with_color(canvas, dest, color)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_with_color<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, dest: Rect, color: Color) -> Result<()> {
|
pub fn render_with_color<C: RenderTarget>(
|
||||||
|
&mut self,
|
||||||
|
canvas: &mut Canvas<C>,
|
||||||
|
atlas: &mut SpriteAtlas,
|
||||||
|
dest: Rect,
|
||||||
|
color: Color,
|
||||||
|
) -> Result<()> {
|
||||||
let src = Rect::new(self.pos.x as i32, self.pos.y as i32, self.size.x as u32, self.size.y as u32);
|
let src = Rect::new(self.pos.x as i32, self.pos.y as i32, self.size.x as u32, self.size.y as u32);
|
||||||
|
|
||||||
let mut atlas = self.atlas.borrow_mut();
|
|
||||||
if atlas.last_modulation != Some(color) {
|
if atlas.last_modulation != Some(color) {
|
||||||
atlas.texture.set_color_mod(color.r, color.g, color.b);
|
atlas.texture.set_color_mod(color.r, color.g, color.b);
|
||||||
atlas.last_modulation = Some(color);
|
atlas.last_modulation = Some(color);
|
||||||
@@ -68,10 +68,8 @@ impl SpriteAtlas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tile(atlas: &Rc<RefCell<SpriteAtlas>>, name: &str) -> Option<AtlasTile> {
|
pub fn get_tile(&self, name: &str) -> Option<AtlasTile> {
|
||||||
let atlas_ref = atlas.borrow();
|
self.tiles.get(name).map(|frame| AtlasTile {
|
||||||
atlas_ref.tiles.get(name).map(|frame| AtlasTile {
|
|
||||||
atlas: Rc::clone(atlas),
|
|
||||||
pos: U16Vec2::new(frame.x, frame.y),
|
pos: U16Vec2::new(frame.x, frame.y),
|
||||||
size: U16Vec2::new(frame.width, frame.height),
|
size: U16Vec2::new(frame.width, frame.height),
|
||||||
color: None,
|
color: None,
|
||||||
@@ -87,6 +85,6 @@ impl SpriteAtlas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn texture_to_static<'a>(texture: Texture<'a>) -> Texture<'static> {
|
pub unsafe fn texture_to_static(texture: Texture) -> Texture<'static> {
|
||||||
std::mem::transmute(texture)
|
std::mem::transmute(texture)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,38 +50,34 @@ use anyhow::Result;
|
|||||||
use glam::UVec2;
|
use glam::UVec2;
|
||||||
|
|
||||||
use sdl2::render::{Canvas, RenderTarget};
|
use sdl2::render::{Canvas, RenderTarget};
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
|
||||||
|
|
||||||
/// A text texture that renders characters from the atlas.
|
/// A text texture that renders characters from the atlas.
|
||||||
pub struct TextTexture {
|
pub struct TextTexture {
|
||||||
atlas: Rc<RefCell<SpriteAtlas>>,
|
|
||||||
char_map: HashMap<char, AtlasTile>,
|
char_map: HashMap<char, AtlasTile>,
|
||||||
scale: f32,
|
scale: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextTexture {
|
impl TextTexture {
|
||||||
/// Creates a new text texture with the given atlas and scale.
|
/// Creates a new text texture with the given atlas and scale.
|
||||||
pub fn new(atlas: Rc<RefCell<SpriteAtlas>>, scale: f32) -> Self {
|
pub fn new(scale: f32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
atlas,
|
|
||||||
char_map: HashMap::new(),
|
char_map: HashMap::new(),
|
||||||
scale,
|
scale,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maps a character to its atlas tile, handling special characters.
|
/// Maps a character to its atlas tile, handling special characters.
|
||||||
fn get_char_tile(&mut self, c: char) -> Option<AtlasTile> {
|
fn get_char_tile(&mut self, atlas: &SpriteAtlas, c: char) -> Option<AtlasTile> {
|
||||||
if let Some(tile) = self.char_map.get(&c) {
|
if let Some(tile) = self.char_map.get(&c) {
|
||||||
return Some(tile.clone());
|
return Some(*tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tile_name = self.char_to_tile_name(c)?;
|
let tile_name = self.char_to_tile_name(c)?;
|
||||||
let tile = SpriteAtlas::get_tile(&self.atlas, &tile_name)?;
|
let tile = atlas.get_tile(&tile_name)?;
|
||||||
self.char_map.insert(c, tile.clone());
|
self.char_map.insert(c, tile);
|
||||||
Some(tile)
|
Some(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,9 +85,7 @@ impl TextTexture {
|
|||||||
fn char_to_tile_name(&self, c: char) -> Option<String> {
|
fn char_to_tile_name(&self, c: char) -> Option<String> {
|
||||||
let name = match c {
|
let name = match c {
|
||||||
// Letters A-Z
|
// Letters A-Z
|
||||||
'A'..='Z' => format!("text/{c}.png"),
|
'A'..='Z' | '0'..='9' => format!("text/{c}.png"),
|
||||||
// Numbers 0-9
|
|
||||||
'0'..='9' => format!("text/{c}.png"),
|
|
||||||
// Special characters
|
// Special characters
|
||||||
'!' => "text/!.png".to_string(),
|
'!' => "text/!.png".to_string(),
|
||||||
'-' => "text/-.png".to_string(),
|
'-' => "text/-.png".to_string(),
|
||||||
@@ -108,15 +102,21 @@ impl TextTexture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Renders a string of text at the given position.
|
/// Renders a string of text at the given position.
|
||||||
pub fn render<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, text: &str, position: UVec2) -> Result<()> {
|
pub fn render<C: RenderTarget>(
|
||||||
|
&mut self,
|
||||||
|
canvas: &mut Canvas<C>,
|
||||||
|
atlas: &mut SpriteAtlas,
|
||||||
|
text: &str,
|
||||||
|
position: UVec2,
|
||||||
|
) -> Result<()> {
|
||||||
let mut x_offset = 0;
|
let mut x_offset = 0;
|
||||||
let char_width = (8.0 * self.scale) as u32;
|
let char_width = (8.0 * self.scale) as u32;
|
||||||
let char_height = (8.0 * self.scale) as u32;
|
let char_height = (8.0 * self.scale) as u32;
|
||||||
|
|
||||||
for c in text.chars() {
|
for c in text.chars() {
|
||||||
if let Some(mut tile) = self.get_char_tile(c) {
|
if let Some(mut tile) = self.get_char_tile(atlas, c) {
|
||||||
let dest = sdl2::rect::Rect::new((position.x + x_offset) as i32, position.y as i32, char_width, char_height);
|
let dest = sdl2::rect::Rect::new((position.x + x_offset) as i32, position.y as i32, char_width, char_height);
|
||||||
tile.render(canvas, dest)?;
|
tile.render(canvas, atlas, dest)?;
|
||||||
}
|
}
|
||||||
// Always advance x_offset for all characters (including spaces)
|
// Always advance x_offset for all characters (including spaces)
|
||||||
x_offset += char_width;
|
x_offset += char_width;
|
||||||
@@ -136,7 +136,7 @@ impl TextTexture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calculates the width of a string in pixels at the current scale.
|
/// Calculates the width of a string in pixels at the current scale.
|
||||||
pub fn text_width(&mut self, text: &str) -> u32 {
|
pub fn text_width(&self, text: &str) -> u32 {
|
||||||
let char_width = (8.0 * self.scale) as u32;
|
let char_width = (8.0 * self.scale) as u32;
|
||||||
let mut width = 0;
|
let mut width = 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user