feat: add bottom row HUD, proper life display sprites

This commit is contained in:
Ryan Walters
2025-09-08 14:29:50 -05:00
parent 7675608391
commit 11af44c469
4 changed files with 53 additions and 10 deletions

2
Cargo.lock generated
View File

@@ -663,7 +663,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "pacman" name = "pacman"
version = "0.78.0" version = "0.78.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bevy_ecs", "bevy_ecs",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "pacman" name = "pacman"
version = "0.78.0" version = "0.78.1"
authors = ["Xevion"] authors = ["Xevion"]
edition = "2021" edition = "2021"
rust-version = "1.86.0" rust-version = "1.86.0"

View File

@@ -25,12 +25,25 @@ pub const SCALE: f32 = 2.6;
/// screen for score display, player lives, and other UI elements. /// screen for score display, player lives, and other UI elements.
pub const BOARD_CELL_OFFSET: UVec2 = UVec2::new(0, 3); pub const BOARD_CELL_OFFSET: UVec2 = UVec2::new(0, 3);
/// Bottom HUD row offset to reserve space below the game board.
///
/// The 2-cell vertical offset (16 pixels) provides space at the bottom of the
/// screen for displaying Pac-Man's lives (left) and fruit symbols (right).
pub const BOARD_BOTTOM_CELL_OFFSET: UVec2 = UVec2::new(0, 2);
/// Pixel-space equivalent of `BOARD_CELL_OFFSET` for rendering calculations. /// Pixel-space equivalent of `BOARD_CELL_OFFSET` for rendering calculations.
/// ///
/// Automatically calculated from the cell offset to maintain consistency /// Automatically calculated from the cell offset to maintain consistency
/// when the cell size changes. Used for positioning sprites and debug overlays. /// when the cell size changes. Used for positioning sprites and debug overlays.
pub const BOARD_PIXEL_OFFSET: UVec2 = UVec2::new(BOARD_CELL_OFFSET.x * CELL_SIZE, BOARD_CELL_OFFSET.y * CELL_SIZE); pub const BOARD_PIXEL_OFFSET: UVec2 = UVec2::new(BOARD_CELL_OFFSET.x * CELL_SIZE, BOARD_CELL_OFFSET.y * CELL_SIZE);
/// Pixel-space equivalent of `BOARD_BOTTOM_CELL_OFFSET` for rendering calculations.
///
/// Automatically calculated from the cell offset to maintain consistency
/// when the cell size changes. Used for positioning bottom HUD elements.
pub const BOARD_BOTTOM_PIXEL_OFFSET: UVec2 =
UVec2::new(BOARD_BOTTOM_CELL_OFFSET.x * CELL_SIZE, BOARD_BOTTOM_CELL_OFFSET.y * CELL_SIZE);
/// Animation timing constants for ghost state management /// Animation timing constants for ghost state management
pub mod animation { pub mod animation {
/// Normal ghost movement animation speed (ticks per frame at 60 ticks/sec) /// Normal ghost movement animation speed (ticks per frame at 60 ticks/sec)
@@ -45,15 +58,15 @@ pub mod animation {
} }
/// The size of the canvas, in pixels. /// The size of the canvas, in pixels.
pub const CANVAS_SIZE: UVec2 = UVec2::new( pub const CANVAS_SIZE: UVec2 = UVec2::new(
(BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE, (BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x + BOARD_BOTTOM_CELL_OFFSET.x) * CELL_SIZE,
(BOARD_CELL_SIZE.y + BOARD_CELL_OFFSET.y) * CELL_SIZE, (BOARD_CELL_SIZE.y + BOARD_CELL_OFFSET.y + BOARD_BOTTOM_CELL_OFFSET.y) * CELL_SIZE,
); );
pub const LARGE_SCALE: f32 = 2.6; pub const LARGE_SCALE: f32 = 2.6;
pub const LARGE_CANVAS_SIZE: UVec2 = UVec2::new( pub const LARGE_CANVAS_SIZE: UVec2 = UVec2::new(
(((BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE) as f32 * LARGE_SCALE) as u32, (((BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x + BOARD_BOTTOM_CELL_OFFSET.x) * CELL_SIZE) as f32 * LARGE_SCALE) as u32,
(((BOARD_CELL_SIZE.y + BOARD_CELL_OFFSET.y) * CELL_SIZE) as f32 * LARGE_SCALE) as u32, (((BOARD_CELL_SIZE.y + BOARD_CELL_OFFSET.y + BOARD_BOTTOM_CELL_OFFSET.y) * CELL_SIZE) as f32 * LARGE_SCALE) as u32,
); );
/// Collider size constants for different entity types /// Collider size constants for different entity types

View File

@@ -1,4 +1,5 @@
use crate::map::builder::Map; use crate::map::builder::Map;
use crate::map::direction::Direction;
use crate::systems::input::TouchState; use crate::systems::input::TouchState;
use crate::systems::{ use crate::systems::{
debug_render_system, BatchedLinesResource, Collider, CursorPosition, DebugState, DebugTextureResource, DeltaTime, debug_render_system, BatchedLinesResource, Collider, CursorPosition, DebugState, DebugTextureResource, DeltaTime,
@@ -6,9 +7,10 @@ use crate::systems::{
StartupSequence, SystemId, SystemTimings, TtfAtlasResource, Velocity, StartupSequence, SystemId, SystemTimings, TtfAtlasResource, Velocity,
}; };
use crate::texture::sprite::SpriteAtlas; use crate::texture::sprite::SpriteAtlas;
use crate::texture::sprites::{GameSprite, PacmanSprite};
use crate::texture::text::TextTexture; use crate::texture::text::TextTexture;
use crate::{ use crate::{
constants::CANVAS_SIZE, constants::{BOARD_BOTTOM_PIXEL_OFFSET, CANVAS_SIZE, CELL_SIZE},
error::{GameError, TextureError}, error::{GameError, TextureError},
}; };
use bevy_ecs::component::Component; use bevy_ecs::component::Component;
@@ -218,14 +220,42 @@ pub fn hud_render_system(
let mut text_renderer = TextTexture::new(1.0); let mut text_renderer = TextTexture::new(1.0);
// Render lives and high score text in white // Render lives and high score text in white
let lives = player_lives.0; let lives_text = "1UP HIGH SCORE ";
let lives_text = format!("{lives}UP HIGH SCORE ");
let lives_position = glam::UVec2::new(4 + 8 * 3, 2); // x_offset + lives_offset * 8, y_offset let lives_position = glam::UVec2::new(4 + 8 * 3, 2); // x_offset + lives_offset * 8, y_offset
if let Err(e) = text_renderer.render(canvas, &mut atlas, &lives_text, lives_position) { if let Err(e) = text_renderer.render(canvas, &mut atlas, lives_text, lives_position) {
errors.write(TextureError::RenderFailed(format!("Failed to render lives text: {}", e)).into()); errors.write(TextureError::RenderFailed(format!("Failed to render lives text: {}", e)).into());
} }
// Render Pac-Man life sprites in bottom left
let lives = player_lives.0;
let life_sprite_path = &GameSprite::Pacman(PacmanSprite::Moving(Direction::Left, 1)).to_path();
// Get the sprite from the atlas for life display
match atlas.get_tile(life_sprite_path) {
Ok(life_sprite) => {
let start_x = CELL_SIZE * 2; // 2 cells from left
let start_y = CANVAS_SIZE.y - BOARD_BOTTOM_PIXEL_OFFSET.y + (CELL_SIZE / 2) + 1; // In bottom area
let sprite_spacing = CELL_SIZE + CELL_SIZE / 2; // 1.5 cells between sprites
// Render one sprite for each remaining life (lives - 1, since current life isn't shown)
let sprites_to_show = if lives > 0 { lives - 1 } else { 0 };
for i in 0..sprites_to_show {
let x = start_x + ((i as f32) * (sprite_spacing as f32 * 1.5)).round() as u32;
let y = start_y - CELL_SIZE / 2;
let dest = sdl2::rect::Rect::new(x as i32, y as i32, life_sprite.size.x as u32, life_sprite.size.y as u32);
if let Err(e) = life_sprite.render(canvas, &mut atlas, dest) {
errors.write(TextureError::RenderFailed(format!("Failed to render life sprite: {}", e)).into());
}
}
}
Err(e) => {
errors.write(e.into());
}
}
// Render score text // Render score text
let score_text = format!("{:02}", score.0); let score_text = format!("{:02}", score.0);
let score_offset = 7 - (score_text.len() as i32); let score_offset = 7 - (score_text.len() as i32);