Something went wrong. Try again.
Homebrew 4D Game Engine In Rust, inspired by Engine4D
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721//! HUD/GUI bindings for Lua//!//! Provides Lua access to the HUD overlay system://! - `hud.text(x, y, text, size, color)` - Draw text at screen position//! - `hud.text_centered(x, y, text, size, color)` - Draw text centered at position//! - `hud.rect(x, y, width, height, color)` - Draw filled rectangle//! - `hud.rect_outline(x, y, width, height, color, stroke)` - Draw rectangle outline//! - `hud.progress_bar(x, y, width, height, progress, bg_color, fill_color)` - Draw progress bar//! - `hud.flash(color)` - Flash the entire screen//! - `hud.screen_size()` - Get screen dimensions//!//! ## Usage (Lua)//!//! ```lua//! -- Example HUD script for a health bar game//! function on_update(dt)//! -- Get screen dimensions//! local sw, sh = hud.screen_size()//!//! -- Draw health bar in top-left//! local health = player:get_health() / player:get_max_health()//! hud.text(10, 10, "Health", 14, {1, 1, 1, 1})//! hud.progress_bar(10, 30, 200, 20, health,//! {0.2, 0.2, 0.2, 0.8}, -- dark gray background//! {0.8, 0.1, 0.1, 1.0}) -- red fill//!//! -- Draw ammo count in top-right//! local ammo_text = string.format("Ammo: %d / %d", current_ammo, max_ammo)//! hud.text(sw - 150, 10, ammo_text, 16, {1, 1, 1, 1})//!//! -- Flash red when taking damage//! if just_took_damage then//! hud.flash({1, 0, 0, 0.3})//! end//! end//! ```//!//! ## Color Format//!//! Colors can be specified as either://! - Array format: `{r, g, b, a}` e.g., `{1, 0, 0, 1}` for red//! - Named format: `{r=1, g=0, b=0, a=1}` for red//!//! All color values are in the 0.0-1.0 range. Alpha defaults to 1.0 if omitted.//!//! ## Coordinate System//!//! All positions are in screen pixels with origin at top-left://! - X increases to the right//! - Y increases downward//!//! ## Design Note//!//! The actual HudContext lives in rust4d_render, not in Lua. This implementation provides//! the binding API structure with stub operations that log debug messages.//! Full integration with the engine's HudContext happens when the engine binary wires up//! the scripting system via `lua.set_app_data()`.//!//! This module is owned by Agent A3 (Lua HUD API Bindings).
use mlua::prelude::*;
/// Screen dimensions configuration for HUD bindings.////// Set this via `lua.set_app_data(ScreenConfig { ... })` to provide/// actual screen dimensions to Lua scripts. If not set, screen_size()/// returns 1920x1080 as a fallback.////// # Example////// ```rust,ignore/// use rust4d_scripting::bindings::hud::ScreenConfig;////// // In your game loop, update the screen config when the window resizes/// lua.set_app_data(ScreenConfig {/// width: window.inner_size().width as f32,/// height: window.inner_size().height as f32,/// });/// ```#[derive(Debug, Clone, Copy)]pub struct ScreenConfig { /// Screen width in logical pixels pub width: f32, /// Screen height in logical pixels pub height: f32,}
impl Default for ScreenConfig { fn default() -> Self { Self { width: 1920.0, height: 1080.0, } }}
/// Register HUD bindings with the Lua VM////// Creates a global `hud` table with the following functions:/// - `hud.text(x, y, text, size, color)` - Draw text at screen position/// - `hud.text_centered(x, y, text, size, color)` - Draw text centered at position/// - `hud.rect(x, y, width, height, color)` - Draw filled rectangle/// - `hud.rect_outline(x, y, width, height, color, stroke)` - Draw rectangle outline/// - `hud.progress_bar(x, y, width, height, progress, bg_color, fill_color)` - Draw progress bar/// - `hud.flash(color)` - Flash the entire screen/// - `hud.screen_size() -> width, height` - Get screen dimensions////// # Stub Implementation////// These functions are currently stubs that log debug messages but don't render./// Full integration requires the engine to provide HudContext via `lua.set_app_data()`.pub fn register(lua: &Lua) -> LuaResult<()> { let hud_table = lua.create_table()?;
// hud.text(x, y, text, size, color) // // Draw text at screen position. // // Arguments: // - x: Horizontal position (pixels from left) // - y: Vertical position (pixels from top) // - text: Text string to display // - size: Font size in points // - color: RGBA color table {r, g, b, a} or {1, 2, 3, 4} hud_table.set( "text", lua.create_function( |_, (x, y, text, size, color): (f32, f32, String, f32, LuaTable)| { let color = table_to_color(&color)?; // STUB: Log at trace level (LOW-6 - called every frame) // Real implementation would: // 1. Get HudContext from lua.app_data() // 2. Call hud.text([x, y], &text, size, color) log::trace!( "[hud] text at ({}, {}): '{}' size={} color={:?}", x, y, text, size, color ); Ok(()) }, )?, )?;
// hud.text_centered(x, y, text, size, color) // // Draw text centered at screen position. // // Arguments: // - x: Center horizontal position (pixels from left) // - y: Center vertical position (pixels from top) // - text: Text string to display // - size: Font size in points // - color: RGBA color table {r, g, b, a} or {1, 2, 3, 4} hud_table.set( "text_centered", lua.create_function( |_, (x, y, text, size, color): (f32, f32, String, f32, LuaTable)| { let color = table_to_color(&color)?; log::trace!( "[hud] text_centered at ({}, {}): '{}' size={} color={:?}", x, y, text, size, color ); Ok(()) }, )?, )?;
// hud.rect(x, y, width, height, color) // // Draw a filled rectangle. // // Arguments: // - x: Left edge position (pixels from left) // - y: Top edge position (pixels from top) // - width: Rectangle width in pixels // - height: Rectangle height in pixels // - color: RGBA fill color table hud_table.set( "rect", lua.create_function(|_, (x, y, w, h, color): (f32, f32, f32, f32, LuaTable)| { let color = table_to_color(&color)?; log::trace!( "[hud] rect at ({}, {}) size=({}, {}) color={:?}", x, y, w, h, color ); Ok(()) })?, )?;
// hud.rect_outline(x, y, width, height, color, stroke) // // Draw a rectangle outline (border only, no fill). // // Arguments: // - x: Left edge position (pixels from left) // - y: Top edge position (pixels from top) // - width: Rectangle width in pixels // - height: Rectangle height in pixels // - color: RGBA stroke color table // - stroke: Stroke width in pixels hud_table.set( "rect_outline", lua.create_function( |_, (x, y, w, h, color, stroke): (f32, f32, f32, f32, LuaTable, f32)| { let color = table_to_color(&color)?; log::trace!( "[hud] rect_outline at ({}, {}) size=({}, {}) stroke={} color={:?}", x, y, w, h, stroke, color ); Ok(()) }, )?, )?;
// hud.progress_bar(x, y, width, height, progress, bg_color, fill_color) // // Draw a progress bar with background and fill. // // Arguments: // - x: Left edge position (pixels from left) // - y: Top edge position (pixels from top) // - width: Bar width in pixels // - height: Bar height in pixels // - progress: Progress value from 0.0 (empty) to 1.0 (full) // - bg_color: RGBA background color table // - fill_color: RGBA fill color table hud_table.set( "progress_bar", lua.create_function( |_, (x, y, w, h, progress, bg_color, fill_color): ( f32, f32, f32, f32, f32, LuaTable, LuaTable, )| { let bg = table_to_color(&bg_color)?; let fill = table_to_color(&fill_color)?; let progress = progress.clamp(0.0, 1.0); log::trace!( "[hud] progress_bar at ({}, {}) size=({}, {}) progress={:.0}% bg={:?} fill={:?}", x, y, w, h, progress * 100.0, bg, fill ); Ok(()) }, )?, )?;
// hud.flash(color) // // Flash the entire screen with a color overlay. // Useful for damage feedback, pickups, transitions, etc. // // Arguments: // - color: RGBA flash color table (alpha controls intensity) hud_table.set( "flash", lua.create_function(|_, color: LuaTable| { let color = table_to_color(&color)?; log::trace!("[hud] flash color={:?}", color); Ok(()) })?, )?;
// hud.screen_size() -> width, height // // Get the current screen dimensions in logical pixels. // // Returns: // - width: Screen width in pixels // - height: Screen height in pixels // // Reads from ScreenConfig if set via app_data, otherwise returns default 1920x1080. hud_table.set( "screen_size", lua.create_function(|lua, ()| { // Try to get ScreenConfig from app_data if let Some(config) = lua.app_data_ref::<ScreenConfig>() { log::trace!( "[hud] screen_size() - returning {}x{}", config.width, config.height ); Ok((config.width, config.height)) } else { // No config set - use default let default = ScreenConfig::default(); log::trace!( "[hud] screen_size() - ScreenConfig not set, using default {}x{}", default.width, default.height ); Ok((default.width, default.height)) } })?, )?;
// Register the hud table as a global lua.globals().set("hud", hud_table)?;
log::debug!("[hud] HUD bindings registered"); Ok(())}
/// Convert a Lua table to an RGBA color array////// Accepts two formats:/// - Array format: `{r, g, b, a}` at indices 1, 2, 3, 4/// - Named format: `{r=r, g=g, b=b, a=a}`////// Alpha defaults to 1.0 if not specified.////// # Warnings////// If using named format (detected by presence of 'r' key), logs a warning if/// any of g, b, or a are missing. This helps catch typos like `{r=1, g=1, bleu=1}`.fn table_to_color(table: &LuaTable) -> LuaResult<[f32; 4]> { // Try named keys first (r, g, b, a) let r: Option<f32> = table.get("r").ok(); if let Some(r) = r { // Named format detected - check for missing components let g_present = table.get::<f32>("g").is_ok(); let b_present = table.get::<f32>("b").is_ok(); let a_present = table.get::<f32>("a").is_ok();
let g: f32 = table.get("g").unwrap_or(0.0); let b: f32 = table.get("b").unwrap_or(0.0); let a: f32 = table.get("a").unwrap_or(1.0);
// Warn about missing components (except 'a' which commonly defaults to 1.0) let mut missing = Vec::new(); if !g_present { missing.push("g"); } if !b_present { missing.push("b"); } if !a_present && a == 1.0 { // Alpha missing is common and defaults to 1.0, only warn at trace level log::trace!("[hud] Color table missing 'a' component, defaulting to 1.0"); }
if !missing.is_empty() { log::warn!( "[hud] Color table using named format but missing component(s): {:?}. \ Typo? Got r={}, g={}, b={}, a={}. Missing components default to 0.0.", missing, r, g, b, a ); }
return Ok([r, g, b, a]); }
// Try array indices {1, 2, 3, 4} let r: f32 = table.get(1).unwrap_or(0.0); let g: f32 = table.get(2).unwrap_or(0.0); let b: f32 = table.get(3).unwrap_or(0.0); let a: f32 = table.get(4).unwrap_or(1.0); Ok([r, g, b, a])}
#[cfg(test)]mod tests { use super::*;
fn create_lua_with_hud() -> Lua { let lua = Lua::new(); register(&lua).expect("Failed to register HUD bindings"); lua }
#[test] fn test_hud_table_exists() { let lua = create_lua_with_hud(); let hud: LuaTable = lua.globals().get("hud").expect("hud table should exist"); assert!(hud.contains_key("text").unwrap()); assert!(hud.contains_key("text_centered").unwrap()); assert!(hud.contains_key("rect").unwrap()); assert!(hud.contains_key("rect_outline").unwrap()); assert!(hud.contains_key("progress_bar").unwrap()); assert!(hud.contains_key("flash").unwrap()); assert!(hud.contains_key("screen_size").unwrap()); }
#[test] fn test_hud_text_callable() { let lua = create_lua_with_hud(); lua.load(r#"hud.text(10, 20, "Hello", 16, {1, 1, 1, 1})"#) .exec() .expect("hud.text should be callable"); }
#[test] fn test_hud_text_with_named_colors() { let lua = create_lua_with_hud(); lua.load(r#"hud.text(10, 20, "Hello", 16, {r=1, g=0.5, b=0, a=1})"#) .exec() .expect("hud.text should accept named color format"); }
#[test] fn test_hud_text_centered_callable() { let lua = create_lua_with_hud(); lua.load(r#"hud.text_centered(400, 300, "Game Over", 32, {1, 0, 0, 1})"#) .exec() .expect("hud.text_centered should be callable"); }
#[test] fn test_hud_rect_callable() { let lua = create_lua_with_hud(); lua.load(r#"hud.rect(0, 0, 100, 50, {0.2, 0.2, 0.2, 0.8})"#) .exec() .expect("hud.rect should be callable"); }
#[test] fn test_hud_rect_outline_callable() { let lua = create_lua_with_hud(); lua.load(r#"hud.rect_outline(50, 50, 200, 100, {1, 1, 1, 1}, 2)"#) .exec() .expect("hud.rect_outline should be callable"); }
#[test] fn test_hud_progress_bar_callable() { let lua = create_lua_with_hud(); lua.load( r#" hud.progress_bar(10, 40, 200, 20, 0.75, {0.2, 0.2, 0.2, 1}, {0, 1, 0, 1}) "#, ) .exec() .expect("hud.progress_bar should be callable"); }
#[test] fn test_hud_progress_bar_clamps_progress() { let lua = create_lua_with_hud(); // Should not error, just clamp values lua.load( r#" hud.progress_bar(10, 40, 200, 20, 2.0, {0.2, 0.2, 0.2, 1}, {0, 1, 0, 1}) hud.progress_bar(10, 40, 200, 20, -1.0, {0.2, 0.2, 0.2, 1}, {0, 1, 0, 1}) "#, ) .exec() .expect("progress_bar should clamp out-of-range progress values"); }
#[test] fn test_hud_flash_callable() { let lua = create_lua_with_hud(); lua.load(r#"hud.flash({1, 0, 0, 0.3})"#) .exec() .expect("hud.flash should be callable"); }
#[test] fn test_hud_screen_size_returns_two_values() { let lua = create_lua_with_hud(); lua.load( r#" local w, h = hud.screen_size() assert(type(w) == 'number', 'width should be number') assert(type(h) == 'number', 'height should be number') assert(w > 0, 'width should be positive') assert(h > 0, 'height should be positive') "#, ) .exec() .expect("hud.screen_size should return two positive numbers"); }
#[test] fn test_color_table_array_format() { let lua = Lua::new(); let table = lua.create_table().unwrap(); table.set(1, 0.5f32).unwrap(); table.set(2, 0.6f32).unwrap(); table.set(3, 0.7f32).unwrap(); table.set(4, 0.8f32).unwrap();
let color = table_to_color(&table).unwrap(); assert!((color[0] - 0.5).abs() < 0.001); assert!((color[1] - 0.6).abs() < 0.001); assert!((color[2] - 0.7).abs() < 0.001); assert!((color[3] - 0.8).abs() < 0.001); }
#[test] fn test_color_table_named_format() { let lua = Lua::new(); let table = lua.create_table().unwrap(); table.set("r", 0.1f32).unwrap(); table.set("g", 0.2f32).unwrap(); table.set("b", 0.3f32).unwrap(); table.set("a", 0.4f32).unwrap();
let color = table_to_color(&table).unwrap(); assert!((color[0] - 0.1).abs() < 0.001); assert!((color[1] - 0.2).abs() < 0.001); assert!((color[2] - 0.3).abs() < 0.001); assert!((color[3] - 0.4).abs() < 0.001); }
#[test] fn test_color_defaults_alpha() { let lua = Lua::new(); let table = lua.create_table().unwrap(); table.set(1, 1.0f32).unwrap(); table.set(2, 0.0f32).unwrap(); table.set(3, 0.0f32).unwrap(); // No alpha specified
let color = table_to_color(&table).unwrap(); assert!( (color[3] - 1.0).abs() < 0.001, "Default alpha should be 1.0" ); }
#[test] fn test_color_named_defaults_missing_components() { let lua = Lua::new(); let table = lua.create_table().unwrap(); table.set("r", 1.0f32).unwrap(); // g, b, a not specified
let color = table_to_color(&table).unwrap(); assert!((color[0] - 1.0).abs() < 0.001); assert!( (color[1] - 0.0).abs() < 0.001, "Missing g should default to 0.0" ); assert!( (color[2] - 0.0).abs() < 0.001, "Missing b should default to 0.0" ); assert!( (color[3] - 1.0).abs() < 0.001, "Missing a should default to 1.0" ); }
#[test] fn test_full_hud_workflow() { let lua = create_lua_with_hud(); lua.load( r#" -- Typical game HUD setup local sw, sh = hud.screen_size()
-- Health bar hud.text(10, 10, "Health", 14, {1, 1, 1, 1}) hud.progress_bar(10, 30, 200, 20, 0.75, {0.2, 0.2, 0.2, 0.8}, {0.8, 0.1, 0.1, 1.0})
-- Ammo counter local ammo_text = string.format("Ammo: %d / %d", 25, 30) hud.text(sw - 150, 10, ammo_text, 16, {1, 1, 1, 1})
-- Stamina bar hud.text(10, 60, "Stamina", 14, {1, 1, 1, 1}) hud.progress_bar(10, 80, 200, 15, 0.5, {0.2, 0.2, 0.2, 0.8}, {0.2, 0.6, 1.0, 1.0})
-- Crosshair (centered) hud.rect(sw/2 - 10, sh/2 - 1, 20, 2, {1, 1, 1, 0.8}) hud.rect(sw/2 - 1, sh/2 - 10, 2, 20, {1, 1, 1, 0.8})
-- Minimap border hud.rect_outline(sw - 160, sh - 160, 150, 150, {1, 1, 1, 0.5}, 2)
-- Game over text (example) -- hud.text_centered(sw/2, sh/2, "GAME OVER", 48, {1, 0, 0, 1})
-- Damage flash (example) -- hud.flash({1, 0, 0, 0.3}) "#, ) .exec() .expect("Full HUD workflow should execute without errors"); }
#[test] fn test_hud_with_special_characters() { let lua = create_lua_with_hud(); lua.load(r#"hud.text(10, 10, "Score: 1,234,567", 18, {1, 1, 1, 1})"#) .exec() .expect("Should handle text with commas");
lua.load(r#"hud.text(10, 10, "Health: 100%", 18, {1, 1, 1, 1})"#) .exec() .expect("Should handle text with percent sign");
lua.load(r#"hud.text(10, 10, "Player \"Hero\"", 18, {1, 1, 1, 1})"#) .exec() .expect("Should handle text with quotes"); }
#[test] fn test_hud_with_empty_string() { let lua = create_lua_with_hud(); lua.load(r#"hud.text(10, 10, "", 16, {1, 1, 1, 1})"#) .exec() .expect("Should handle empty string"); }
#[test] fn test_hud_with_zero_dimensions() { let lua = create_lua_with_hud(); lua.load(r#"hud.rect(10, 10, 0, 0, {1, 0, 0, 1})"#) .exec() .expect("Should handle zero-size rect");
lua.load(r#"hud.progress_bar(10, 10, 0, 0, 0.5, {0.2, 0.2, 0.2, 1}, {0, 1, 0, 1})"#) .exec() .expect("Should handle zero-size progress bar"); }
#[test] fn test_hud_with_negative_positions() { let lua = create_lua_with_hud(); lua.load(r#"hud.text(-10, -20, "Offscreen", 16, {1, 1, 1, 1})"#) .exec() .expect("Should handle negative positions (offscreen)"); }
#[test] fn test_screen_size_reads_from_config() { let lua = Lua::new();
// Set custom screen config lua.set_app_data(ScreenConfig { width: 2560.0, height: 1440.0, });
register(&lua).unwrap();
// Verify screen_size reads from config let (w, h): (f32, f32) = lua .load( r#" return hud.screen_size() "#, ) .eval() .expect("screen_size should work");
assert!( (w - 2560.0).abs() < 0.001, "width should read from ScreenConfig (expected 2560, got {})", w ); assert!( (h - 1440.0).abs() < 0.001, "height should read from ScreenConfig (expected 1440, got {})", h ); }
#[test] fn test_screen_size_default_without_config() { let lua = create_lua_with_hud(); // No ScreenConfig set - should use default 1920x1080
let (w, h): (f32, f32) = lua .load( r#" return hud.screen_size() "#, ) .eval() .expect("screen_size should work");
assert!( (w - 1920.0).abs() < 0.001, "width should use default when no config (expected 1920, got {})", w ); assert!( (h - 1080.0).abs() < 0.001, "height should use default when no config (expected 1080, got {})", h ); }}