feat: add experimental game engine & demo game, fix ACPI events

This commit is contained in:
2026-03-18 14:26:50 +01:00
parent ffffb6a0c5
commit b5d2cc515c
42 changed files with 2237 additions and 12 deletions
+81
View File
@@ -0,0 +1,81 @@
/*
* audio.h
* MontaukOS 2D Game Engine - Audio System
* Wraps MontaukOS audio syscalls for game sound playback
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <montauk/syscall.h>
namespace engine {
static constexpr int AUDIO_SAMPLE_RATE = 44100;
static constexpr int AUDIO_CHANNELS = 2;
static constexpr int AUDIO_BITS = 16;
struct AudioEngine {
int handle = -1;
int volume = 80;
bool init() {
handle = montauk::audio_open(AUDIO_SAMPLE_RATE, AUDIO_CHANNELS, AUDIO_BITS);
if (handle < 0) return false;
montauk::audio_set_volume(handle, volume);
return true;
}
void shutdown() {
if (handle >= 0) {
montauk::audio_close(handle);
handle = -1;
}
}
void set_volume(int percent) {
volume = percent;
if (handle >= 0)
montauk::audio_set_volume(handle, volume);
}
// Write raw PCM samples to the audio device.
// data: signed 16-bit stereo PCM samples
// size: number of bytes
bool write_pcm(const void* data, uint32_t size) {
if (handle < 0) return false;
return montauk::audio_write(handle, data, size) >= 0;
}
// Generate and play a simple tone (square wave) for duration_ms.
// Useful for basic sound effects. Non-blocking (writes samples to buffer).
void play_tone(int freq_hz, int duration_ms, int tone_volume = 50) {
if (handle < 0 || freq_hz <= 0) return;
int total_samples = (AUDIO_SAMPLE_RATE * duration_ms) / 1000;
int half_period = AUDIO_SAMPLE_RATE / (2 * freq_hz);
if (half_period <= 0) half_period = 1;
int16_t amp = (int16_t)(327 * tone_volume / 100); // ~1% of max
static constexpr int CHUNK = 512;
int16_t buf[CHUNK * 2]; // stereo
int written = 0;
while (written < total_samples) {
int n = total_samples - written;
if (n > CHUNK) n = CHUNK;
for (int i = 0; i < n; i++) {
int sample_pos = written + i;
int16_t val = ((sample_pos / half_period) % 2 == 0) ? amp : -amp;
buf[i * 2] = val;
buf[i * 2 + 1] = val;
}
montauk::audio_write(handle, buf, n * 4);
written += n;
}
}
};
} // namespace engine
+48
View File
@@ -0,0 +1,48 @@
/*
* collision.h
* MontaukOS 2D Game Engine - Collision Detection
* AABB overlap testing and tile-based collision response
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include "engine/tilemap.h"
namespace engine {
struct AABB {
float x, y, w, h;
bool overlaps(const AABB& other) const {
return x < other.x + other.w && x + w > other.x &&
y < other.y + other.h && y + h > other.y;
}
bool contains_point(float px, float py) const {
return px >= x && px < x + w && py >= y && py < y + h;
}
};
// Try to move an entity by (dx, dy) on the tilemap.
// Uses separate X/Y axis resolution so the entity slides along walls.
// box: the entity's collision box in world pixel coordinates.
// Returns adjusted dx, dy after collision.
inline void resolve_tilemap_collision(const Tilemap& map,
float bx, float by, float bw, float bh,
float& dx, float& dy) {
// Try X movement
float new_x = bx + dx;
if (map.collides((int)new_x, (int)by, (int)bw, (int)bh)) {
dx = 0.0f;
new_x = bx;
}
// Try Y movement
float new_y = by + dy;
if (map.collides((int)new_x, (int)new_y, (int)bw, (int)bh)) {
dy = 0.0f;
}
}
} // namespace engine
+200
View File
@@ -0,0 +1,200 @@
/*
* engine.h
* MontaukOS 2D Game Engine - Core
* Window management, timing, pixel buffer helpers
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <montauk/syscall.h>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <gui/gui.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <stdio.h>
}
namespace engine {
// ============================================================================
// Core engine
// ============================================================================
struct Engine {
int win_id = -1;
uint32_t* pixels = nullptr;
int screen_w = 0;
int screen_h = 0;
// Timing
uint64_t last_time_ms = 0;
float dt = 0.016f; // delta time in seconds
uint64_t frame_count = 0;
// Font
gui::TrueTypeFont* font = nullptr;
bool running = true;
// ---- Lifecycle ----
bool init(const char* title, int w, int h) {
screen_w = w;
screen_h = h;
// Load font
font = (gui::TrueTypeFont*)montauk::malloc(sizeof(gui::TrueTypeFont));
if (font) {
montauk::memset(font, 0, sizeof(gui::TrueTypeFont));
if (!font->init("0:/fonts/Roboto-Medium.ttf")) {
montauk::mfree(font);
font = nullptr;
}
}
// Create window
Montauk::WinCreateResult wres;
if (montauk::win_create(title, w, h, &wres) < 0 || wres.id < 0)
return false;
win_id = wres.id;
pixels = (uint32_t*)(uintptr_t)wres.pixelVa;
last_time_ms = montauk::get_milliseconds();
return true;
}
void update_timing() {
uint64_t now = montauk::get_milliseconds();
uint64_t elapsed = now - last_time_ms;
if (elapsed == 0) elapsed = 1;
if (elapsed > 100) elapsed = 100; // cap to avoid spiral
dt = (float)elapsed / 1000.0f;
last_time_ms = now;
frame_count++;
}
// Poll one window event. Returns true if an event was received.
bool poll(Montauk::WinEvent* ev) {
int r = montauk::win_poll(win_id, ev);
if (r < 0) { running = false; return false; }
if (r == 0) return false;
if (ev->type == 3) { running = false; return false; } // close
if (ev->type == 2) { // resize
screen_w = ev->resize.w;
screen_h = ev->resize.h;
pixels = (uint32_t*)(uintptr_t)montauk::win_resize(
win_id, screen_w, screen_h);
}
return true;
}
void present() {
montauk::win_present(win_id);
}
void shutdown() {
if (win_id >= 0) montauk::win_destroy(win_id);
win_id = -1;
}
// ---- Drawing helpers ----
void clear(uint32_t color) {
int count = screen_w * screen_h;
for (int i = 0; i < count; i++)
pixels[i] = color;
}
void fill_rect(int x, int y, int w, int h, uint32_t color) {
int x0 = x < 0 ? 0 : x;
int y0 = y < 0 ? 0 : y;
int x1 = x + w > screen_w ? screen_w : x + w;
int y1 = y + h > screen_h ? screen_h : y + h;
for (int row = y0; row < y1; row++)
for (int col = x0; col < x1; col++)
pixels[row * screen_w + col] = color;
}
void fill_rect_alpha(int x, int y, int w, int h, uint32_t color) {
uint8_t sa = (color >> 24) & 0xFF;
uint8_t sr = (color >> 16) & 0xFF;
uint8_t sg = (color >> 8) & 0xFF;
uint8_t sb = color & 0xFF;
if (sa == 0) return;
int x0 = x < 0 ? 0 : x;
int y0 = y < 0 ? 0 : y;
int x1 = x + w > screen_w ? screen_w : x + w;
int y1 = y + h > screen_h ? screen_h : y + h;
for (int row = y0; row < y1; row++) {
for (int col = x0; col < x1; col++) {
uint32_t dst = pixels[row * screen_w + col];
uint8_t dr = (dst >> 16) & 0xFF;
uint8_t dg = (dst >> 8) & 0xFF;
uint8_t db = dst & 0xFF;
uint32_t inv = 255 - sa;
uint32_t rr = (sa * sr + inv * dr + 128) / 255;
uint32_t gg = (sa * sg + inv * dg + 128) / 255;
uint32_t bb = (sa * sb + inv * db + 128) / 255;
pixels[row * screen_w + col] =
0xFF000000 | (rr << 16) | (gg << 8) | bb;
}
}
}
void draw_rect_outline(int x, int y, int w, int h, uint32_t color) {
fill_rect(x, y, w, 1, color);
fill_rect(x, y + h - 1, w, 1, color);
fill_rect(x, y, 1, h, color);
fill_rect(x + w - 1, y, 1, h, color);
}
void draw_text(int x, int y, const char* text, gui::Color c, int size = 16) {
if (font)
font->draw_to_buffer(pixels, screen_w, screen_h, x, y, text, c, size);
}
int text_width(const char* text, int size = 16) {
return font ? font->measure_text(text, size) : 0;
}
int text_height(int size = 16) {
if (!font) return size;
auto* gc = font->get_cache(size);
if (!gc) return size;
return gc->ascent - gc->descent;
}
};
// ============================================================================
// File loading helper
// ============================================================================
struct FileData {
uint8_t* data = nullptr;
uint64_t size = 0;
bool load(const char* vfs_path) {
int fd = montauk::open(vfs_path);
if (fd < 0) return false;
size = montauk::getsize(fd);
if (size == 0 || size > 16 * 1024 * 1024) {
montauk::close(fd);
return false;
}
data = (uint8_t*)montauk::malloc(size);
if (!data) { montauk::close(fd); return false; }
montauk::read(fd, data, 0, size);
montauk::close(fd);
return true;
}
void free() {
if (data) { montauk::mfree(data); data = nullptr; }
size = 0;
}
};
} // namespace engine
+87
View File
@@ -0,0 +1,87 @@
/*
* input.h
* MontaukOS 2D Game Engine - Input State
* Keyboard and mouse state tracking across frames
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <montauk/string.h>
#include <Api/Syscall.hpp>
namespace engine {
// Scancode constants for common keys
namespace key {
static constexpr uint8_t ESC = 0x01;
static constexpr uint8_t W = 0x11;
static constexpr uint8_t A = 0x1E;
static constexpr uint8_t S = 0x1F;
static constexpr uint8_t D = 0x20;
static constexpr uint8_t E = 0x12;
static constexpr uint8_t SPACE = 0x39;
static constexpr uint8_t UP = 0x48;
static constexpr uint8_t DOWN = 0x50;
static constexpr uint8_t LEFT = 0x4B;
static constexpr uint8_t RIGHT = 0x4D;
static constexpr uint8_t ENTER = 0x1C;
static constexpr uint8_t TAB = 0x0F;
}
struct InputState {
// Key states: true = currently held
bool keys[256];
// Keys pressed this frame (edge-triggered)
bool keys_pressed[256];
// Mouse
int mouse_x = 0;
int mouse_y = 0;
int mouse_scroll = 0;
uint8_t mouse_buttons = 0;
uint8_t prev_mouse_buttons = 0;
void init() {
montauk::memset(keys, 0, sizeof(keys));
montauk::memset(keys_pressed, 0, sizeof(keys_pressed));
}
// Call at the start of each frame to clear per-frame state
void begin_frame() {
montauk::memset(keys_pressed, 0, sizeof(keys_pressed));
mouse_scroll = 0;
prev_mouse_buttons = mouse_buttons;
}
// Process a window event
void handle_event(const Montauk::WinEvent& ev) {
if (ev.type == 0) { // keyboard
// PS/2 scan code set 1: release codes have bit 7 set.
// Mask to base scancode so press and release map to the same index.
uint8_t sc = ev.key.scancode & 0x7F;
if (ev.key.pressed) {
if (!keys[sc])
keys_pressed[sc] = true;
keys[sc] = true;
} else {
keys[sc] = false;
}
} else if (ev.type == 1) { // mouse
mouse_x = ev.mouse.x;
mouse_y = ev.mouse.y;
mouse_scroll = ev.mouse.scroll;
mouse_buttons = ev.mouse.buttons;
}
}
bool key_held(uint8_t scancode) const { return keys[scancode]; }
bool key_just_pressed(uint8_t scancode) const { return keys_pressed[scancode]; }
bool mouse_clicked() const {
return (mouse_buttons & 1) && !(prev_mouse_buttons & 1);
}
bool mouse_held() const { return mouse_buttons & 1; }
};
} // namespace engine
+264
View File
@@ -0,0 +1,264 @@
/*
* sprite.h
* MontaukOS 2D Game Engine - Sprite and Animation System
* PNG spritesheet loading, frame extraction, alpha-blended rendering
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <montauk/heap.h>
#include <montauk/string.h>
#include "engine/engine.h"
extern "C" {
#include <gui/stb_image.h>
}
namespace engine {
// ============================================================================
// Convert stb_image RGBA output to MontaukOS ARGB pixel format
// stb on little-endian x86: pixel bytes in memory are R,G,B,A
// When read as uint32_t: 0xAABBGGRR
// MontaukOS format: 0xAARRGGBB
// ============================================================================
inline void rgba_to_argb(uint32_t* data, int count) {
for (int i = 0; i < count; i++) {
uint32_t px = data[i];
uint8_t r = px & 0xFF;
uint8_t g = (px >> 8) & 0xFF;
uint8_t b = (px >> 16) & 0xFF;
uint8_t a = (px >> 24) & 0xFF;
data[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) |
((uint32_t)g << 8) | b;
}
}
// ============================================================================
// Spritesheet
// ============================================================================
struct Spritesheet {
uint32_t* pixels = nullptr;
int width = 0;
int height = 0;
int frame_w = 0;
int frame_h = 0;
int cols = 0;
int rows = 0;
// Load a PNG spritesheet from VFS and split into frames of given size.
// If frame_w/frame_h are 0, treat the entire image as a single frame.
bool load(const char* vfs_path, int fw = 0, int fh = 0) {
FileData file;
if (!file.load(vfs_path)) return false;
int w, h, channels;
uint32_t* img = (uint32_t*)stbi_load_from_memory(
file.data, (int)file.size, &w, &h, &channels, 4);
file.free();
if (!img) return false;
// Convert RGBA to ARGB
rgba_to_argb(img, w * h);
pixels = img;
width = w;
height = h;
frame_w = fw > 0 ? fw : w;
frame_h = fh > 0 ? fh : h;
cols = w / frame_w;
rows = h / frame_h;
return true;
}
void unload() {
if (pixels) {
stbi_image_free(pixels);
pixels = nullptr;
}
}
// Blit a single frame to the destination buffer with scaling and alpha.
// frame_col/frame_row select which frame from the spritesheet.
// dst_x/dst_y is the screen position. scale is the integer scale factor.
// flip_h mirrors the sprite horizontally.
void draw_frame(uint32_t* dst, int dst_w, int dst_h,
int frame_col, int frame_row,
int dst_x, int dst_y, int scale = 1,
bool flip_h = false) const {
if (!pixels) return;
if (frame_col < 0 || frame_col >= cols) return;
if (frame_row < 0 || frame_row >= rows) return;
int src_ox = frame_col * frame_w;
int src_oy = frame_row * frame_h;
int out_w = frame_w * scale;
int out_h = frame_h * scale;
for (int py = 0; py < out_h; py++) {
int dy = dst_y + py;
if (dy < 0 || dy >= dst_h) continue;
int sy = src_oy + py / scale;
for (int px = 0; px < out_w; px++) {
int dx = dst_x + px;
if (dx < 0 || dx >= dst_w) continue;
int sx_local = px / scale;
if (flip_h) sx_local = frame_w - 1 - sx_local;
int sx = src_ox + sx_local;
uint32_t src_px = pixels[sy * width + sx];
uint8_t sa = (src_px >> 24) & 0xFF;
if (sa == 0) continue;
if (sa == 255) {
dst[dy * dst_w + dx] = src_px;
} else {
uint32_t d = dst[dy * dst_w + dx];
uint8_t sr = (src_px >> 16) & 0xFF;
uint8_t sg = (src_px >> 8) & 0xFF;
uint8_t sb = src_px & 0xFF;
uint8_t dr = (d >> 16) & 0xFF;
uint8_t dg = (d >> 8) & 0xFF;
uint8_t db = d & 0xFF;
uint32_t inv = 255 - sa;
uint32_t rr = (sa * sr + inv * dr + 128) / 255;
uint32_t gg = (sa * sg + inv * dg + 128) / 255;
uint32_t bb = (sa * sb + inv * db + 128) / 255;
dst[dy * dst_w + dx] =
0xFF000000 | (rr << 16) | (gg << 8) | bb;
}
}
}
}
// Draw an arbitrary sub-rectangle of the spritesheet (not frame-aligned)
void draw_region(uint32_t* dst, int dst_w, int dst_h,
int src_x, int src_y, int src_w, int src_h,
int dst_x, int dst_y, int scale = 1) const {
if (!pixels) return;
int out_w = src_w * scale;
int out_h = src_h * scale;
for (int py = 0; py < out_h; py++) {
int dy = dst_y + py;
if (dy < 0 || dy >= dst_h) continue;
int sy = src_y + py / scale;
if (sy < 0 || sy >= height) continue;
for (int px = 0; px < out_w; px++) {
int dx = dst_x + px;
if (dx < 0 || dx >= dst_w) continue;
int sx = src_x + px / scale;
if (sx < 0 || sx >= width) continue;
uint32_t src_px = pixels[sy * width + sx];
uint8_t sa = (src_px >> 24) & 0xFF;
if (sa == 0) continue;
if (sa == 255) {
dst[dy * dst_w + dx] = src_px;
} else {
uint32_t d = dst[dy * dst_w + dx];
uint8_t sr = (src_px >> 16) & 0xFF;
uint8_t sg = (src_px >> 8) & 0xFF;
uint8_t sb = src_px & 0xFF;
uint8_t dr = (d >> 16) & 0xFF;
uint8_t dg = (d >> 8) & 0xFF;
uint8_t db = d & 0xFF;
uint32_t inv = 255 - sa;
uint32_t rr = (sa * sr + inv * dr + 128) / 255;
uint32_t gg = (sa * sg + inv * dg + 128) / 255;
uint32_t bb = (sa * sb + inv * db + 128) / 255;
dst[dy * dst_w + dx] =
0xFF000000 | (rr << 16) | (gg << 8) | bb;
}
}
}
}
};
// ============================================================================
// Animation
// ============================================================================
struct Animation {
int row = 0; // spritesheet row for this animation
int start_col = 0; // first frame column
int num_frames = 1; // number of frames
float speed = 8.0f; // frames per second
float timer = 0.0f;
int current = 0;
void update(float dt) {
timer += dt * speed;
while (timer >= 1.0f) {
timer -= 1.0f;
current++;
if (current >= num_frames) current = 0;
}
}
void reset() {
timer = 0.0f;
current = 0;
}
int frame_col() const { return start_col + current; }
int frame_row() const { return row; }
};
// ============================================================================
// Animated Sprite - combines a spritesheet with named animations
// ============================================================================
static constexpr int MAX_ANIMS = 16;
struct AnimatedSprite {
Spritesheet* sheet = nullptr;
Animation anims[MAX_ANIMS];
int anim_count = 0;
int current_anim = 0;
bool flip_h = false;
int add_anim(int row, int start_col, int num_frames, float speed = 8.0f) {
if (anim_count >= MAX_ANIMS) return -1;
int idx = anim_count++;
anims[idx].row = row;
anims[idx].start_col = start_col;
anims[idx].num_frames = num_frames;
anims[idx].speed = speed;
return idx;
}
void play(int anim_idx) {
if (anim_idx < 0 || anim_idx >= anim_count) return;
if (current_anim != anim_idx) {
current_anim = anim_idx;
anims[current_anim].reset();
}
}
void update(float dt) {
if (current_anim >= 0 && current_anim < anim_count)
anims[current_anim].update(dt);
}
void draw(uint32_t* dst, int dst_w, int dst_h,
int x, int y, int scale = 1) const {
if (!sheet || current_anim < 0 || current_anim >= anim_count) return;
const Animation& a = anims[current_anim];
sheet->draw_frame(dst, dst_w, dst_h,
a.frame_col(), a.frame_row(),
x, y, scale, flip_h);
}
};
} // namespace engine
+134
View File
@@ -0,0 +1,134 @@
/*
* tilemap.h
* MontaukOS 2D Game Engine - Tile Map
* Grid-based terrain with per-tile rendering and collision
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include <montauk/heap.h>
#include <montauk/string.h>
#include "engine/sprite.h"
namespace engine {
static constexpr int MAX_TILE_TYPES = 16;
struct TileType {
Spritesheet* sheet; // tile image (may be a single-tile spritesheet)
int src_x, src_y; // source position within the sheet
int src_w, src_h; // source size (typically tile_size x tile_size)
bool solid; // blocks movement
};
struct Tilemap {
int* data = nullptr;
int map_w = 0;
int map_h = 0;
int tile_size = 16; // native tile size in pixels
TileType types[MAX_TILE_TYPES];
int type_count = 0;
bool alloc(int w, int h, int ts = 16) {
map_w = w;
map_h = h;
tile_size = ts;
data = (int*)montauk::malloc(w * h * sizeof(int));
if (!data) return false;
montauk::memset(data, 0, w * h * sizeof(int));
return true;
}
void free_map() {
if (data) { montauk::mfree(data); data = nullptr; }
}
// Register a tile type. Returns the tile ID.
int add_type(Spritesheet* sheet, int sx, int sy, int sw, int sh, bool solid) {
if (type_count >= MAX_TILE_TYPES) return -1;
int id = type_count++;
types[id].sheet = sheet;
types[id].src_x = sx;
types[id].src_y = sy;
types[id].src_w = sw;
types[id].src_h = sh;
types[id].solid = solid;
return id;
}
void set(int x, int y, int tile_id) {
if (x >= 0 && x < map_w && y >= 0 && y < map_h)
data[y * map_w + x] = tile_id;
}
int get(int x, int y) const {
if (x < 0 || x >= map_w || y < 0 || y >= map_h) return -1;
return data[y * map_w + x];
}
bool is_solid(int x, int y) const {
int id = get(x, y);
if (id < 0 || id >= type_count) return true; // out of bounds = solid
return types[id].solid;
}
// Check if a world-pixel rectangle collides with any solid tile.
// World coordinates are in native (unscaled) pixels.
bool collides(int wx, int wy, int ww, int wh) const {
// Negative coordinates are always solid (out of bounds)
if (wx < 0 || wy < 0) return true;
int tx0 = wx / tile_size;
int ty0 = wy / tile_size;
int tx1 = (wx + ww - 1) / tile_size;
int ty1 = (wy + wh - 1) / tile_size;
for (int ty = ty0; ty <= ty1; ty++)
for (int tx = tx0; tx <= tx1; tx++)
if (is_solid(tx, ty))
return true;
return false;
}
// Draw visible tiles to the pixel buffer.
// cam_x/cam_y: camera position in world pixels (native scale).
// scale: rendering scale factor.
void draw(uint32_t* dst, int dst_w, int dst_h,
int cam_x, int cam_y, int scale) const {
if (!data) return;
int ts = tile_size * scale;
// Determine visible tile range
int tx0 = cam_x / tile_size;
int ty0 = cam_y / tile_size;
int tx1 = tx0 + dst_w / ts + 2;
int ty1 = ty0 + dst_h / ts + 2;
if (tx0 < 0) tx0 = 0;
if (ty0 < 0) ty0 = 0;
if (tx1 > map_w) tx1 = map_w;
if (ty1 > map_h) ty1 = map_h;
for (int ty = ty0; ty < ty1; ty++) {
for (int tx = tx0; tx < tx1; tx++) {
int id = data[ty * map_w + tx];
if (id < 0 || id >= type_count) continue;
const TileType& tt = types[id];
if (!tt.sheet) continue;
int sx = tx * ts - cam_x * scale;
int sy = ty * ts - cam_y * scale;
tt.sheet->draw_region(dst, dst_w, dst_h,
tt.src_x, tt.src_y,
tt.src_w, tt.src_h,
sx, sy, scale);
}
}
}
};
} // namespace engine
+114
View File
@@ -0,0 +1,114 @@
/*
* ui.h
* MontaukOS 2D Game Engine - UI Rendering
* Health bars, text overlays, dialog boxes, menus
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <cstdint>
#include "engine/engine.h"
extern "C" {
#include <stdio.h>
}
namespace engine {
// ============================================================================
// Health / stat bar
// ============================================================================
inline void draw_bar(Engine& eng, int x, int y, int w, int h,
int value, int max_value,
uint32_t fill_color, uint32_t bg_color,
uint32_t border_color) {
eng.fill_rect(x, y, w, h, bg_color);
if (max_value > 0 && value > 0) {
int fw = (value * (w - 2)) / max_value;
if (fw < 1) fw = 1;
eng.fill_rect(x + 1, y + 1, fw, h - 2, fill_color);
}
eng.draw_rect_outline(x, y, w, h, border_color);
}
// ============================================================================
// Text box / dialog
// ============================================================================
inline void draw_dialog(Engine& eng, int x, int y, int w, int h,
const char* text, gui::Color text_color,
int font_size = 16) {
// Background with border
eng.fill_rect(x, y, w, h, 0xF0FFFFFF);
eng.draw_rect_outline(x, y, w, h, 0xFF333333);
eng.draw_rect_outline(x + 1, y + 1, w - 2, h - 2, 0xFFCCCCCC);
// Text (with word wrapping)
int tx = x + 8;
int ty = y + 6;
int max_w = w - 16;
int line_h = eng.text_height(font_size) + 2;
// Simple word wrap
char line_buf[128];
int li = 0;
const char* p = text;
while (*p) {
li = 0;
while (*p && *p != '\n') {
// Save state before adding next word
int word_start = li;
const char* word_p = p;
while (*p && *p != ' ' && *p != '\n' && li < 126) {
line_buf[li++] = *p++;
}
line_buf[li] = '\0';
// Check width
if (eng.text_width(line_buf, font_size) > max_w && word_start > 0) {
// Line too long - rewind to before this word
li = word_start;
if (li > 0 && line_buf[li - 1] == ' ') li--;
line_buf[li] = '\0';
p = word_p; // rewind pointer to retry this word on next line
break;
}
if (*p == ' ') { line_buf[li++] = ' '; p++; }
}
line_buf[li] = '\0';
if (*p == '\n') p++;
if (ty + line_h > y + h - 4) break;
eng.draw_text(tx, ty, line_buf, text_color, font_size);
ty += line_h;
}
}
// ============================================================================
// Prompt bar (bottom of screen)
// ============================================================================
inline void draw_prompt(Engine& eng, const char* text, int font_size = 14) {
int bar_h = eng.text_height(font_size) + 8;
int y = eng.screen_h - bar_h;
eng.fill_rect_alpha(0, y, eng.screen_w, bar_h, 0xCC000000);
int tw = eng.text_width(text, font_size);
eng.draw_text((eng.screen_w - tw) / 2, y + 4, text,
gui::Color::from_rgb(0xFF, 0xFF, 0xFF), font_size);
}
// ============================================================================
// Simple HUD text
// ============================================================================
inline void draw_hud_text(Engine& eng, int x, int y,
const char* text, gui::Color c, int size = 14) {
// Draw shadow first for readability
eng.draw_text(x + 1, y + 1, text, gui::Color::from_rgba(0, 0, 0, 180), size);
eng.draw_text(x, y, text, c, size);
}
} // namespace engine