neurodoom 0.6.7

Deterministic no_std Doom engine with semantic and depth perception buffers for AI
Documentation
//! HUD overlay — health, armor, ammo readout drawn into the indexed
//! framebuffer after the 3D scene and weapon sprite.
//!
//! Pure pixel drawing on `Renderer::screen`. Inputs arrive as plain POD
//! (see `HudFrame`) so this module has no dependency on `World` or
//! `Entity` — the engine's `render_for` gathers the data first, then
//! hands it off here.

// HUD pixel stamping is pre-bounds-checked per-pixel — see rationale in
// src/render/mod.rs.
#![allow(clippy::indexing_slicing)]

use crate::math::{SCREENHEIGHT, SCREENWIDTH};
use crate::types::WeaponType;

use super::Renderer;

/// Inputs for one HUD frame. Built by the engine from the viewing
/// peer's entity and player state, then passed to `Renderer::draw_hud`.
#[derive(Clone, Copy, Debug, Default)]
pub struct HudFrame {
    /// Current player health. Negative values are clamped to 0 for display.
    pub health: i32,
    /// Current armor points. `0` suppresses the armor row.
    pub armor: i32,
    /// Current ammo counts [bullets, shells, cells, rockets].
    pub ammo: [i32; 4],
    /// Which ammo type the ready weapon uses (for highlighting). `None`
    /// means the weapon uses no ammo (fist, chainsaw).
    pub ammo_active: Option<usize>,
}

impl HudFrame {
    /// Build from a player's ready weapon + stats. Convenience for the
    /// engine's render_for path.
    pub fn from_stats(
        health: i32,
        armor: i32,
        ammo: [i32; 4],
        ready_weapon: WeaponType,
    ) -> Self {
        Self {
            health,
            armor,
            ammo,
            ammo_active: weapon_ammo_type(ready_weapon),
        }
    }
}

impl Renderer {
    /// Paint the HUD overlay onto the indexed framebuffer. Call after
    /// the 3D scene and weapon psprite, before `indexed_to_rgba`.
    pub fn draw_hud(&mut self, hud: HudFrame) {
        let screen = &mut self.screen;

        let health = hud.health.max(0) as u32;
        // Health: bottom-left with cross icon.
        draw_icon(screen, 4, SCREENHEIGHT - 12, &ICON_CROSS, 176); // red
        draw_number(screen, 14, SCREENHEIGHT - 11, health, 3);

        // Ammo: bottom-right with icons, active slot highlighted.
        let icons: [&[u8; 25]; 4] = [&ICON_BULLET, &ICON_SHELL, &ICON_CELL, &ICON_ROCKET];
        let x_base = SCREENWIDTH - 56;
        let y_num = SCREENHEIGHT - 8;
        for (i, icon) in icons.iter().enumerate() {
            let x = x_base + i * 14;
            let is_active = hud.ammo_active == Some(i);
            let color = if is_active { 4u8 } else { 96 };
            draw_icon5x5(screen, x + 1, y_num - 6, icon, color);
            let val = hud.ammo.get(i).copied().unwrap_or(0).max(0) as u32;
            draw_small_number(screen, x, y_num, val, 3, color);
        }

        // Armor: bottom-left, above health, only when > 0.
        if hud.armor > 0 {
            let armor = hud.armor as u32;
            draw_icon(screen, 4, SCREENHEIGHT - 22, &ICON_SHIELD, 192); // blue-ish
            draw_number(screen, 14, SCREENHEIGHT - 21, armor, 3);
        }
    }
}

/// Which ammo slot (0..=3) does `weapon` consume? `None` = no ammo.
fn weapon_ammo_type(weapon: WeaponType) -> Option<usize> {
    match weapon {
        WeaponType::Fist | WeaponType::Chainsaw => None,
        WeaponType::Pistol | WeaponType::Chaingun => Some(0), // Bullets
        WeaponType::Shotgun | WeaponType::SuperShotgun => Some(1), // Shells
        WeaponType::PlasmaRifle | WeaponType::Bfg => Some(2), // Cells
        WeaponType::RocketLauncher => Some(3),                // Rockets
    }
}

// --- Icon bitmaps ------------------------------------------------------

/// 7x7 cross (health icon).
#[rustfmt::skip]
const ICON_CROSS: [u8; 49] = [
    0,0,0,1,0,0,0,
    0,0,0,1,0,0,0,
    0,0,0,1,0,0,0,
    1,1,1,1,1,1,1,
    0,0,0,1,0,0,0,
    0,0,0,1,0,0,0,
    0,0,0,1,0,0,0,
];

/// 7x7 shield (armor icon).
#[rustfmt::skip]
const ICON_SHIELD: [u8; 49] = [
    0,1,1,1,1,1,0,
    1,1,1,1,1,1,1,
    1,1,0,1,0,1,1,
    1,1,1,1,1,1,1,
    0,1,1,1,1,1,0,
    0,0,1,1,1,0,0,
    0,0,0,1,0,0,0,
];

/// 5x5 ammo-type icons.
#[rustfmt::skip]
const ICON_BULLET: [u8; 25] = [
    0,0,1,0,0,
    0,1,1,1,0,
    0,1,1,1,0,
    0,1,1,1,0,
    0,1,1,1,0,
];
#[rustfmt::skip]
const ICON_SHELL: [u8; 25] = [
    0,1,1,0,0,
    1,1,1,1,0,
    1,1,1,1,0,
    1,1,1,1,0,
    0,1,1,0,0,
];
#[rustfmt::skip]
const ICON_CELL: [u8; 25] = [
    1,1,1,1,1,
    1,0,1,0,1,
    1,1,1,1,1,
    1,0,1,0,1,
    1,1,1,1,1,
];
#[rustfmt::skip]
const ICON_ROCKET: [u8; 25] = [
    0,0,1,0,0,
    0,1,1,1,0,
    0,1,1,1,0,
    1,1,1,1,1,
    0,1,0,1,0,
];

/// 3x5 tiny font for ammo counts.
#[rustfmt::skip]
const TINY_DIGITS: [[u8; 15]; 10] = [
    [1,1,1, 1,0,1, 1,0,1, 1,0,1, 1,1,1], // 0
    [0,1,0, 1,1,0, 0,1,0, 0,1,0, 1,1,1], // 1
    [1,1,1, 0,0,1, 1,1,1, 1,0,0, 1,1,1], // 2
    [1,1,1, 0,0,1, 1,1,1, 0,0,1, 1,1,1], // 3
    [1,0,1, 1,0,1, 1,1,1, 0,0,1, 0,0,1], // 4
    [1,1,1, 1,0,0, 1,1,1, 0,0,1, 1,1,1], // 5
    [1,1,1, 1,0,0, 1,1,1, 1,0,1, 1,1,1], // 6
    [1,1,1, 0,0,1, 0,0,1, 0,0,1, 0,0,1], // 7
    [1,1,1, 1,0,1, 1,1,1, 1,0,1, 1,1,1], // 8
    [1,1,1, 1,0,1, 1,1,1, 0,0,1, 1,1,1], // 9
];

/// 4x7 large font for health / armor.
#[rustfmt::skip]
const BIG_DIGITS: [[u8; 28]; 10] = [
    [1,1,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0, 1,1,1,0],
    [0,1,0,0, 1,1,0,0, 0,1,0,0, 0,1,0,0, 0,1,0,0, 0,1,0,0, 1,1,1,0],
    [1,1,1,0, 0,0,1,0, 0,0,1,0, 1,1,1,0, 1,0,0,0, 1,0,0,0, 1,1,1,0],
    [1,1,1,0, 0,0,1,0, 0,0,1,0, 1,1,1,0, 0,0,1,0, 0,0,1,0, 1,1,1,0],
    [1,0,1,0, 1,0,1,0, 1,0,1,0, 1,1,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
    [1,1,1,0, 1,0,0,0, 1,0,0,0, 1,1,1,0, 0,0,1,0, 0,0,1,0, 1,1,1,0],
    [1,1,1,0, 1,0,0,0, 1,0,0,0, 1,1,1,0, 1,0,1,0, 1,0,1,0, 1,1,1,0],
    [1,1,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
    [1,1,1,0, 1,0,1,0, 1,0,1,0, 1,1,1,0, 1,0,1,0, 1,0,1,0, 1,1,1,0],
    [1,1,1,0, 1,0,1,0, 1,0,1,0, 1,1,1,0, 0,0,1,0, 0,0,1,0, 1,1,1,0],
];

// --- Pixel stamping ----------------------------------------------------

fn draw_icon5x5(screen: &mut [u8], x: usize, y: usize, icon: &[u8; 25], color: u8) {
    stamp(screen, x, y, 5, 5, icon, color);
}

fn draw_icon(screen: &mut [u8], x: usize, y: usize, icon: &[u8; 49], color: u8) {
    stamp(screen, x, y, 7, 7, icon, color);
}

#[inline]
fn stamp(screen: &mut [u8], x: usize, y: usize, w: usize, h: usize, icon: &[u8], color: u8) {
    for py in 0..h {
        for px in 0..w {
            if icon[py * w + px] != 0 {
                let sx = x + px;
                let sy = y + py;
                if sx < SCREENWIDTH && sy < SCREENHEIGHT {
                    screen[sy * SCREENWIDTH + sx] = color;
                }
            }
        }
    }
}

/// Right-aligned `digits`-wide decimal number using the tiny font.
fn draw_small_number(screen: &mut [u8], x: usize, y: usize, value: u32, digits: usize, color: u8) {
    let positions = decompose(value, digits);
    for (d, &pos) in positions.iter().enumerate().take(digits) {
        let digit = pos as usize;
        if digit < TINY_DIGITS.len() {
            stamp(screen, x + d * 4, y, 3, 5, &TINY_DIGITS[digit], color);
        }
    }
}

/// Right-aligned `digits`-wide decimal number using the large font (red).
fn draw_number(screen: &mut [u8], x: usize, y: usize, value: u32, digits: usize) {
    let positions = decompose(value, digits);
    for (d, &pos) in positions.iter().enumerate().take(digits) {
        let digit = pos as usize;
        if digit < BIG_DIGITS.len() {
            stamp(screen, x + d * 5, y, 4, 7, &BIG_DIGITS[digit], 4);
        }
    }
}

/// Split `value` into its decimal digits (most-significant first),
/// padded with leading zeros to `digits` places.
fn decompose(value: u32, digits: usize) -> [u32; 10] {
    let mut out = [0u32; 10];
    let mut val = value;
    for i in 0..digits.min(out.len()) {
        out[digits - 1 - i] = val % 10;
        val /= 10;
    }
    out
}